diff --git a/.github/workflows/samples-kotlin-server-jdk17.yaml b/.github/workflows/samples-kotlin-server-jdk17.yaml index b5bde97258c5..d7d64fd88957 100644 --- a/.github/workflows/samples-kotlin-server-jdk17.yaml +++ b/.github/workflows/samples-kotlin-server-jdk17.yaml @@ -11,6 +11,7 @@ on: - 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**' - 'samples/server/petstore/kotlin-spring-declarative*/**' - 'samples/server/petstore/kotlin-spring-sealed-interfaces/**' + - 'samples/server/petstore/kotlin-springboot-sort-validation/**' # comment out due to gradle build failure # - samples/server/petstore/kotlin-spring-default/** pull_request: @@ -23,6 +24,7 @@ on: - 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**' - 'samples/server/petstore/kotlin-spring-declarative*/**' - 'samples/server/petstore/kotlin-spring-sealed-interfaces/**' + - 'samples/server/petstore/kotlin-springboot-sort-validation/**' # comment out due to gradle build failure # - samples/server/petstore/kotlin-spring-default/** @@ -62,6 +64,7 @@ jobs: - samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped - samples/server/petstore/kotlin-spring-declarative-interface-wrapped - samples/server/petstore/kotlin-spring-sealed-interfaces + - samples/server/petstore/kotlin-springboot-sort-validation # comment out due to gradle build failure # - samples/server/petstore/kotlin-spring-default/ steps: diff --git a/.github/workflows/samples-spring-jdk17.yaml b/.github/workflows/samples-spring-jdk17.yaml index 72ca29dc8070..86a7ec2fb48e 100644 --- a/.github/workflows/samples-spring-jdk17.yaml +++ b/.github/workflows/samples-spring-jdk17.yaml @@ -14,6 +14,7 @@ on: - samples/server/petstore/springboot-lombok-tostring/** - samples/server/petstore/springboot-file-delegate-optional/** - samples/server/petstore/springboot-petstore-with-api-response-examples/** + - samples/server/petstore/springboot-sort-validation/** - samples/openapi3/server/petstore/spring-boot-oneof-sealed/** - samples/openapi3/server/petstore/spring-boot-oneof-interface/** pull_request: @@ -29,6 +30,7 @@ on: - samples/server/petstore/springboot-lombok-tostring/** - samples/server/petstore/springboot-file-delegate-optional/** - samples/server/petstore/springboot-petstore-with-api-response-examples/** + - samples/server/petstore/springboot-sort-validation/** - samples/openapi3/server/petstore/spring-boot-oneof-sealed/** - samples/openapi3/server/petstore/spring-boot-oneof-interface/** jobs: @@ -52,6 +54,7 @@ jobs: - samples/server/petstore/springboot-lombok-tostring - samples/server/petstore/springboot-file-delegate-optional - samples/server/petstore/springboot-petstore-with-api-response-examples + - samples/server/petstore/springboot-sort-validation - samples/openapi3/server/petstore/spring-boot-oneof-sealed - samples/openapi3/server/petstore/spring-boot-oneof-interface steps: diff --git a/bin/configs/kotlin-spring-boot-3-no-response-entity.yaml b/bin/configs/kotlin-spring-boot-3-no-response-entity.yaml index 661548dcd783..b0b7c3fe3825 100644 --- a/bin/configs/kotlin-spring-boot-3-no-response-entity.yaml +++ b/bin/configs/kotlin-spring-boot-3-no-response-entity.yaml @@ -9,6 +9,6 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" useSpringBoot3: "true" requestMappingMode: api_interface diff --git a/bin/configs/kotlin-spring-boot-3.yaml b/bin/configs/kotlin-spring-boot-3.yaml index 8a1a8e317c68..5c0572cf1cb4 100644 --- a/bin/configs/kotlin-spring-boot-3.yaml +++ b/bin/configs/kotlin-spring-boot-3.yaml @@ -9,6 +9,6 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" useSpringBoot3: "true" requestMappingMode: api_interface diff --git a/bin/configs/kotlin-spring-boot-4.yaml b/bin/configs/kotlin-spring-boot-4.yaml index 60639347c763..6779d1910974 100644 --- a/bin/configs/kotlin-spring-boot-4.yaml +++ b/bin/configs/kotlin-spring-boot-4.yaml @@ -9,7 +9,7 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" useSpringBoot4: "true" useJackson3: "true" requestMappingMode: api_interface diff --git a/bin/configs/kotlin-spring-boot-additionalproperties.yaml b/bin/configs/kotlin-spring-boot-additionalproperties.yaml index 3c3275fce2fa..e9c962afffd6 100644 --- a/bin/configs/kotlin-spring-boot-additionalproperties.yaml +++ b/bin/configs/kotlin-spring-boot-additionalproperties.yaml @@ -9,6 +9,6 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" useSpringBoot3: "true" requestMappingMode: api_interface diff --git a/bin/configs/kotlin-spring-boot-delegate-nodefaults.yaml b/bin/configs/kotlin-spring-boot-delegate-nodefaults.yaml index bf22ad329a77..3596dbcf6383 100644 --- a/bin/configs/kotlin-spring-boot-delegate-nodefaults.yaml +++ b/bin/configs/kotlin-spring-boot-delegate-nodefaults.yaml @@ -9,6 +9,6 @@ additionalProperties: useSwaggerUI: "true" delegatePattern: "true" skipDefaultInterface: "true" - beanValidations: "true" + useBeanValidation: "true" requestMappingMode: "api_interface" useSpringBoot3: "true" diff --git a/bin/configs/kotlin-spring-boot-delegate.yaml b/bin/configs/kotlin-spring-boot-delegate.yaml index 38fdee37dc7e..2eb2f2e43e89 100644 --- a/bin/configs/kotlin-spring-boot-delegate.yaml +++ b/bin/configs/kotlin-spring-boot-delegate.yaml @@ -9,5 +9,5 @@ additionalProperties: useSwaggerUI: "true" delegatePattern: "true" appendRequestToHandler: "true" - beanValidations: "true" + useBeanValidation: "true" requestMappingMode: none diff --git a/bin/configs/kotlin-spring-boot-include-http-request-context-delegate.yaml b/bin/configs/kotlin-spring-boot-include-http-request-context-delegate.yaml index e8b7eabf1c1c..44b82e9702fc 100644 --- a/bin/configs/kotlin-spring-boot-include-http-request-context-delegate.yaml +++ b/bin/configs/kotlin-spring-boot-include-http-request-context-delegate.yaml @@ -11,7 +11,7 @@ additionalProperties: skipDefaultInterface: true interfaceOnly: false serializableModel: true - beanValidations: true + useBeanValidation: true includeHttpRequestContext: true reactive: true delegatePattern: true diff --git a/bin/configs/kotlin-spring-boot-modelMutable.yaml b/bin/configs/kotlin-spring-boot-modelMutable.yaml index 6a75dc2d04a1..ba49fcc91f90 100644 --- a/bin/configs/kotlin-spring-boot-modelMutable.yaml +++ b/bin/configs/kotlin-spring-boot-modelMutable.yaml @@ -9,5 +9,5 @@ additionalProperties: useSwaggerUI: "true" serializableModel: "true" serviceImplementation: "true" - beanValidations: "true" + useBeanValidation: "true" modelMutable: "true" diff --git a/bin/configs/kotlin-spring-boot-no-response-entity-delegate.yaml b/bin/configs/kotlin-spring-boot-no-response-entity-delegate.yaml index 00c2a32823f8..d5ad5a5a4b41 100644 --- a/bin/configs/kotlin-spring-boot-no-response-entity-delegate.yaml +++ b/bin/configs/kotlin-spring-boot-no-response-entity-delegate.yaml @@ -9,7 +9,7 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" useResponseEntity: "false" delegatePattern: true requestMappingMode: controller diff --git a/bin/configs/kotlin-spring-boot-no-response-entity.yaml b/bin/configs/kotlin-spring-boot-no-response-entity.yaml index d4d1daa263d5..007fadf41864 100644 --- a/bin/configs/kotlin-spring-boot-no-response-entity.yaml +++ b/bin/configs/kotlin-spring-boot-no-response-entity.yaml @@ -9,6 +9,6 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" useResponseEntity: "false" requestMappingMode: controller diff --git a/bin/configs/kotlin-spring-boot-reactive-without-flow.yaml b/bin/configs/kotlin-spring-boot-reactive-without-flow.yaml index e2988c675d18..8f29401890a4 100644 --- a/bin/configs/kotlin-spring-boot-reactive-without-flow.yaml +++ b/bin/configs/kotlin-spring-boot-reactive-without-flow.yaml @@ -9,5 +9,5 @@ additionalProperties: useSwaggerUI: "true" serviceImplementation: "true" reactive: "true" - beanValidations: "true" + useBeanValidation: "true" useFlowForArrayReturnType: "false" diff --git a/bin/configs/kotlin-spring-boot-reactive.yaml b/bin/configs/kotlin-spring-boot-reactive.yaml index cff5b79adc79..c05cc232fcf4 100644 --- a/bin/configs/kotlin-spring-boot-reactive.yaml +++ b/bin/configs/kotlin-spring-boot-reactive.yaml @@ -9,6 +9,6 @@ additionalProperties: useSwaggerUI: "true" serviceImplementation: "true" reactive: "true" - beanValidations: "true" + useBeanValidation: "true" # the following option is set to true by default #useFlowForArrayReturnType: "true" diff --git a/bin/configs/kotlin-spring-boot-sort-validation.yaml b/bin/configs/kotlin-spring-boot-sort-validation.yaml new file mode 100644 index 000000000000..6f543e0cf4d5 --- /dev/null +++ b/bin/configs/kotlin-spring-boot-sort-validation.yaml @@ -0,0 +1,18 @@ +generatorName: kotlin-spring +outputDir: samples/server/petstore/kotlin-springboot-sort-validation +library: spring-boot +inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml +templateDir: modules/openapi-generator/src/main/resources/kotlin-spring +additionalProperties: + documentationProvider: none + annotationLibrary: none + useSwaggerUI: "false" + serviceImplementation: "false" + serializableModel: "true" + useBeanValidation: "true" + interfaceOnly: "true" + useSpringBoot3: "true" + generateSortValidation: "true" + generatePageableConstraintValidation: "true" + useTags: "true" + requestMappingMode: api_interface diff --git a/bin/configs/kotlin-spring-boot-source-swagger1.yaml b/bin/configs/kotlin-spring-boot-source-swagger1.yaml index 4c6ce15fda44..5a304d628e03 100644 --- a/bin/configs/kotlin-spring-boot-source-swagger1.yaml +++ b/bin/configs/kotlin-spring-boot-source-swagger1.yaml @@ -9,5 +9,5 @@ additionalProperties: useSwaggerUI: "true" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" implicitHeaders: "true" diff --git a/bin/configs/kotlin-spring-boot-source-swagger2.yaml b/bin/configs/kotlin-spring-boot-source-swagger2.yaml index aab768cb39c2..287d71be535c 100644 --- a/bin/configs/kotlin-spring-boot-source-swagger2.yaml +++ b/bin/configs/kotlin-spring-boot-source-swagger2.yaml @@ -9,5 +9,5 @@ additionalProperties: useSwaggerUI: "true" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" implicitHeaders: "true" diff --git a/bin/configs/kotlin-spring-boot-x-kotlin-implements.yaml b/bin/configs/kotlin-spring-boot-x-kotlin-implements.yaml index f5763ebfd70d..685c64a31c4d 100644 --- a/bin/configs/kotlin-spring-boot-x-kotlin-implements.yaml +++ b/bin/configs/kotlin-spring-boot-x-kotlin-implements.yaml @@ -11,7 +11,7 @@ additionalProperties: skipDefaultInterface: true interfaceOnly: true serializableModel: true - beanValidations: true + useBeanValidation: true includeHttpRequestContext: true schemaImplements: Pet: com.some.pack.WithId diff --git a/bin/configs/kotlin-spring-boot.yaml b/bin/configs/kotlin-spring-boot.yaml index bea19cc18f95..7fd7b00a9c2a 100644 --- a/bin/configs/kotlin-spring-boot.yaml +++ b/bin/configs/kotlin-spring-boot.yaml @@ -9,5 +9,5 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" requestMappingMode: controller diff --git a/bin/configs/kotlin-spring-cloud.yaml b/bin/configs/kotlin-spring-cloud.yaml index 676e15788e13..b4185c2a38d7 100644 --- a/bin/configs/kotlin-spring-cloud.yaml +++ b/bin/configs/kotlin-spring-cloud.yaml @@ -8,5 +8,5 @@ additionalProperties: annotationLibrary: none useSwaggerUI: "false" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" interfaceOnly: "true" diff --git a/bin/configs/kotlin-spring-declarative-interface-reactive-coroutines.yaml b/bin/configs/kotlin-spring-declarative-interface-reactive-coroutines.yaml index f5375f8f0fb7..e8df0fd8855c 100644 --- a/bin/configs/kotlin-spring-declarative-interface-reactive-coroutines.yaml +++ b/bin/configs/kotlin-spring-declarative-interface-reactive-coroutines.yaml @@ -8,7 +8,7 @@ additionalProperties: annotationLibrary: swagger2 useSwaggerUI: "false" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" interfaceOnly: true reactive: true useResponseEntity: false diff --git a/bin/configs/kotlin-spring-declarative-interface-reactive-reactor-wrapped.yaml b/bin/configs/kotlin-spring-declarative-interface-reactive-reactor-wrapped.yaml index 0826f5059e33..1f16d8ec5dbc 100644 --- a/bin/configs/kotlin-spring-declarative-interface-reactive-reactor-wrapped.yaml +++ b/bin/configs/kotlin-spring-declarative-interface-reactive-reactor-wrapped.yaml @@ -8,7 +8,7 @@ additionalProperties: annotationLibrary: swagger2 useSwaggerUI: "false" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" interfaceOnly: true reactive: true useResponseEntity: true diff --git a/bin/configs/kotlin-spring-declarative-interface-wrapped.yaml b/bin/configs/kotlin-spring-declarative-interface-wrapped.yaml index dc46abecec9d..01911f6a4b66 100644 --- a/bin/configs/kotlin-spring-declarative-interface-wrapped.yaml +++ b/bin/configs/kotlin-spring-declarative-interface-wrapped.yaml @@ -8,7 +8,7 @@ additionalProperties: annotationLibrary: swagger2 useSwaggerUI: "false" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" interfaceOnly: true reactive: false useResponseEntity: true diff --git a/bin/configs/kotlin-spring-declarative-interface.yaml b/bin/configs/kotlin-spring-declarative-interface.yaml index da5c9c0a8854..5770ddafd8c8 100644 --- a/bin/configs/kotlin-spring-declarative-interface.yaml +++ b/bin/configs/kotlin-spring-declarative-interface.yaml @@ -8,7 +8,7 @@ additionalProperties: annotationLibrary: swagger2 useSwaggerUI: "false" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" interfaceOnly: true reactive: false useResponseEntity: true diff --git a/bin/configs/kotlin-spring-default.yaml b/bin/configs/kotlin-spring-default.yaml index c251860d937a..7cfefdc631cc 100644 --- a/bin/configs/kotlin-spring-default.yaml +++ b/bin/configs/kotlin-spring-default.yaml @@ -10,5 +10,5 @@ templateDir: modules/openapi-generator/src/main/resources/kotlin-spring # useSwaggerUI: "false" # serviceImplementation: "true" # serializableModel: "true" -# beanValidations: "true" +# useBeanValidation: "true" # useSpringBoot3: "true" diff --git a/bin/configs/spring-boot-sort-validation.yaml b/bin/configs/spring-boot-sort-validation.yaml new file mode 100644 index 000000000000..d75e330e414e --- /dev/null +++ b/bin/configs/spring-boot-sort-validation.yaml @@ -0,0 +1,20 @@ +generatorName: spring +outputDir: samples/server/petstore/springboot-sort-validation +library: spring-boot +inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml +templateDir: modules/openapi-generator/src/main/resources/JavaSpring +additionalProperties: + documentationProvider: none + annotationLibrary: none + useSwaggerUI: "false" + serviceImplementation: "false" + serializableModel: "true" + useBeanValidation: "true" + interfaceOnly: "false" + skipDefaultInterface: "true" + useSpringBoot3: "true" + generateSortValidation: "true" + hideGenerationTimestamp: "true" + generatePageableConstraintValidation: "true" + useTags: "true" + requestMappingMode: api_interface diff --git a/bin/configs/unmaintained/kotlin-spring-boot-reactive.yaml b/bin/configs/unmaintained/kotlin-spring-boot-reactive.yaml index 9c21c305bd8e..ca3cf7c7b630 100644 --- a/bin/configs/unmaintained/kotlin-spring-boot-reactive.yaml +++ b/bin/configs/unmaintained/kotlin-spring-boot-reactive.yaml @@ -9,4 +9,4 @@ additionalProperties: useSwaggerUI: "true" serviceImplementation: "true" reactive: "true" - beanValidations: "true" + useBeanValidation: "true" diff --git a/bin/configs/unmaintained/kotlin-spring-boot.yaml b/bin/configs/unmaintained/kotlin-spring-boot.yaml index a1a137b865bf..2bfa6c8c5bb3 100644 --- a/bin/configs/unmaintained/kotlin-spring-boot.yaml +++ b/bin/configs/unmaintained/kotlin-spring-boot.yaml @@ -9,4 +9,4 @@ additionalProperties: useSwaggerUI: "false" serviceImplementation: "true" serializableModel: "true" - beanValidations: "true" + useBeanValidation: "true" diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 31d185771c9f..f97418750e9f 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -31,6 +31,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |artifactUrl|artifact URL in generated pom.xml| |https://github.com/openapitools/openapi-generator| |artifactVersion|artifact version in generated pom.xml. This also becomes part of the generated library's filename. If not provided, uses the version from the OpenAPI specification file. If that's also not present, uses the default value of the artifactVersion option.| |1.0.0| |async|use async Callable controllers| |false| +|autoXSpringPaginated|Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected. Only applies when library=spring-boot.| |false| |basePackage|base package (invokerPackage) for generated code| |org.openapitools| |bigDecimalAsString|Treat BigDecimal values as Strings to avoid precision loss.| |false| |booleanGetterPrefix|Set booleanGetterPrefix| |get| @@ -62,6 +63,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl |generateBuilders|Whether to generate builders for models| |false| |generateConstructorWithAllArgs|whether to generate a constructor for all arguments| |false| |generateGenericResponseEntity|Use a generic type for the `ResponseEntity` wrapping return values of generated API methods. If enabled, method are generated with return type ResponseEntity<?>| |false| +|generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.| |false| +|generateSortValidation|Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.| |false| |generatedConstructorWithRequiredArgs|Whether to generate constructors with required args for models| |true| |groupId|groupId in generated pom.xml| |org.openapitools| |hateoas|Use Spring HATEOAS library to allow adding HATEOAS links| |false| diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index 580d834f52c0..90828667f3a5 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -34,6 +34,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl |documentationProvider|Select the OpenAPI documentation provider.|
**none**
Do not publish an OpenAPI specification.
**source**
Publish the original input OpenAPI specification.
**springdoc**
Generate an OpenAPI 3 specification using SpringDoc.
|springdoc| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original| |exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true| +|generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.| |false| +|generateSortValidation|Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.| |false| |gradleBuildFile|generate a gradle build file using the Kotlin DSL| |true| |groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools| |implicitHeaders|Skip header parameters in the generated API methods.| |false| diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 2384667faf34..6b09baded2ab 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -31,6 +31,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |artifactUrl|artifact URL in generated pom.xml| |https://github.com/openapitools/openapi-generator| |artifactVersion|artifact version in generated pom.xml. This also becomes part of the generated library's filename. If not provided, uses the version from the OpenAPI specification file. If that's also not present, uses the default value of the artifactVersion option.| |1.0.0| |async|use async Callable controllers| |false| +|autoXSpringPaginated|Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected. Only applies when library=spring-boot.| |false| |basePackage|base package (invokerPackage) for generated code| |org.openapitools| |bigDecimalAsString|Treat BigDecimal values as Strings to avoid precision loss.| |false| |booleanGetterPrefix|Set booleanGetterPrefix| |get| @@ -55,6 +56,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl |generateBuilders|Whether to generate builders for models| |false| |generateConstructorWithAllArgs|whether to generate a constructor for all arguments| |false| |generateGenericResponseEntity|Use a generic type for the `ResponseEntity` wrapping return values of generated API methods. If enabled, method are generated with return type ResponseEntity<?>| |false| +|generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.| |false| +|generateSortValidation|Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.| |false| |generatedConstructorWithRequiredArgs|Whether to generate constructors with required args for models| |true| |groupId|groupId in generated pom.xml| |org.openapitools| |hateoas|Use Spring HATEOAS library to allow adding HATEOAS links| |false| diff --git a/docs/usage.md b/docs/usage.md index fbd26a69014c..62ca03b84b6b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -555,7 +555,7 @@ openapi-generator-cli generate \ -i petstore.yaml \ -g kotlin-spring \ -o out \ - --additional-properties=library=spring-boot,beanValidations=true,serviceImplementation=true \ + --additional-properties=library=spring-boot,useBeanValidation=true,serviceImplementation=true \ --import-mappings=DateTime=java.time.LocalDateTime \ --type-mappings=DateTime=java.time.LocalDateTime ``` diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 5dc081bbd3ab..e894cc25000d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -22,6 +22,9 @@ import com.samskivert.mustache.Template; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; import lombok.Getter; import lombok.Setter; import org.openapitools.codegen.*; @@ -98,6 +101,8 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen public static final String USE_REQUEST_MAPPING_ON_CONTROLLER = "useRequestMappingOnController"; public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface"; public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated"; + public static final String GENERATE_SORT_VALIDATION = "generateSortValidation"; + public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation"; public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces"; public static final String COMPANION_OBJECT = "companionObject"; @@ -164,6 +169,8 @@ public String getDescription() { @Setter private DeclarativeInterfaceReactiveMode declarativeInterfaceReactiveMode = DeclarativeInterfaceReactiveMode.coroutines; @Setter private boolean useResponseEntity = true; @Setter private boolean autoXSpringPaginated = false; + @Setter private boolean generateSortValidation = false; + @Setter private boolean generatePageableConstraintValidation = false; @Setter private boolean useSealedResponseInterfaces = false; @Setter private boolean companionObject = false; @@ -180,6 +187,15 @@ public String getDescription() { private Map sealedInterfaceToOperationId = new HashMap<>(); private boolean sealedInterfacesFileWritten = false; + // Map from operationId to allowed sort values for @ValidSort annotation generation + private Map> sortValidationEnums = new HashMap<>(); + + // Map from operationId to pageable defaults for @PageableDefault/@SortDefault annotation generation + private Map pageableDefaultsRegistry = new HashMap<>(); + + // Map from operationId to pageable constraints for @ValidPageable annotation generation + private Map pageableConstraintsRegistry = new HashMap<>(); + public KotlinSpringServerCodegen() { super(); @@ -272,6 +288,8 @@ public KotlinSpringServerCodegen() { addOption(SCHEMA_IMPLEMENTS, "A map of single interface or a list of interfaces per schema name that should be implemented (serves similar purpose as `x-kotlin-implements`, but is fully decoupled from the api spec). Example: yaml `schemaImplements: {Pet: com.some.pack.WithId, Category: [com.some.pack.CategoryInterface], Dog: [com.some.pack.Canine, com.some.pack.OtherInterface]}` implements interfaces in schemas `Pet` (interface `com.some.pack.WithId`), `Category` (interface `com.some.pack.CategoryInterface`), `Dog`(interfaces `com.some.pack.Canine`, `com.some.pack.OtherInterface`)", "empty map"); addOption(SCHEMA_IMPLEMENTS_FIELDS, "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)", "empty map"); addSwitch(AUTO_X_SPRING_PAGINATED, "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected.", autoXSpringPaginated); + addSwitch(GENERATE_SORT_VALIDATION, "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.", generateSortValidation); + addSwitch(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.", generatePageableConstraintValidation); addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject); supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application."); supportedLibraries.put(SPRING_CLOUD_LIBRARY, @@ -704,6 +722,14 @@ public void processOpts() { this.setAutoXSpringPaginated(convertPropertyToBoolean(AUTO_X_SPRING_PAGINATED)); } writePropertyBack(AUTO_X_SPRING_PAGINATED, autoXSpringPaginated); + if (additionalProperties.containsKey(GENERATE_SORT_VALIDATION) && library.equals(SPRING_BOOT)) { + this.setGenerateSortValidation(convertPropertyToBoolean(GENERATE_SORT_VALIDATION)); + } + writePropertyBack(GENERATE_SORT_VALIDATION, generateSortValidation); + if (additionalProperties.containsKey(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION) && library.equals(SPRING_BOOT)) { + this.setGeneratePageableConstraintValidation(convertPropertyToBoolean(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION)); + } + writePropertyBack(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, generatePageableConstraintValidation); if (isUseSpringBoot3() && isUseSpringBoot4()) { throw new IllegalArgumentException("Choose between Spring Boot 3 and Spring Boot 4"); } @@ -1042,6 +1068,52 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation } // #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used + // Build pageable parameter annotations (@ValidPageable, @ValidSort, @PageableDefault, @SortDefault.SortDefaults) + List pageableAnnotations = new ArrayList<>(); + + if (generatePageableConstraintValidation && useBeanValidation && pageableConstraintsRegistry.containsKey(codegenOperation.operationId)) { + SpringPageableScanUtils.PageableConstraintsData constraints = pageableConstraintsRegistry.get(codegenOperation.operationId); + List attrs = new ArrayList<>(); + if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize); + if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage); + pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")"); + codegenOperation.imports.add("ValidPageable"); + } + + if (generateSortValidation && useBeanValidation && sortValidationEnums.containsKey(codegenOperation.operationId)) { + List allowedSortValues = sortValidationEnums.get(codegenOperation.operationId); + String allowedValuesStr = allowedSortValues.stream() + .map(v -> "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\"") + .collect(Collectors.joining(", ")); + pageableAnnotations.add("@ValidSort(allowedValues = [" + allowedValuesStr + "])"); + codegenOperation.imports.add("ValidSort"); + } + + // Generate @PageableDefault / @SortDefault.SortDefaults annotations if defaults are present + if (pageableDefaultsRegistry.containsKey(codegenOperation.operationId)) { + SpringPageableScanUtils.PageableDefaultsData defaults = pageableDefaultsRegistry.get(codegenOperation.operationId); + + if (defaults.page != null || defaults.size != null) { + List attrs = new ArrayList<>(); + if (defaults.page != null) attrs.add("page = " + defaults.page); + if (defaults.size != null) attrs.add("size = " + defaults.size); + pageableAnnotations.add("@PageableDefault(" + String.join(", ", attrs) + ")"); + codegenOperation.imports.add("PageableDefault"); + } + + if (!defaults.sortDefaults.isEmpty()) { + List sortEntries = defaults.sortDefaults.stream() + .map(sf -> "SortDefault(sort = [\"" + sf.field + "\"], direction = Sort.Direction." + sf.direction + ")") + .collect(Collectors.toList()); + pageableAnnotations.add("@SortDefault.SortDefaults(" + String.join(", ", sortEntries) + ")"); + codegenOperation.imports.add("SortDefault"); + codegenOperation.imports.add("Sort"); + } + } + + if (!pageableAnnotations.isEmpty()) { + codegenOperation.vendorExtensions.put("x-pageable-extra-annotation", pageableAnnotations); + } codegenOperation.queryParams.removeIf(param -> defaultPageableQueryParams.contains(param.baseName)); codegenOperation.allParams.removeIf(param -> param.isQueryParam && defaultPageableQueryParams.contains(param.baseName)); } @@ -1058,6 +1130,33 @@ public void preprocessOpenAPI(OpenAPI openAPI) { (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.kt")); } + if (SPRING_BOOT.equals(library) && generateSortValidation && useBeanValidation) { + sortValidationEnums = SpringPageableScanUtils.scanSortValidationEnums(openAPI, autoXSpringPaginated); + if (!sortValidationEnums.isEmpty()) { + importMapping.putIfAbsent("ValidSort", configPackage + ".ValidSort"); + supportingFiles.add(new SupportingFile("validSort.mustache", + (sourceFolder + File.separator + configPackage).replace(".", File.separator), "ValidSort.kt")); + } + } + + if (SPRING_BOOT.equals(library)) { + pageableDefaultsRegistry = SpringPageableScanUtils.scanPageableDefaults(openAPI, autoXSpringPaginated); + if (!pageableDefaultsRegistry.isEmpty()) { + importMapping.putIfAbsent("PageableDefault", "org.springframework.data.web.PageableDefault"); + importMapping.putIfAbsent("SortDefault", "org.springframework.data.web.SortDefault"); + importMapping.putIfAbsent("Sort", "org.springframework.data.domain.Sort"); + } + } + + if (SPRING_BOOT.equals(library) && generatePageableConstraintValidation && useBeanValidation) { + pageableConstraintsRegistry = SpringPageableScanUtils.scanPageableConstraints(openAPI, autoXSpringPaginated); + if (!pageableConstraintsRegistry.isEmpty()) { + importMapping.putIfAbsent("ValidPageable", configPackage + ".ValidPageable"); + supportingFiles.add(new SupportingFile("validPageable.mustache", + (sourceFolder + File.separator + configPackage).replace(".", File.separator), "ValidPageable.kt")); + } + } + if (!additionalProperties.containsKey(TITLE)) { // The purpose of the title is for: // - README documentation @@ -1123,6 +1222,14 @@ public void preprocessOpenAPI(OpenAPI openAPI) { // TODO: Handle tags } + /** + * Returns true if the given operation will have a Pageable parameter injected. + * Delegates to {@link SpringPageableScanUtils#willBePageable}. + */ + private boolean willBePageable(Operation operation) { + return SpringPageableScanUtils.willBePageable(operation, autoXSpringPaginated); + } + @Override public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index e6a150c4e21a..8a340845468e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -111,6 +111,9 @@ public class SpringCodegen extends AbstractJavaCodegen public static final String JACKSON3_PACKAGE = "tools.jackson"; public static final String JACKSON_PACKAGE = "jacksonPackage"; public static final String ADDITIONAL_NOT_NULL_ANNOTATIONS = "additionalNotNullAnnotations"; + public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated"; + public static final String GENERATE_SORT_VALIDATION = "generateSortValidation"; + public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation"; @Getter public enum RequestMappingMode { @@ -186,6 +189,16 @@ public enum RequestMappingMode { @Getter @Setter protected boolean additionalNotNullAnnotations = false; @Setter boolean useHttpServiceProxyFactoryInterfacesConfigurator = false; + @Setter protected boolean autoXSpringPaginated = false; + @Setter protected boolean generateSortValidation = false; + @Setter protected boolean generatePageableConstraintValidation = false; + + // Map from operationId to allowed sort values for @ValidSort annotation generation + private Map> sortValidationEnums = new HashMap<>(); + // Map from operationId to pageable defaults for @PageableDefault/@SortDefault annotation generation + private Map pageableDefaultsRegistry = new HashMap<>(); + // Map from operationId to pageable constraints for @ValidPageable annotation generation + private Map pageableConstraintsRegistry = new HashMap<>(); public SpringCodegen() { super(); @@ -338,6 +351,24 @@ public SpringCodegen() { cliOptions.add(CliOption.newBoolean(ADDITIONAL_NOT_NULL_ANNOTATIONS, "Add @NotNull to path variables (required by default) and requestBody.", additionalNotNullAnnotations)); + cliOptions.add(CliOption.newBoolean(AUTO_X_SPRING_PAGINATED, + "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. " + + "When enabled, operations with all three parameters will have Pageable support automatically applied. " + + "Operations with x-spring-paginated explicitly set to false will not be auto-detected. " + + "Only applies when library=spring-boot.", + autoXSpringPaginated)); + cliOptions.add(CliOption.newBoolean(GENERATE_SORT_VALIDATION, + "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to " + + "the injected Pageable parameter of operations whose 'sort' parameter has enum values. " + + "The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. " + + "Requires useBeanValidation=true and library=spring-boot.", + generateSortValidation)); + cliOptions.add(CliOption.newBoolean(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, + "Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to " + + "the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. " + + "The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. " + + "Requires useBeanValidation=true and library=spring-boot.", + generatePageableConstraintValidation)); } @@ -547,6 +578,12 @@ public void processOpts() { convertPropertyToBooleanAndWriteBack(ADDITIONAL_NOT_NULL_ANNOTATIONS, this::setAdditionalNotNullAnnotations); + if (SPRING_BOOT.equals(library)) { + convertPropertyToBooleanAndWriteBack(AUTO_X_SPRING_PAGINATED, this::setAutoXSpringPaginated); + convertPropertyToBooleanAndWriteBack(GENERATE_SORT_VALIDATION, this::setGenerateSortValidation); + convertPropertyToBooleanAndWriteBack(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, this::setGeneratePageableConstraintValidation); + } + // override parent one importMapping.put("JsonDeserialize", (useJackson3 ? JACKSON3_PACKAGE : JACKSON2_PACKAGE) + ".databind.annotation.JsonDeserialize"); @@ -792,6 +829,33 @@ public void preprocessOpenAPI(OpenAPI openAPI) { (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.java")); } + if (SPRING_BOOT.equals(library) && generateSortValidation && useBeanValidation) { + sortValidationEnums = SpringPageableScanUtils.scanSortValidationEnums(openAPI, autoXSpringPaginated); + if (!sortValidationEnums.isEmpty()) { + importMapping.putIfAbsent("ValidSort", configPackage + ".ValidSort"); + supportingFiles.add(new SupportingFile("validSort.mustache", + (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "ValidSort.java")); + } + } + + if (SPRING_BOOT.equals(library)) { + pageableDefaultsRegistry = SpringPageableScanUtils.scanPageableDefaults(openAPI, autoXSpringPaginated); + if (!pageableDefaultsRegistry.isEmpty()) { + importMapping.putIfAbsent("PageableDefault", "org.springframework.data.web.PageableDefault"); + importMapping.putIfAbsent("SortDefault", "org.springframework.data.web.SortDefault"); + importMapping.putIfAbsent("Sort", "org.springframework.data.domain.Sort"); + } + } + + if (SPRING_BOOT.equals(library) && generatePageableConstraintValidation && useBeanValidation) { + pageableConstraintsRegistry = SpringPageableScanUtils.scanPageableConstraints(openAPI, autoXSpringPaginated); + if (!pageableConstraintsRegistry.isEmpty()) { + importMapping.putIfAbsent("ValidPageable", configPackage + ".ValidPageable"); + supportingFiles.add(new SupportingFile("validPageable.mustache", + (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "ValidPageable.java")); + } + } + /* * TODO the following logic should not need anymore in OAS 3.0 if * ("/".equals(swagger.getBasePath())) { swagger.setBasePath(""); } @@ -1114,6 +1178,24 @@ protected boolean isConstructorWithAllArgsAllowed(CodegenModel codegenModel) { @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { + // Auto-detect pagination parameters and add x-spring-paginated if autoXSpringPaginated is enabled. + // Only for spring-boot; respect manual x-spring-paginated: false override. + if (SPRING_BOOT.equals(library) && autoXSpringPaginated) { + if (operation.getExtensions() == null || !Boolean.FALSE.equals(operation.getExtensions().get("x-spring-paginated"))) { + if (operation.getParameters() != null) { + Set paramNames = operation.getParameters().stream() + .map(io.swagger.v3.oas.models.parameters.Parameter::getName) + .collect(Collectors.toSet()); + if (paramNames.containsAll(Arrays.asList("page", "size", "sort"))) { + if (operation.getExtensions() == null) { + operation.setExtensions(new HashMap<>()); + } + operation.getExtensions().put("x-spring-paginated", Boolean.TRUE); + } + } + } + } + // add Pageable import only if x-spring-paginated explicitly used // this allows to use a custom Pageable schema without importing Spring Pageable. if (Boolean.TRUE.equals(operation.getExtensions().get("x-spring-paginated"))) { @@ -1142,6 +1224,52 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation // #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used codegenOperation.queryParams.removeIf(param -> defaultPageableQueryParams.contains(param.baseName)); codegenOperation.allParams.removeIf(param -> param.isQueryParam && defaultPageableQueryParams.contains(param.baseName)); + + // Build pageable parameter annotations (@ValidPageable, @ValidSort, @PageableDefault, @SortDefault.SortDefaults) + List pageableAnnotations = new ArrayList<>(); + + if (generatePageableConstraintValidation && useBeanValidation && pageableConstraintsRegistry.containsKey(codegenOperation.operationId)) { + SpringPageableScanUtils.PageableConstraintsData constraints = pageableConstraintsRegistry.get(codegenOperation.operationId); + List attrs = new ArrayList<>(); + if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize); + if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage); + pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")"); + codegenOperation.imports.add("ValidPageable"); + } + + if (generateSortValidation && useBeanValidation && sortValidationEnums.containsKey(codegenOperation.operationId)) { + List allowedSortValues = sortValidationEnums.get(codegenOperation.operationId); + // Java annotation arrays use {} syntax + String allowedValuesStr = allowedSortValues.stream() + .map(v -> "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\"") + .collect(Collectors.joining(", ")); + pageableAnnotations.add("@ValidSort(allowedValues = {" + allowedValuesStr + "})"); + codegenOperation.imports.add("ValidSort"); + } + + if (pageableDefaultsRegistry.containsKey(codegenOperation.operationId)) { + SpringPageableScanUtils.PageableDefaultsData defaults = pageableDefaultsRegistry.get(codegenOperation.operationId); + if (defaults.page != null || defaults.size != null) { + List attrs = new ArrayList<>(); + if (defaults.page != null) attrs.add("page = " + defaults.page); + if (defaults.size != null) attrs.add("size = " + defaults.size); + pageableAnnotations.add("@PageableDefault(" + String.join(", ", attrs) + ")"); + codegenOperation.imports.add("PageableDefault"); + } + if (!defaults.sortDefaults.isEmpty()) { + // Java annotation arrays use @SortDefault(...) with {} for the sort field array + List sortEntries = defaults.sortDefaults.stream() + .map(sf -> "@SortDefault(sort = {\"" + sf.field + "\"}, direction = Sort.Direction." + sf.direction + ")") + .collect(Collectors.toList()); + pageableAnnotations.add("@SortDefault.SortDefaults({" + String.join(", ", sortEntries) + "})"); + codegenOperation.imports.add("SortDefault"); + codegenOperation.imports.add("Sort"); + } + } + + if (!pageableAnnotations.isEmpty()) { + codegenOperation.vendorExtensions.put("x-pageable-extra-annotation", pageableAnnotations); + } } if (codegenOperation.vendorExtensions.containsKey("x-spring-provide-args") && !provideArgsClassSet.isEmpty()) { codegenOperation.imports.addAll(provideArgsClassSet); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringPageableScanUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringPageableScanUtils.java new file mode 100644 index 000000000000..17ceb3757fdb --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringPageableScanUtils.java @@ -0,0 +1,313 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.languages; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import org.openapitools.codegen.utils.ModelUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Language-agnostic utility methods for scanning OpenAPI specs for Spring Pageable-related + * features: sort enum validation, pageable defaults, and pageable constraints (max page/size). + * + *

Used by both kotlin {@link KotlinSpringServerCodegen} and java {@link SpringCodegen} to share + * scan logic. Only the mustache templates and their registration remain language-specific.

+ */ +public final class SpringPageableScanUtils { + + private SpringPageableScanUtils() {} + + // ------------------------------------------------------------------------- + // Data classes + // ------------------------------------------------------------------------- + + /** Carries a parsed sort field and its direction (always "ASC" or "DESC") from the spec default. */ + public static final class SortFieldDefault { + public final String field; + public final String direction; + + public SortFieldDefault(String field, String direction) { + this.field = field; + this.direction = direction; + } + } + + /** Carries parsed default values for page, size, and sort fields from a pageable operation. */ + public static final class PageableDefaultsData { + public final Integer page; + public final Integer size; + public final List sortDefaults; + + public PageableDefaultsData(Integer page, Integer size, List sortDefaults) { + this.page = page; + this.size = size; + this.sortDefaults = sortDefaults; + } + + public boolean hasAny() { + return page != null || size != null || !sortDefaults.isEmpty(); + } + } + + /** + * Carries max constraints for page number and page size from a pageable operation. + * {@code -1} means no constraint specified (no {@code maximum:} in the spec). + */ + public static final class PageableConstraintsData { + /** Maximum allowed page number, or {@code -1} if unconstrained. */ + public final int maxPage; + /** Maximum allowed page size, or {@code -1} if unconstrained. */ + public final int maxSize; + + public PageableConstraintsData(int maxPage, int maxSize) { + this.maxPage = maxPage; + this.maxSize = maxSize; + } + + public boolean hasAny() { + return maxPage >= 0 || maxSize >= 0; + } + } + + // ------------------------------------------------------------------------- + // Scan methods + // ------------------------------------------------------------------------- + + /** + * Returns {@code true} if the given operation will have a Pageable parameter injected — + * either because it has {@code x-spring-paginated: true} explicitly, or because + * {@code autoXSpringPaginated} is enabled and the operation has all three default + * pagination query parameters (page, size, sort). + */ + public static boolean willBePageable(Operation operation, boolean autoXSpringPaginated) { + if (operation.getExtensions() != null) { + Object paginated = operation.getExtensions().get("x-spring-paginated"); + if (Boolean.FALSE.equals(paginated)) { + return false; + } + if (Boolean.TRUE.equals(paginated)) { + return true; + } + } + if (autoXSpringPaginated && operation.getParameters() != null) { + Set paramNames = operation.getParameters().stream() + .map(Parameter::getName) + .collect(Collectors.toSet()); + return paramNames.containsAll(Arrays.asList("page", "size", "sort")); + } + return false; + } + + /** + * Scans all pageable operations for a {@code sort} parameter with enum values. + * + * @return map from operationId to list of allowed sort strings (e.g. {@code ["id,asc", "id,desc"]}) + */ + public static Map> scanSortValidationEnums( + OpenAPI openAPI, boolean autoXSpringPaginated) { + Map> result = new LinkedHashMap<>(); + if (openAPI.getPaths() == null) { + return result; + } + for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) { + for (Operation operation : pathEntry.getValue().readOperations()) { + String operationId = operation.getOperationId(); + if (operationId == null || !willBePageable(operation, autoXSpringPaginated)) { + continue; + } + if (operation.getParameters() == null) { + continue; + } + for (Parameter param : operation.getParameters()) { + if (!"sort".equals(param.getName())) { + continue; + } + Schema schema = param.getSchema(); + if (schema == null) { + continue; + } + if (schema.get$ref() != null) { + schema = ModelUtils.getReferencedSchema(openAPI, schema); + } + if (schema == null) { + continue; + } + // If the top-level schema is an array, the enum lives on its items + Schema enumSchema = schema; + if (schema.getItems() != null) { + enumSchema = schema.getItems(); + if (enumSchema.get$ref() != null) { + enumSchema = ModelUtils.getReferencedSchema(openAPI, enumSchema); + } + } + if (enumSchema == null || enumSchema.getEnum() == null || enumSchema.getEnum().isEmpty()) { + continue; + } + List enumValues = enumSchema.getEnum().stream() + .map(Object::toString) + .collect(Collectors.toList()); + result.put(operationId, enumValues); + } + } + } + return result; + } + + /** + * Scans all pageable operations for default values on {@code page}, {@code size}, + * and {@code sort} parameters. + * + * @return map from operationId to {@link PageableDefaultsData} (only operations with at + * least one default are included) + */ + public static Map scanPageableDefaults( + OpenAPI openAPI, boolean autoXSpringPaginated) { + Map result = new LinkedHashMap<>(); + if (openAPI.getPaths() == null) { + return result; + } + for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) { + for (Operation operation : pathEntry.getValue().readOperations()) { + String operationId = operation.getOperationId(); + if (operationId == null || !willBePageable(operation, autoXSpringPaginated)) { + continue; + } + if (operation.getParameters() == null) { + continue; + } + Integer pageDefault = null; + Integer sizeDefault = null; + List sortDefaults = new ArrayList<>(); + + for (Parameter param : operation.getParameters()) { + Schema schema = param.getSchema(); + if (schema == null) { + continue; + } + if (schema.get$ref() != null) { + schema = ModelUtils.getReferencedSchema(openAPI, schema); + } + if (schema == null || schema.getDefault() == null) { + continue; + } + Object defaultValue = schema.getDefault(); + switch (param.getName()) { + case "page": + if (defaultValue instanceof Number) { + pageDefault = ((Number) defaultValue).intValue(); + } + break; + case "size": + if (defaultValue instanceof Number) { + sizeDefault = ((Number) defaultValue).intValue(); + } + break; + case "sort": + List sortValues = new ArrayList<>(); + if (defaultValue instanceof String) { + sortValues.add((String) defaultValue); + } else if (defaultValue instanceof ArrayNode) { + ((ArrayNode) defaultValue).forEach(node -> sortValues.add(node.asText())); + } else if (defaultValue instanceof List) { + for (Object item : (List) defaultValue) { + sortValues.add(item.toString()); + } + } + for (String sortStr : sortValues) { + String[] parts = sortStr.split(",", 2); + String field = parts[0].trim(); + String direction = parts.length > 1 ? parts[1].trim().toUpperCase(Locale.ROOT) : "ASC"; + sortDefaults.add(new SortFieldDefault(field, direction)); + } + break; + default: + break; + } + } + + PageableDefaultsData data = new PageableDefaultsData(pageDefault, sizeDefault, sortDefaults); + if (data.hasAny()) { + result.put(operationId, data); + } + } + } + return result; + } + + /** + * Scans all pageable operations for {@code maximum:} constraints on {@code page} and + * {@code size} parameters. + * + * @return map from operationId to {@link PageableConstraintsData} (only operations with + * at least one {@code maximum:} constraint are included) + */ + public static Map scanPageableConstraints( + OpenAPI openAPI, boolean autoXSpringPaginated) { + Map result = new LinkedHashMap<>(); + if (openAPI.getPaths() == null) { + return result; + } + for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) { + for (Operation operation : pathEntry.getValue().readOperations()) { + String operationId = operation.getOperationId(); + if (operationId == null || !willBePageable(operation, autoXSpringPaginated)) { + continue; + } + if (operation.getParameters() == null) { + continue; + } + int maxPage = -1; + int maxSize = -1; + for (Parameter param : operation.getParameters()) { + Schema schema = param.getSchema(); + if (schema == null) { + continue; + } + if (schema.get$ref() != null) { + schema = ModelUtils.getReferencedSchema(openAPI, schema); + } + if (schema == null || schema.getMaximum() == null) { + continue; + } + int maximum = schema.getMaximum().intValue(); + switch (param.getName()) { + case "page": + maxPage = maximum; + break; + case "size": + maxSize = maximum; + break; + default: + break; + } + } + PageableConstraintsData data = new PageableConstraintsData(maxPage, maxSize); + if (data.hasAny()) { + result.put(operationId, data); + } + } + } + return result; + } +} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache index 2f05bbace9ca..5ac4a949ca1d 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache @@ -279,7 +279,7 @@ public interface {{classname}} { {{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{^-last}}, {{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}} final {{#reactive}}ServerWebExchange exchange{{/reactive}}{{^reactive}}HttpServletRequest servletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, - {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},{{/includeHttpRequestContext}}{{/hasParams}}{{#springDocDocumentationProvider}}@ParameterObject{{/springDocDocumentationProvider}} final Pageable pageable{{/vendorExtensions.x-spring-paginated}}{{#vendorExtensions.x-spring-provide-args}}{{#hasParams}}, + {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},{{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#springDocDocumentationProvider}}@ParameterObject{{/springDocDocumentationProvider}} final Pageable pageable{{/vendorExtensions.x-spring-paginated}}{{#vendorExtensions.x-spring-provide-args}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},{{/includeHttpRequestContext}}{{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}} {{{.}}}{{^hasParams}}{{^-last}}{{^reactive}},{{/reactive}} {{/-last}}{{/hasParams}}{{/vendorExtensions.x-spring-provide-args}} ){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} { diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/validPageable.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/validPageable.mustache new file mode 100644 index 000000000000..daf547481640 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/validPageable.mustache @@ -0,0 +1,99 @@ +package {{configPackage}}; + +import {{javaxPackage}}.validation.Constraint; +import {{javaxPackage}}.validation.ConstraintValidator; +import {{javaxPackage}}.validation.ConstraintValidatorContext; +import {{javaxPackage}}.validation.Payload; +import org.springframework.data.domain.Pageable; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Validates that the page number and page size in the annotated {@link Pageable} parameter do not + * exceed their configured maximums. + * + *

Apply directly on a {@code Pageable} parameter. Each attribute is independently optional: + *

    + *
  • {@link #maxSize()} — when set (>= 0), validates {@code pageable.getPageSize() <= maxSize} + *
  • {@link #maxPage()} — when set (>= 0), validates {@code pageable.getPageNumber() <= maxPage} + *
+ * + *

Use {@link #NO_LIMIT} (= {@code -1}, the default) to leave an attribute unconstrained. + * + *

Constraining {@link #maxPage()} is useful to prevent deep-pagination attacks, where a large + * page offset (e.g. {@code ?page=100000&size=20}) causes an expensive {@code OFFSET} query on the + * database. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {ValidPageable.PageableConstraintValidator.class}) +@Target({ElementType.PARAMETER}) +public @interface ValidPageable { + + /** Sentinel value meaning no limit is applied. */ + int NO_LIMIT = -1; + + /** Maximum allowed page size, or {@link #NO_LIMIT} if unconstrained. */ + int maxSize() default NO_LIMIT; + + /** Maximum allowed page number (0-based), or {@link #NO_LIMIT} if unconstrained. */ + int maxPage() default NO_LIMIT; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String message() default "Invalid page request"; + + class PageableConstraintValidator implements ConstraintValidator { + + private int maxSize = NO_LIMIT; + private int maxPage = NO_LIMIT; + + @Override + public void initialize(ValidPageable constraintAnnotation) { + maxSize = constraintAnnotation.maxSize(); + maxPage = constraintAnnotation.maxPage(); + } + + @Override + public boolean isValid(Pageable pageable, ConstraintValidatorContext context) { + if (pageable == null) { + return true; + } + + if (!pageable.isPaged()) { + return true; + } + + boolean valid = true; + context.disableDefaultConstraintViolation(); + + if (maxSize >= 0 && pageable.getPageSize() > maxSize) { + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate() + + ": page size " + pageable.getPageSize() + + " exceeds maximum " + maxSize) + .addPropertyNode("size") + .addConstraintViolation(); + valid = false; + } + + if (maxPage >= 0 && pageable.getPageNumber() > maxPage) { + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate() + + ": page number " + pageable.getPageNumber() + + " exceeds maximum " + maxPage) + .addPropertyNode("page") + .addConstraintViolation(); + valid = false; + } + + return valid; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/validSort.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/validSort.mustache new file mode 100644 index 000000000000..6b47813dd477 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/validSort.mustache @@ -0,0 +1,102 @@ +package {{configPackage}}; + +import {{javaxPackage}}.validation.Constraint; +import {{javaxPackage}}.validation.ConstraintValidator; +import {{javaxPackage}}.validation.ConstraintValidatorContext; +import {{javaxPackage}}.validation.Payload; +import org.springframework.data.domain.Pageable; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Validates that sort properties in the annotated {@link Pageable} parameter match the allowed values. + * + *

Apply directly on a {@code Pageable} parameter. The validator checks that each sort + * property and direction combination in the {@link Pageable} matches one of the strings specified + * in {@link #allowedValues()}. + * + *

Two formats are accepted in {@link #allowedValues()}: + *

    + *
  • {@code "property,direction"} — permits only the specific direction (e.g. {@code "id,asc"}, + * {@code "name,desc"}). Direction matching is case-insensitive. + *
  • {@code "property"} — permits any direction for that property (e.g. {@code "id"} matches + * {@code sort=id,asc} and {@code sort=id,desc}). Note: because Spring always normalises a + * bare {@code sort=id} to ascending before the validator runs, bare property names in + * {@link #allowedValues()} effectively allow all directions. + *
+ * + *

Both formats may be mixed freely. For example {@code {"id", "name,desc"}} allows {@code id} + * in any direction but restricts {@code name} to descending only. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {ValidSort.SortValidator.class}) +@Target({ElementType.PARAMETER}) +public @interface ValidSort { + + /** The allowed sort strings (e.g. {@code {"id,asc", "id,desc"}}). */ + String[] allowedValues(); + + Class[] groups() default {}; + + Class[] payload() default {}; + + String message() default "Invalid sort column"; + + class SortValidator implements ConstraintValidator { + + private Set allowedValues; + + @Override + public void initialize(ValidSort constraintAnnotation) { + allowedValues = Arrays.stream(constraintAnnotation.allowedValues()) + .map(entry -> entry + .replaceAll("(?i),ASC$", ",asc") + .replaceAll("(?i),DESC$", ",desc")) + .collect(Collectors.toSet()); + } + + @Override + public boolean isValid(Pageable pageable, ConstraintValidatorContext context) { + if (pageable == null || pageable.getSort().isUnsorted()) { + return true; + } + + Map invalid = new TreeMap<>(); + int[] index = {0}; + pageable.getSort().forEach(order -> { + String sortValue = order.getProperty() + "," + order.getDirection().name().toLowerCase(java.util.Locale.ROOT); + // Accept "property,direction" (exact match) OR "property" alone (any direction allowed) + if (!allowedValues.contains(sortValue) && !allowedValues.contains(order.getProperty())) { + invalid.put(index[0], order.getProperty()); + } + index[0]++; + }); + + if (!invalid.isEmpty()) { + context.disableDefaultConstraintViolation(); + invalid.forEach((i, property) -> + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate() + " [" + property + "]") + .addPropertyNode("sort") + .addPropertyNode("property") + .inIterable() + .atIndex(i) + .addConstraintViolation()); + } + + return invalid.isEmpty(); + } + } +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache index 253cd1719766..033d044fb28e 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache @@ -110,7 +110,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v {{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, - {{/includeHttpRequestContext}}{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} + {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}} { return {{>returnValue}} } diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache index 04232e570533..795070dde368 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache @@ -125,7 +125,7 @@ interface {{classname}} { {{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, - {{/includeHttpRequestContext}}{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} + {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{#useResponseEntity}}>{{/useResponseEntity}}{{^skipDefaultApiInterface}} { {{^isDelegate}} return {{>returnValue}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache index c1fb61e35c18..6264845e18b8 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb3-Kts.mustache @@ -50,6 +50,7 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") {{#useBeanValidation}} + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache index c9c5996d0e09..893231362d49 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache @@ -58,6 +58,7 @@ dependencies { {{/useJackson3}} implementation("org.springframework.data:spring-data-commons") {{#useBeanValidation}} + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} implementation("jakarta.annotation:jakarta.annotation-api:3.0.0") diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache index fe0ff44f0d34..ec356c78edea 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradleKts.mustache @@ -57,6 +57,7 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") {{#useBeanValidation}} + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api"){{/useBeanValidation}} implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache index ea1ac4523871..0019fc4b0b37 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb3.mustache @@ -192,6 +192,10 @@ jakarta.validation jakarta.validation-api + + + org.springframework.boot + spring-boot-starter-validation {{/useBeanValidation}} jakarta.annotation diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache index 9d249a18caac..18c2c5b22c5c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache @@ -179,6 +179,10 @@ jakarta.validation jakarta.validation-api + + + org.springframework.boot + spring-boot-starter-validation {{/useBeanValidation}} jakarta.annotation diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache index 06d98458f75d..8765049c3494 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom.mustache @@ -179,6 +179,10 @@ javax.validation validation-api + + + org.springframework.boot + spring-boot-starter-validation {{/useBeanValidation}} javax.annotation diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache new file mode 100644 index 000000000000..6b26b7a26803 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache @@ -0,0 +1,81 @@ +package {{configPackage}} + +import {{javaxPackage}}.validation.Constraint +import {{javaxPackage}}.validation.ConstraintValidator +import {{javaxPackage}}.validation.ConstraintValidatorContext +import {{javaxPackage}}.validation.Payload +import org.springframework.data.domain.Pageable + +/** + * Validates that the page number and page size in the annotated [Pageable] parameter do not + * exceed their configured maximums. + * + * Apply directly on a `pageable: Pageable` parameter. Each attribute is independently optional: + * - [maxSize] — when set (>= 0), validates `pageable.pageSize <= maxSize` + * - [maxPage] — when set (>= 0), validates `pageable.pageNumber <= maxPage` + * + * Use [NO_LIMIT] (= -1, the default) to leave an attribute unconstrained. + * + * Constraining [maxPage] is useful to prevent deep-pagination attacks, where a large page + * offset (e.g. `?page=100000&size=20`) causes an expensive `OFFSET` query on the database. + * + * @property maxSize Maximum allowed page size, or [NO_LIMIT] if unconstrained + * @property maxPage Maximum allowed page number (0-based), or [NO_LIMIT] if unconstrained + * @property groups Validation groups (optional) + * @property payload Additional payload (optional) + * @property message Validation error message (default: "Invalid page request") + */ +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [PageableConstraintValidator::class]) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class ValidPageable( + val maxSize: Int = ValidPageable.NO_LIMIT, + val maxPage: Int = ValidPageable.NO_LIMIT, + val groups: Array> = [], + val payload: Array> = [], + val message: String = "Invalid page request" +) { + companion object { + const val NO_LIMIT = -1 + } +} + +class PageableConstraintValidator : ConstraintValidator { + + private var maxSize = ValidPageable.NO_LIMIT + private var maxPage = ValidPageable.NO_LIMIT + + override fun initialize(constraintAnnotation: ValidPageable) { + maxSize = constraintAnnotation.maxSize + maxPage = constraintAnnotation.maxPage + } + + override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean { + if (pageable == null) return true + if (!pageable.isPaged) return true + + var valid = true + context.disableDefaultConstraintViolation() + + if (maxSize >= 0 && pageable.pageSize > maxSize) { + context.buildConstraintViolationWithTemplate( + "${context.defaultConstraintMessageTemplate}: page size ${pageable.pageSize} exceeds maximum $maxSize" + ) + .addPropertyNode("size") + .addConstraintViolation() + valid = false + } + + if (maxPage >= 0 && pageable.pageNumber > maxPage) { + context.buildConstraintViolationWithTemplate( + "${context.defaultConstraintMessageTemplate}: page number ${pageable.pageNumber} exceeds maximum $maxPage" + ) + .addPropertyNode("page") + .addConstraintViolation() + valid = false + } + + return valid + } +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/validSort.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/validSort.mustache new file mode 100644 index 000000000000..7b3f7cf0adbd --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/validSort.mustache @@ -0,0 +1,87 @@ +package {{configPackage}} + +import {{javaxPackage}}.validation.Constraint +import {{javaxPackage}}.validation.ConstraintValidator +import {{javaxPackage}}.validation.ConstraintValidatorContext +import {{javaxPackage}}.validation.Payload +import org.springframework.data.domain.Pageable + +/** + * Validates that sort properties in the annotated [Pageable] parameter match the allowed values. + * + * Apply directly on a `pageable: Pageable` parameter. The validator checks that each sort + * property and direction combination in the [Pageable] matches one of the strings specified + * in [allowedValues]. + * + * Two formats are accepted in [allowedValues]: + * - `"property,direction"` — permits only the specific direction (e.g. `"id,asc"`, `"name,desc"`). + * Direction matching is case-insensitive: `"id,ASC"` and `"id,asc"` are treated identically. + * - `"property"` — permits any direction for that property (e.g. `"id"` matches `sort=id,asc` + * and `sort=id,desc`). Note: because Spring always normalises a bare `sort=id` to ascending + * before the validator runs, bare property names in [allowedValues] effectively allow all + * directions — the original omission of a direction cannot be detected. + * + * Both formats may be mixed freely. For example `["id", "name,desc"]` allows `id` in any + * direction but restricts `name` to descending only. + * + * @property allowedValues The allowed sort strings (e.g. `["id,asc", "id,desc"]`) + * @property groups Validation groups (optional) + * @property payload Additional payload (optional) + * @property message Validation error message (default: "Invalid sort column") + */ +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [SortValidator::class]) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class ValidSort( + val allowedValues: Array, + val groups: Array> = [], + val payload: Array> = [], + val message: String = "Invalid sort column" +) + +class SortValidator : ConstraintValidator { + + private lateinit var allowedValues: Set + + override fun initialize(constraintAnnotation: ValidSort) { + allowedValues = constraintAnnotation.allowedValues.map { entry -> + DIRECTION_ASC_SUFFIX.replace(entry, ",asc") + .let { DIRECTION_DESC_SUFFIX.replace(it, ",desc") } + }.toSet() + } + + private companion object { + val DIRECTION_ASC_SUFFIX = Regex(",ASC$", RegexOption.IGNORE_CASE) + val DIRECTION_DESC_SUFFIX = Regex(",DESC$", RegexOption.IGNORE_CASE) + } + + override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean { + if (pageable == null || pageable.sort.isUnsorted) return true + + val invalid = pageable.sort + .foldIndexed(emptyMap()) { index, acc, order -> + val sortValue = "${order.property},${order.direction.name.lowercase()}" + // Accept "property,direction" (exact match) OR "property" alone (any direction allowed) + if (sortValue !in allowedValues && order.property !in allowedValues) acc + (index to order.property) + else acc + } + .toSortedMap() + + if (invalid.isNotEmpty()) { + context.disableDefaultConstraintViolation() + invalid.forEach { (index, property) -> + context.buildConstraintViolationWithTemplate( + "${context.defaultConstraintMessageTemplate} [$property]" + ) + .addPropertyNode("sort") + .addPropertyNode("property") + .inIterable() + .atIndex(index) + .addConstraintViolation() + } + } + + return invalid.isEmpty() + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 96444d9aee77..524dde54bb2e 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -6664,4 +6664,474 @@ public void testJspecify(String library, int springBootVersion, String fooApiFil JavaFileAssert.assertThat(files.get("model/package-info.java")) .fileContains("@org.jspecify.annotations.NullMarked"); } + + // ------------------------------------------------------------------------- + // autoXSpringPaginated tests + // ------------------------------------------------------------------------- + + @Test + public void autoXSpringPaginatedDetectsAllThreeParams() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsWithAutoDetect has page+size+sort → Pageable should be injected + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithAutoDetect") + .assertParameter("pageable").hasType("Pageable"); + } + + @Test + public void autoXSpringPaginatedManualFalseTakesPrecedence() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsManualFalse has x-spring-paginated: false → Pageable must NOT be injected + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsManualFalse") + .doesNotHaveParameter("pageable"); + } + + @Test + public void autoXSpringPaginatedCaseSensitiveMatching() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsCaseSensitive uses Page/Size/Sort (capital) → must NOT auto-detect + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsCaseSensitive") + .doesNotHaveParameter("pageable"); + } + + @Test + public void autoXSpringPaginatedNoDetectionWhenMissingPage() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsMissingPage: missing 'page' param → Pageable must NOT be injected + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsMissingPage") + .doesNotHaveParameter("pageable"); + } + + @Test + public void autoXSpringPaginatedNoDetectionWhenMissingSize() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsMissingSize: missing 'size' param → Pageable must NOT be injected + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsMissingSize") + .doesNotHaveParameter("pageable"); + } + + @Test + public void autoXSpringPaginatedNoDetectionWhenMissingSort() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsMissingSort: missing 'sort' param → Pageable must NOT be injected + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsMissingSort") + .doesNotHaveParameter("pageable"); + } + + @Test + public void autoXSpringPaginatedOnlyForSpringBoot() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + // spring-cloud generates a Feign client — auto-detect should not apply there + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", "spring-cloud", props); + + File petApiClient = files.get("PetApiClient.java"); + if (petApiClient != null) { + String content = java.nio.file.Files.readString(petApiClient.toPath()); + assertThat(content).doesNotContain("Pageable pageable"); + } + } + + @Test + public void autoXSpringPaginatedDisabledByDefault() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + // NOT setting AUTO_X_SPRING_PAGINATED (defaults to false) + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsWithAutoDetect: should NOT get Pageable when autoXSpringPaginated is not enabled + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithAutoDetect") + .doesNotHaveParameter("pageable"); + } + + @Test + public void autoXSpringPaginatedWorksWithManualTrue() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsManualTrue: explicit x-spring-paginated: true → Pageable must be injected + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsManualTrue") + .assertParameter("pageable").hasType("Pageable"); + } + + @Test + public void autoXSpringPaginatedNoParamsDoesNotDetect() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-auto-paginated.yaml", SPRING_BOOT, props); + + // findPetsNoParams: no params at all → Pageable must NOT be injected + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsNoParams") + .doesNotHaveParameter("pageable"); + } + + // ------------------------------------------------------------------------- + // generateSortValidation tests + // ------------------------------------------------------------------------- + + @Test + public void generateSortValidationAddsAnnotationAndGeneratesFile() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // ValidSort.java must be generated + assertThat(files).containsKey("ValidSort.java"); + + // findPetsWithSortEnum has explicit x-spring-paginated + sort enum → @ValidSort applied with all 4 values + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("@ValidSort(allowedValues = {") + .fileContains("\"id,asc\"") + .fileContains("\"id,desc\"") + .fileContains("\"name,asc\"") + .fileContains("\"name,desc\""); + } + + @Test + public void generateSortValidationUsesJavaArraySyntax() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // The generated API file must use Java {} array syntax (not Kotlin []) + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("@ValidSort(allowedValues = {"); + } + + @Test + public void generateSortValidationWithAutoDetect() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.AUTO_X_SPRING_PAGINATED, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsAutoDetectedWithSort: auto-detected + sort enum → ValidSort applied with Java {} syntax + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("@ValidSort(allowedValues = {") + .fileContains("\"id,asc\"") + .fileContains("\"id,desc\""); + } + + @Test + public void generateSortValidationNotAppliedWhenNoSortEnum() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithoutSortEnum: paginated but sort has no enum → no @ValidSort + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithoutSortEnum") + .assertParameter("pageable") + .assertParameterAnnotations() + .doesNotContainWithName("ValidSort"); + } + + @Test + public void generateSortValidationWorksForArraySortEnum() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithArraySortEnum: sort is type:array, items have inline enum → @ValidSort with Java {} syntax + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithArraySortEnum") + .assertParameter("pageable") + .assertParameterAnnotations() + .containsWithName("ValidSort"); + + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("@ValidSort(allowedValues = {") + .fileContains("\"id,asc\"") + .fileContains("\"id,desc\"") + .fileContains("\"name,asc\"") + .fileContains("\"name,desc\""); + } + + @Test + public void generateSortValidationWorksForArraySortRefEnum() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithArraySortRefEnum: sort is type:array, items $ref to PetSort enum → @ValidSort with PetSort values + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithArraySortRefEnum") + .assertParameter("pageable") + .assertParameterAnnotations() + .containsWithName("ValidSort"); + + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("\"id,asc\"") + .fileContains("\"id,desc\"") + .fileContains("\"createdAt,asc\"") + .fileContains("\"createdAt,desc\""); + } + + @Test + public void generateSortValidationWorksForExternalParamRefArraySort() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithExternalParamRefArraySort: sort param $ref to external components file, + // type:array with items $ref to PetSortEnum in the same external file + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithExternalParamRefArraySort") + .assertParameter("pageable") + .assertParameterAnnotations() + .containsWithName("ValidSort"); + + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("\"name,asc\"") + .fileContains("\"name,desc\"") + .fileContains("\"id,asc\"") + .fileContains("\"id,desc\""); + } + + @Test + public void generateSortValidationWorksForNonExplodedExternalParamRefArraySort() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithNonExplodedExternalParamRefArraySort: sort param $ref to external file, + // explode: false — @ValidSort works identically since it validates the deserialized Pageable + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithNonExplodedExternalParamRefArraySort") + .assertParameter("pageable") + .assertParameterAnnotations() + .containsWithName("ValidSort"); + + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("\"name,asc\"") + .fileContains("\"name,desc\"") + .fileContains("\"id,asc\"") + .fileContains("\"id,desc\""); + } + + // ------------------------------------------------------------------------- + // generatePageableConstraintValidation tests + // ------------------------------------------------------------------------- + + @Test + public void generatePageableConstraintValidationAddsAnnotationAndGeneratesFile() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // ValidPageable.java must be generated + assertThat(files).containsKey("ValidPageable.java"); + + // findPetsWithSizeConstraint: size maximum=100 → @ValidPageable(maxSize = 100) + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithSizeConstraint") + .assertParameter("pageable") + .assertParameterAnnotations() + .containsWithNameAndAttributes("ValidPageable", Map.of("maxSize", "100")); + } + + @Test + public void generatePageableConstraintValidationWithBothConstraints() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + props.put(SpringCodegen.GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithPageAndSizeConstraint: page maximum=999, size maximum=50 + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithPageAndSizeConstraint") + .assertParameter("pageable") + .assertParameterAnnotations() + .containsWithNameAndAttributes("ValidPageable", Map.of("maxSize", "50", "maxPage", "999")); + } + + // ------------------------------------------------------------------------- + // @PageableDefault / @SortDefault tests + // ------------------------------------------------------------------------- + + @Test + public void pageableDefaultAnnotationApplied() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithPageSizeDefaultsOnly: page=0, size=25 → @PageableDefault(page = 0, size = 25) + JavaFileAssert.assertThat(files.get("PetApi.java")) + .assertMethod("findPetsWithPageSizeDefaultsOnly") + .assertParameter("pageable") + .assertParameterAnnotations() + .containsWithNameAndAttributes("PageableDefault", Map.of("page", "0", "size", "25")); + } + + @Test + public void sortDefaultAnnotationApplied() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithSortDefaultOnly: sort default "name,desc" → @SortDefault.SortDefaults generated + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("@SortDefault.SortDefaults({@SortDefault(sort = {\"name\"}, direction = Sort.Direction.DESC)})"); + } + + @Test + public void sortDefaultAndPageableDefaultBothApplied() throws IOException { + Map props = new HashMap<>(); + props.put(SpringCodegen.INTERFACE_ONLY, "true"); + props.put(SpringCodegen.SKIP_DEFAULT_INTERFACE, "true"); + props.put(SpringCodegen.USE_TAGS, "true"); + props.put(SpringCodegen.USE_SPRING_BOOT3, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/spring/petstore-sort-validation.yaml", SPRING_BOOT, props); + + // findPetsWithAllDefaults: page=0, size=10, sort=["name,desc","id,asc"] + // → @PageableDefault + @SortDefault.SortDefaults both present + JavaFileAssert.assertThat(files.get("PetApi.java")) + .fileContains("@PageableDefault(page = 0, size = 10)") + .fileContains("@SortDefault.SortDefaults({@SortDefault(sort = {\"name\"}, direction = Sort.Direction.DESC), @SortDefault(sort = {\"id\"}, direction = Sort.Direction.ASC)})"); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index 504583c759dd..d66ab3797e66 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -3919,7 +3919,7 @@ public void springPaginatedNoParamsNoContext() throws Exception { // Test operation listAllPets which has no parameters except pageable File petApi = files.get("PetApi.kt"); - assertFileContains(petApi.toPath(), "fun listAllPets(@Parameter(hidden = true) pageable: Pageable)"); + assertFileContains(petApi.toPath(), "fun listAllPets(@PageableDefault(page = 0, size = 20) @Parameter(hidden = true) pageable: Pageable)"); } @Test @@ -4160,8 +4160,459 @@ private Map generateFromContract( .collect(Collectors.toMap(File::getName, Function.identity())); } + // ========== GENERATE PAGEABLE CONSTRAINT VALIDATION TESTS ========== + + @Test + public void generatePageableConstraintValidationAddsSizeConstraint() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsWithSizeConstraint has maximum: 100 on size only + int methodStart = content.indexOf("fun findPetsWithSizeConstraint("); + Assert.assertTrue(methodStart >= 0, "findPetsWithSizeConstraint method should exist"); + String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500)); + Assert.assertTrue(paramBlock.contains("@ValidPageable(maxSize = 100)"), + "@ValidPageable(maxSize = 100) should appear on the pageable parameter"); + Assert.assertFalse(paramBlock.contains("maxPage"), + "maxPage should not appear when only size has a maximum constraint"); + + assertFileContains(petApi.toPath(), "import org.openapitools.configuration.ValidPageable"); + } + + @Test + public void generatePageableConstraintValidationAddsPageAndSizeConstraint() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsWithPageAndSizeConstraint has maximum: 999 on page and maximum: 50 on size + int methodStart = content.indexOf("fun findPetsWithPageAndSizeConstraint("); + Assert.assertTrue(methodStart >= 0, "findPetsWithPageAndSizeConstraint method should exist"); + String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500)); + Assert.assertTrue(paramBlock.contains("@ValidPageable(maxSize = 50, maxPage = 999)"), + "@ValidPageable(maxSize = 50, maxPage = 999) should appear on the pageable parameter"); + } + + @Test + public void generatePageableConstraintValidationGeneratesValidPageableFile() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File validPageableFile = files.get("ValidPageable.kt"); + Assert.assertNotNull(validPageableFile, "ValidPageable.kt should be generated when generatePageableConstraintValidation=true"); + assertFileContains(validPageableFile.toPath(), "annotation class ValidPageable"); + assertFileContains(validPageableFile.toPath(), "class PageableConstraintValidator"); + assertFileContains(validPageableFile.toPath(), "val maxSize: Int"); + assertFileContains(validPageableFile.toPath(), "val maxPage: Int"); + assertFileContains(validPageableFile.toPath(), "NO_LIMIT"); + } + + @Test + public void generatePageableConstraintValidationDoesNotGenerateFileWhenDisabled() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + // NOT setting GENERATE_PAGEABLE_CONSTRAINT_VALIDATION (defaults to false) + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + Assert.assertNull(files.get("ValidPageable.kt"), "ValidPageable.kt should NOT be generated when generatePageableConstraintValidation=false"); + File petApi = files.get("PetApi.kt"); + assertFileNotContains(petApi.toPath(), "@ValidPageable"); + } + + @Test + public void generatePageableConstraintValidationDoesNotGenerateFileWhenBeanValidationDisabled() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_PAGEABLE_CONSTRAINT_VALIDATION, "true"); + additionalProperties.put(USE_BEANVALIDATION, "false"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + Assert.assertNull(files.get("ValidPageable.kt"), "ValidPageable.kt should NOT be generated when useBeanValidation=false"); + File petApi = files.get("PetApi.kt"); + assertFileNotContains(petApi.toPath(), "@ValidPageable"); + } + // ========== AUTO X-SPRING-PAGINATED TESTS ========== + // ========== GENERATE SORT VALIDATION TESTS ========== + + @Test + public void generateSortValidationAddsAnnotationForExplicitPaginated() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + assertFileContains(petApi.toPath(), "@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"name,asc\", \"name,desc\"])"); + assertFileContains(petApi.toPath(), "import org.openapitools.configuration.ValidSort"); + + // @ValidSort must be a parameter annotation — appears in the 500-char window AFTER `fun findPetsWithSortEnum(` + String content = Files.readString(petApi.toPath()); + int methodStart = content.indexOf("fun findPetsWithSortEnum("); + Assert.assertTrue(methodStart >= 0, "findPetsWithSortEnum method should exist"); + String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500)); + Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"name,asc\", \"name,desc\"])"), + "@ValidSort should appear as a parameter annotation (inside the method signature, after `fun`)"); + Assert.assertTrue(paramBlock.contains("pageable: Pageable"), + "findPetsWithSortEnum should have a pageable: Pageable parameter"); + + // @ValidSort must NOT be a method-level annotation (not in the 500-char prefix before `fun`) + String prefixBlock = content.substring(Math.max(0, methodStart - 500), methodStart); + Assert.assertFalse(prefixBlock.contains("@ValidSort"), + "@ValidSort should be a parameter annotation, not a method-level annotation"); + } + + @Test + public void generateSortValidationAddsAnnotationForAutoDetectedPaginated() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + additionalProperties.put(AUTO_X_SPRING_PAGINATED, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + assertFileContains(petApi.toPath(), "@ValidSort(allowedValues = [\"id,asc\", \"id,desc\"])"); + } + + @Test + public void generateSortValidationHandlesRefSortEnum() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + assertFileContains(petApi.toPath(), "@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"createdAt,asc\", \"createdAt,desc\"])"); + } + + @Test + public void generateSortValidationDoesNotAnnotateNonPaginatedOperation() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsNonPaginatedWithSortEnum has sort enum but NO pagination — must not get @ValidSort + int methodStart = content.indexOf("fun findPetsNonPaginatedWithSortEnum("); + Assert.assertTrue(methodStart >= 0, "findPetsNonPaginatedWithSortEnum method should exist"); + String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart); + Assert.assertFalse(methodBlock.contains("@ValidSort"), + "Non-paginated operation should not have @ValidSort even if sort param has enum values"); + } + + @Test + public void generateSortValidationDoesNotAnnotateWhenSortHasNoEnum() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsWithoutSortEnum has pagination but sort has NO enum values + int methodStart = content.indexOf("fun findPetsWithoutSortEnum("); + Assert.assertTrue(methodStart >= 0, "findPetsWithoutSortEnum method should exist"); + String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart); + Assert.assertFalse(methodBlock.contains("@ValidSort"), + "Paginated operation with non-enum sort should not have @ValidSort"); + } + + @Test + public void generateSortValidationGeneratesValidSortFile() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File validSortFile = files.get("ValidSort.kt"); + Assert.assertNotNull(validSortFile, "ValidSort.kt should be generated when generateSortValidation=true"); + assertFileContains(validSortFile.toPath(), "annotation class ValidSort"); + assertFileContains(validSortFile.toPath(), "class SortValidator"); + assertFileContains(validSortFile.toPath(), "val allowedValues: Array"); + assertFileContains(validSortFile.toPath(), "DIRECTION_ASC_SUFFIX"); + assertFileContains(validSortFile.toPath(), "DIRECTION_DESC_SUFFIX"); + } + + @Test + public void generateSortValidationDoesNotGenerateValidSortFileWhenDisabled() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + // NOT setting GENERATE_SORT_VALIDATION (defaults to false) + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + Assert.assertNull(files.get("ValidSort.kt"), "ValidSort.kt should NOT be generated when generateSortValidation=false"); + File petApi = files.get("PetApi.kt"); + assertFileNotContains(petApi.toPath(), "@ValidSort"); + } + + @Test + public void generateSortValidationDoesNotGenerateValidSortFileWhenBeanValidationDisabled() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + additionalProperties.put(USE_BEANVALIDATION, "false"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + Assert.assertNull(files.get("ValidSort.kt"), "ValidSort.kt should NOT be generated when useBeanValidation=false"); + File petApi = files.get("PetApi.kt"); + assertFileNotContains(petApi.toPath(), "@ValidSort"); + } + + @Test + public void generateSortValidationWorksForArraySortEnum() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsWithArraySortEnum: sort is type:array, items have inline enum → @ValidSort applied with Kotlin [] syntax + int methodStart = content.indexOf("fun findPetsWithArraySortEnum("); + Assert.assertTrue(methodStart >= 0, "findPetsWithArraySortEnum method should exist"); + String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500)); + Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"name,asc\", \"name,desc\"])"), + "@ValidSort with all four enum values should appear on the pageable parameter"); + Assert.assertTrue(paramBlock.contains("pageable: Pageable"), + "findPetsWithArraySortEnum should have a pageable: Pageable parameter"); + } + + @Test + public void generateSortValidationWorksForArraySortRefEnum() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsWithArraySortRefEnum: sort is type:array, items $ref to PetSort enum → @ValidSort with PetSort values + int methodStart = content.indexOf("fun findPetsWithArraySortRefEnum("); + Assert.assertTrue(methodStart >= 0, "findPetsWithArraySortRefEnum method should exist"); + String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500)); + Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"id,asc\", \"id,desc\", \"createdAt,asc\", \"createdAt,desc\"])"), + "@ValidSort with PetSort enum values should appear on the pageable parameter"); + } + + @Test + public void generateSortValidationWorksForExternalParamRefArraySort() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsWithExternalParamRefArraySort: sort param $ref to external components file, + // which defines type:array with items $ref to PetSortEnum in the same external file + int methodStart = content.indexOf("fun findPetsWithExternalParamRefArraySort("); + Assert.assertTrue(methodStart >= 0, "findPetsWithExternalParamRefArraySort method should exist"); + String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500)); + Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = ["), + "@ValidSort should appear when sort param is resolved from an external $ref parameter"); + Assert.assertTrue(paramBlock.contains("\"name,asc\"") || paramBlock.contains("\"id,asc\""), + "@ValidSort should contain the enum values from the external PetSortEnum schema"); + Assert.assertTrue(paramBlock.contains("pageable: Pageable"), + "findPetsWithExternalParamRefArraySort should have a pageable: Pageable parameter"); + } + + @Test + public void generateSortValidationWorksForNonExplodedExternalParamRefArraySort() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + additionalProperties.put(GENERATE_SORT_VALIDATION, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsWithNonExplodedExternalParamRefArraySort: sort param $ref to external file, + // explode: false — Spring parses ?sort=id,asc,name,desc as sequential token pairs. + // @ValidSort validation works the same way since it operates on the deserialized Pageable. + int methodStart = content.indexOf("fun findPetsWithNonExplodedExternalParamRefArraySort("); + Assert.assertTrue(methodStart >= 0, "findPetsWithNonExplodedExternalParamRefArraySort method should exist"); + String paramBlock = content.substring(methodStart, Math.min(content.length(), methodStart + 500)); + Assert.assertTrue(paramBlock.contains("@ValidSort(allowedValues = [\"name,asc\", \"name,desc\", \"id,asc\", \"id,desc\"])"), + "@ValidSort with PetSortEnum values should appear even for non-exploded array sort param"); + Assert.assertTrue(paramBlock.contains("pageable: Pageable"), + "findPetsWithNonExplodedExternalParamRefArraySort should have a pageable: Pageable parameter"); + } + + // ========== PAGEABLE DEFAULTS TESTS ========== + + @Test + public void pageableDefaultsGeneratesSortDefaultsForSingleDescField() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + assertFileContains(petApi.toPath(), + "@SortDefault.SortDefaults(SortDefault(sort = [\"name\"], direction = Sort.Direction.DESC))"); + assertFileContains(petApi.toPath(), "import org.springframework.data.domain.Sort"); + assertFileContains(petApi.toPath(), "import org.springframework.data.web.SortDefault"); + } + + @Test + public void pageableDefaultsGeneratesSortDefaultsForSingleAscField() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + assertFileContains(petApi.toPath(), + "@SortDefault.SortDefaults(SortDefault(sort = [\"id\"], direction = Sort.Direction.ASC))"); + } + + @Test + public void pageableDefaultsGeneratesSortDefaultsForMixedDirections() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + assertFileContains(petApi.toPath(), + "@SortDefault.SortDefaults(SortDefault(sort = [\"name\"], direction = Sort.Direction.DESC), SortDefault(sort = [\"id\"], direction = Sort.Direction.ASC))"); + } + + @Test + public void pageableDefaultsGeneratesPageableDefaultForPageAndSize() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + assertFileContains(petApi.toPath(), "@PageableDefault(page = 0, size = 25)"); + assertFileContains(petApi.toPath(), "import org.springframework.data.web.PageableDefault"); + } + + @Test + public void pageableDefaultsGeneratesBothAnnotationsWhenAllDefaultsPresent() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + int methodStart = content.indexOf("fun findPetsWithAllDefaults("); + Assert.assertTrue(methodStart >= 0, "findPetsWithAllDefaults method should exist"); + String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart + 500); + + Assert.assertTrue(methodBlock.contains("@PageableDefault(page = 0, size = 10)"), + "findPetsWithAllDefaults should have @PageableDefault(page = 0, size = 10)"); + Assert.assertTrue(methodBlock.contains( + "@SortDefault.SortDefaults(SortDefault(sort = [\"name\"], direction = Sort.Direction.DESC), SortDefault(sort = [\"id\"], direction = Sort.Direction.ASC))"), + "findPetsWithAllDefaults should have @SortDefault.SortDefaults with both fields"); + } + + @Test + public void pageableDefaultsDoesNotAnnotateNonPageableOperation() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(USE_TAGS, "true"); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(SKIP_DEFAULT_INTERFACE, "true"); + + Map files = generateFromContract("src/test/resources/3_0/spring/petstore-sort-validation.yaml", additionalProperties); + + File petApi = files.get("PetApi.kt"); + String content = Files.readString(petApi.toPath()); + + // findPetsNonPaginatedWithSortEnum has no x-spring-paginated, so no pageable annotations + int methodStart = content.indexOf("fun findPetsNonPaginatedWithSortEnum("); + Assert.assertTrue(methodStart >= 0, "findPetsNonPaginatedWithSortEnum method should exist"); + String methodBlock = content.substring(Math.max(0, methodStart - 500), methodStart); + Assert.assertFalse(methodBlock.contains("@SortDefault"), + "Non-paginated operation should not have @SortDefault"); + Assert.assertFalse(methodBlock.contains("@PageableDefault"), + "Non-paginated operation should not have @PageableDefault"); + } + @Test public void autoXSpringPaginatedDetectsAllThreeParams() throws Exception { Map additionalProperties = new HashMap<>(); diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation-components.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation-components.yaml new file mode 100644 index 000000000000..8f82109468eb --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation-components.yaml @@ -0,0 +1,31 @@ +components: + parameters: + PetSortParam: + name: sort + in: query + required: false + description: Sort order — multi-column, each value must be one of the allowed enum values + schema: + type: array + default: [] + items: + $ref: '#/components/schemas/PetSortEnum' + PetSortParamNonExploded: + name: sort + in: query + required: false + style: form + explode: false + description: Sort order — non-exploded multi-column (e.g. sort=id,asc,name,desc), each token pair must match the allowed enum values + schema: + type: array + items: + $ref: '#/components/schemas/PetSortEnum' + schemas: + PetSortEnum: + type: string + enum: + - "name,asc" + - "name,desc" + - "id,asc" + - "id,desc" diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml new file mode 100644 index 000000000000..86d398d2c407 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-sort-validation.yaml @@ -0,0 +1,564 @@ +openapi: 3.0.1 +info: + title: OpenAPI Petstore - Sort Validation Test + description: Test spec for generateSortValidation feature + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +tags: + - name: pet + description: Everything about your Pets +paths: + /pet/findByStatusWithSort: + get: + tags: + - pet + summary: Find pets with explicit x-spring-paginated and inline sort enum + operationId: findPetsWithSortEnum + x-spring-paginated: true + parameters: + - name: status + in: query + description: Status filter + schema: + type: string + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + description: Sort order + schema: + type: string + enum: + - "id,asc" + - "id,desc" + - "name,asc" + - "name,desc" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findAutoDetectedWithSort: + get: + tags: + - pet + summary: Find pets with auto-detected pagination and sort enum + operationId: findPetsAutoDetectedWithSort + parameters: + - name: status + in: query + description: Status filter + schema: + type: string + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + description: Sort order + schema: + type: string + enum: + - "id,asc" + - "id,desc" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithRefSort: + get: + tags: + - pet + summary: Find pets with x-spring-paginated and $ref sort enum + operationId: findPetsWithRefSort + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + description: Sort order + schema: + $ref: '#/components/schemas/PetSort' + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithoutSortEnum: + get: + tags: + - pet + summary: Find pets with pagination but sort has no enum constraint + operationId: findPetsWithoutSortEnum + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + description: Sort order (no enum constraint) + schema: + type: string + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findNonPaginatedWithSortEnum: + get: + tags: + - pet + summary: Find pets without pagination but sort param has enum — no sort validation expected + operationId: findPetsNonPaginatedWithSortEnum + parameters: + - name: sort + in: query + description: Sort order with enum but no pagination + schema: + type: string + enum: + - "id,asc" + - "id,desc" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + + # ---- Pageable defaults test cases ---- + + /pet/findWithSortDefaultOnly: + get: + tags: + - pet + summary: Find pets — sort default only (single field DESC, no page/size defaults) + operationId: findPetsWithSortDefaultOnly + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + - name: size + in: query + schema: + type: integer + - name: sort + in: query + schema: + type: string + default: "name,desc" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithSortDefaultAsc: + get: + tags: + - pet + summary: Find pets — sort default only (single field, no explicit direction defaults to ASC) + operationId: findPetsWithSortDefaultAsc + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + - name: size + in: query + schema: + type: integer + - name: sort + in: query + schema: + type: string + default: "id" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithMixedSortDefaults: + get: + tags: + - pet + summary: Find pets — multiple sort defaults with mixed directions (array sort param) + operationId: findPetsWithMixedSortDefaults + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + - name: size + in: query + schema: + type: integer + - name: sort + in: query + style: form + explode: true + schema: + type: array + items: + type: string + default: + - "name,desc" + - "id,asc" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithPageSizeDefaultsOnly: + get: + tags: + - pet + summary: Find pets — page and size defaults only, no sort default + operationId: findPetsWithPageSizeDefaultsOnly + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 25 + - name: sort + in: query + schema: + type: string + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithAllDefaults: + get: + tags: + - pet + summary: Find pets — page, size, and mixed sort defaults all present + operationId: findPetsWithAllDefaults + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 10 + - name: sort + in: query + style: form + explode: true + schema: + type: array + items: + type: string + default: + - "name,desc" + - "id,asc" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithSizeConstraint: + get: + tags: + - pet + summary: Find pets — size has maximum constraint only + operationId: findPetsWithSizeConstraint + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + - name: size + in: query + schema: + type: integer + maximum: 100 + - name: sort + in: query + schema: + type: string + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithPageAndSizeConstraint: + get: + tags: + - pet + summary: Find pets — both page and size have maximum constraints + operationId: findPetsWithPageAndSizeConstraint + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + maximum: 999 + - name: size + in: query + schema: + type: integer + maximum: 50 + - name: sort + in: query + schema: + type: string + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithArraySortEnum: + get: + tags: + - pet + summary: Find pets with x-spring-paginated and array sort param with inline enum on items + operationId: findPetsWithArraySortEnum + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + description: Sort order (multi-column, each element must be an allowed value) + style: form + explode: true + schema: + type: array + items: + type: string + enum: + - "id,asc" + - "id,desc" + - "name,asc" + - "name,desc" + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithArraySortRefEnum: + get: + tags: + - pet + summary: Find pets with x-spring-paginated and array sort param whose items use a $ref enum + operationId: findPetsWithArraySortRefEnum + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + description: Sort order (multi-column, items $ref to PetSort enum) + style: form + explode: true + schema: + type: array + items: + $ref: '#/components/schemas/PetSort' + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithExternalParamRefArraySort: + get: + tags: + - pet + summary: Find pets with x-spring-paginated and sort param referenced from an external components file + operationId: findPetsWithExternalParamRefArraySort + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - $ref: './petstore-sort-validation-components.yaml#/components/parameters/PetSortParam' + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/findWithNonExplodedExternalParamRefArraySort: + get: + tags: + - pet + summary: Find pets with x-spring-paginated and non-exploded sort param referenced from an external components file + operationId: findPetsWithNonExplodedExternalParamRefArraySort + x-spring-paginated: true + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - $ref: './petstore-sort-validation-components.yaml#/components/parameters/PetSortParamNonExploded' + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' +components: + schemas: + PetSort: + type: string + enum: + - "id,asc" + - "id,desc" + - "createdAt,asc" + - "createdAt,desc" + Pet: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + status: + type: string + description: pet status in the store diff --git a/samples/server/petstore/kotlin-spring-default/build.gradle.kts b/samples/server/petstore/kotlin-spring-default/build.gradle.kts index 319a704f5842..3b564eb0a224 100644 --- a/samples/server/petstore/kotlin-spring-default/build.gradle.kts +++ b/samples/server/petstore/kotlin-spring-default/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-spring-default/pom.xml b/samples/server/petstore/kotlin-spring-default/pom.xml index 76ef64f955da..3266249b3379 100644 --- a/samples/server/petstore/kotlin-spring-default/pom.xml +++ b/samples/server/petstore/kotlin-spring-default/pom.xml @@ -125,6 +125,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts b/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts index 559b1f327bec..ff5dc81c6a46 100644 --- a/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts +++ b/samples/server/petstore/kotlin-spring-sealed-interfaces/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml b/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml index 67af2523dcb8..6e01abd57bbb 100644 --- a/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml +++ b/samples/server/petstore/kotlin-spring-sealed-interfaces/pom.xml @@ -132,6 +132,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts b/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts index db73c5e21693..e5e9d4bc3a66 100644 --- a/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-3-no-response-entity/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml b/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml index 3844d9e01f44..3af22f009e45 100644 --- a/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml +++ b/samples/server/petstore/kotlin-springboot-3-no-response-entity/pom.xml @@ -132,6 +132,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-3/build.gradle.kts b/samples/server/petstore/kotlin-springboot-3/build.gradle.kts index db73c5e21693..e5e9d4bc3a66 100644 --- a/samples/server/petstore/kotlin-springboot-3/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-3/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/samples/server/petstore/kotlin-springboot-3/pom.xml b/samples/server/petstore/kotlin-springboot-3/pom.xml index 3844d9e01f44..3af22f009e45 100644 --- a/samples/server/petstore/kotlin-springboot-3/pom.xml +++ b/samples/server/petstore/kotlin-springboot-3/pom.xml @@ -132,6 +132,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-4/build.gradle.kts b/samples/server/petstore/kotlin-springboot-4/build.gradle.kts index 3459856672ac..17e2e5ab29b9 100644 --- a/samples/server/petstore/kotlin-springboot-4/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-4/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation("tools.jackson.dataformat:jackson-dataformat-xml") implementation("tools.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:3.0.0") diff --git a/samples/server/petstore/kotlin-springboot-4/pom.xml b/samples/server/petstore/kotlin-springboot-4/pom.xml index 3ce44dbf761c..08ccfa05f02e 100644 --- a/samples/server/petstore/kotlin-springboot-4/pom.xml +++ b/samples/server/petstore/kotlin-springboot-4/pom.xml @@ -115,6 +115,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts b/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts index db73c5e21693..e5e9d4bc3a66 100644 --- a/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-additionalproperties/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml b/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml index 3844d9e01f44..3af22f009e45 100644 --- a/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml +++ b/samples/server/petstore/kotlin-springboot-additionalproperties/pom.xml @@ -132,6 +132,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts b/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts index 319a704f5842..3b564eb0a224 100644 --- a/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-bigdecimal-default/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml b/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml index 76ef64f955da..3266249b3379 100644 --- a/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml +++ b/samples/server/petstore/kotlin-springboot-bigdecimal-default/pom.xml @@ -125,6 +125,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts index 6122df330e69..10999697dcaa 100644 --- a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml index c7a557207ccc..0c987a9af209 100644 --- a/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml +++ b/samples/server/petstore/kotlin-springboot-delegate-nodefaults/pom.xml @@ -138,6 +138,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts b/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts index 319a704f5842..3b564eb0a224 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-delegate/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-delegate/pom.xml b/samples/server/petstore/kotlin-springboot-delegate/pom.xml index 76ef64f955da..3266249b3379 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/pom.xml +++ b/samples/server/petstore/kotlin-springboot-delegate/pom.xml @@ -125,6 +125,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts index 3fa8025c39dd..0f622040f5d6 100644 --- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml index 9e5b5f9d5877..a8e82b9771f9 100644 --- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml +++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/pom.xml @@ -136,6 +136,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt index 977ec3a70961..ee67a71a0410 100644 --- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -7,6 +7,7 @@ package org.openapitools.api import org.openapitools.model.ModelApiResponse import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault import org.openapitools.model.Pet import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation @@ -183,7 +184,7 @@ interface PetApi { produces = ["application/json"] ) fun listAllPetsPaginated(@ApiParam(hidden = true) exchange: org.springframework.web.server.ServerWebExchange, - @ApiParam(hidden = true) pageable: Pageable): ResponseEntity> { + @PageableDefault(page = 0, size = 20) @ApiParam(hidden = true) pageable: Pageable): ResponseEntity> { return getDelegate().listAllPetsPaginated(exchange, pageable) } diff --git a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt index f1b877a0fbb7..ad4642030058 100644 --- a/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt +++ b/samples/server/petstore/kotlin-springboot-include-http-request-context-delegate/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt @@ -2,6 +2,7 @@ package org.openapitools.api import org.openapitools.model.ModelApiResponse import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault import org.openapitools.model.Pet import org.springframework.http.HttpStatus import org.springframework.http.MediaType diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts b/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts index 559b1f327bec..ff5dc81c6a46 100644 --- a/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-integer-enum/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml b/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml index 67af2523dcb8..6e01abd57bbb 100644 --- a/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml +++ b/samples/server/petstore/kotlin-springboot-integer-enum/pom.xml @@ -132,6 +132,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts b/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts index 319a704f5842..3b564eb0a224 100644 --- a/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-modelMutable/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml b/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml index 76ef64f955da..3266249b3379 100644 --- a/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml +++ b/samples/server/petstore/kotlin-springboot-modelMutable/pom.xml @@ -125,6 +125,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts b/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts index 319a704f5842..3b564eb0a224 100644 --- a/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml b/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml index 76ef64f955da..3266249b3379 100644 --- a/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml +++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/pom.xml @@ -125,6 +125,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts index b3086b53f094..7dcfd792a49e 100644 --- a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml index 7a6726d7bbfd..37342a0b679b 100644 --- a/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml +++ b/samples/server/petstore/kotlin-springboot-no-response-entity-delegate/pom.xml @@ -119,6 +119,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts b/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts index b3086b53f094..7dcfd792a49e 100644 --- a/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-no-response-entity/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml b/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml index 7a6726d7bbfd..37342a0b679b 100644 --- a/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml +++ b/samples/server/petstore/kotlin-springboot-no-response-entity/pom.xml @@ -119,6 +119,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts b/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts index f8d51547a12e..9294e90a715a 100644 --- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml b/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml index 39afc668db31..d0e993868ac5 100644 --- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml +++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/pom.xml @@ -136,6 +136,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts b/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts index f8d51547a12e..9294e90a715a 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-reactive/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-reactive/pom.xml b/samples/server/petstore/kotlin-springboot-reactive/pom.xml index 39afc668db31..d0e993868ac5 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/pom.xml +++ b/samples/server/petstore/kotlin-springboot-reactive/pom.xml @@ -136,6 +136,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts b/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts index fa644c9b144c..ddfd0a5cffd6 100644 --- a/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-request-cookie/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") diff --git a/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml b/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml index 797865c697a9..c7a7145785d9 100644 --- a/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml +++ b/samples/server/petstore/kotlin-springboot-request-cookie/pom.xml @@ -138,6 +138,10 @@ jakarta.validation jakarta.validation-api + + org.springframework.boot + spring-boot-starter-validation + jakarta.annotation jakarta.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator-ignore b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/FILES new file mode 100644 index 000000000000..90a2baceec60 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/FILES @@ -0,0 +1,17 @@ +README.md +build.gradle.kts +gradle/wrapper/gradle-wrapper.jar +gradle/wrapper/gradle-wrapper.properties +gradlew +gradlew.bat +pom.xml +settings.gradle +src/main/kotlin/org/openapitools/api/ApiUtil.kt +src/main/kotlin/org/openapitools/api/Exceptions.kt +src/main/kotlin/org/openapitools/api/PetApi.kt +src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt +src/main/kotlin/org/openapitools/configuration/ValidPageable.kt +src/main/kotlin/org/openapitools/configuration/ValidSort.kt +src/main/kotlin/org/openapitools/model/Pet.kt +src/main/kotlin/org/openapitools/model/PetSort.kt +src/main/kotlin/org/openapitools/model/PetSortEnum.kt diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/VERSION b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/VERSION new file mode 100644 index 000000000000..f7962df3e243 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.22.0-SNAPSHOT diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/README.md b/samples/server/petstore/kotlin-springboot-sort-validation/README.md new file mode 100644 index 000000000000..3808563e513f --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/README.md @@ -0,0 +1,21 @@ +# openAPIPetstoreSortValidationTest + +This Kotlin based [Spring Boot](https://spring.io/projects/spring-boot) application has been generated using the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator). + +## Getting Started + +This document assumes you have either maven or gradle available, either via the wrapper or otherwise. This does not come with a gradle / maven wrapper checked in. + +By default a [`pom.xml`](pom.xml) file will be generated. If you specified `gradleBuildFile=true` when generating this project, a `build.gradle.kts` will also be generated. Note this uses [Gradle Kotlin DSL](https://github.com/gradle/kotlin-dsl). + +To build the project using maven, run: +```bash +mvn package && java -jar target/openapi-spring-1.0.0.jar +``` + +To build the project using gradle, run: +```bash +gradle build && java -jar build/libs/openapi-spring-1.0.0.jar +``` + +If all builds successfully, the server should run on [http://localhost:8080/](http://localhost:8080/) diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/build.gradle.kts b/samples/server/petstore/kotlin-springboot-sort-validation/build.gradle.kts new file mode 100644 index 000000000000..ff5dc81c6a46 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +group = "org.openapitools" +version = "1.0.0" +java.sourceCompatibility = JavaVersion.VERSION_17 + +repositories { + mavenCentral() + maven { url = uri("https://repo.spring.io/milestone") } +} + +tasks.withType { + kotlinOptions.jvmTarget = "17" +} + +tasks.bootJar { + enabled = false +} + +plugins { + val kotlinVersion = "1.9.25" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "3.0.2" + id("io.spring.dependency-management") version "1.0.14.RELEASE" +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springframework.boot:spring-boot-starter-web") + + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("jakarta.validation:jakarta.validation-api") + implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") + + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(module = "junit") + } +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.jar b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000000..e6441136f3d4 Binary files /dev/null and b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.properties b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..80187ac30432 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradlew b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew new file mode 100644 index 000000000000..9d0ce634cb11 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while +APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path +[ -h "$app_path" ] +do +ls=$( ls -ld "$app_path" ) +link=${ls#*' -> '} +case $link in #( +/*) app_path=$link ;; #( +*) app_path=$APP_HOME$link ;; +esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { +echo "$*" +} >&2 + +die () { +echo +echo "$*" +echo +exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( +CYGWIN* ) cygwin=true ;; #( +Darwin* ) darwin=true ;; #( +MSYS* | MINGW* ) msys=true ;; #( +NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then +if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# IBM's JDK on AIX uses strange locations for the executables +JAVACMD=$JAVA_HOME/jre/sh/java +else +JAVACMD=$JAVA_HOME/bin/java +fi +if [ ! -x "$JAVACMD" ] ; then +die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +else +JAVACMD=java +if ! command -v java >/dev/null 2>&1 +then +die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then +case $MAX_FD in #( +max*) +# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +MAX_FD=$( ulimit -H -n ) || +warn "Could not query maximum file descriptor limit" +esac +case $MAX_FD in #( +'' | soft) :;; #( +*) +# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +ulimit -n "$MAX_FD" || +warn "Could not set maximum file descriptor limit to $MAX_FD" +esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then +APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) +CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + +JAVACMD=$( cygpath --unix "$JAVACMD" ) + +# Now convert the arguments - kludge to limit ourselves to /bin/sh +for arg do +if +case $arg in #( +-*) false ;; # don't mess with options #( +/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath +[ -e "$t" ] ;; #( +*) false ;; +esac +then +arg=$( cygpath --path --ignore --mixed "$arg" ) +fi +# Roll the args list around exactly as many times as the number of +# args, so each arg winds up back in the position where it started, but +# possibly modified. +# +# NB: a `for` loop captures its iteration list before it begins, so +# changing the positional parameters here affects neither the number of +# iterations, nor the values presented in `arg`. +shift # remove old arg +set -- "$@" "$arg" # push replacement arg +done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ +"-Dorg.gradle.appname=$APP_BASE_NAME" \ +-classpath "$CLASSPATH" \ +org.gradle.wrapper.GradleWrapperMain \ +"$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then +die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( +printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | +xargs -n1 | +sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | +tr '\n' ' ' +)" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/gradlew.bat b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew.bat new file mode 100644 index 000000000000..25da30dbdeee --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/pom.xml b/samples/server/petstore/kotlin-springboot-sort-validation/pom.xml new file mode 100644 index 000000000000..6e01abd57bbb --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/pom.xml @@ -0,0 +1,152 @@ + + 4.0.0 + org.openapitools + openapi-spring + jar + openapi-spring + 1.0.0 + + 3.0.2 + 2.1.0 + 1.7.10 + + 1.7.10 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + + repository.spring.milestone + Spring Milestone Repository + https://repo.spring.io/milestone + + + + + spring-milestones + https://repo.spring.io/milestone + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 17 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.data + spring-data-commons + + + + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + + jakarta.validation + jakarta.validation-api + + + org.springframework.boot + spring-boot-starter-validation + + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/settings.gradle b/samples/server/petstore/kotlin-springboot-sort-validation/settings.gradle new file mode 100644 index 000000000000..14844905cd40 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url = uri("https://repo.spring.io/snapshot") } + maven { url = uri("https://repo.spring.io/milestone") } + gradlePluginPortal() + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") + } + } + } +} +rootProject.name = "openapi-spring" diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/Application.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/Application.kt new file mode 100644 index 000000000000..411871b327af --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/Application.kt @@ -0,0 +1,13 @@ +package org.openapitools + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan("org.openapitools") +class Application + +fun main(args: Array) { + runApplication(*args) +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/ApiUtil.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/ApiUtil.kt new file mode 100644 index 000000000000..03344e13b474 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/ApiUtil.kt @@ -0,0 +1,19 @@ +package org.openapitools.api + +import org.springframework.web.context.request.NativeWebRequest + +import jakarta.servlet.http.HttpServletResponse +import java.io.IOException + +object ApiUtil { + fun setExampleResponse(req: NativeWebRequest, contentType: String, example: String) { + try { + val res = req.getNativeResponse(HttpServletResponse::class.java) + res?.characterEncoding = "UTF-8" + res?.addHeader("Content-Type", contentType) + res?.writer?.print(example) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/Exceptions.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/Exceptions.kt new file mode 100644 index 000000000000..1bd78f54576a --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/Exceptions.kt @@ -0,0 +1,30 @@ +package org.openapitools.api + +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import jakarta.servlet.http.HttpServletResponse +import jakarta.validation.ConstraintViolationException + +// TODO Extend ApiException for custom exception handling, e.g. the below NotFound exception +sealed class ApiException(msg: String, val code: Int) : Exception(msg) + +class NotFoundException(msg: String, code: Int = HttpStatus.NOT_FOUND.value()) : ApiException(msg, code) + +@Configuration("org.openapitools.api.DefaultExceptionHandler") +@ControllerAdvice +class DefaultExceptionHandler { + + @ExceptionHandler(value = [ApiException::class]) + fun onApiException(ex: ApiException, response: HttpServletResponse): Unit = + response.sendError(ex.code, ex.message) + + @ExceptionHandler(value = [NotImplementedError::class]) + fun onNotImplemented(ex: NotImplementedError, response: HttpServletResponse): Unit = + response.sendError(HttpStatus.NOT_IMPLEMENTED.value()) + + @ExceptionHandler(value = [ConstraintViolationException::class]) + fun onConstraintViolation(ex: ConstraintViolationException, response: HttpServletResponse): Unit = + response.sendError(HttpStatus.BAD_REQUEST.value(), ex.constraintViolations.joinToString(", ") { it.message }) +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApi.kt new file mode 100644 index 000000000000..ab39b580d32d --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -0,0 +1,250 @@ +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.22.0-SNAPSHOT). + * https://openapi-generator.tech + * Do not edit the class manually. +*/ +package org.openapitools.api + +import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault +import org.openapitools.model.Pet +import org.openapitools.model.PetSort +import org.openapitools.model.PetSortEnum +import org.springframework.data.domain.Sort +import org.springframework.data.web.SortDefault +import org.openapitools.configuration.ValidPageable +import org.openapitools.configuration.ValidSort +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@RequestMapping("\${api.base-path:/v2}") +interface PetApi { + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findAutoDetectedWithSort" + value = [PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT], + produces = ["application/json"] + ) + fun findPetsAutoDetectedWithSort( + @Valid @RequestParam(value = "status", required = false) status: kotlin.String?, + @Valid @RequestParam(value = "page", required = false, defaultValue = "0") page: kotlin.Int, + @Valid @RequestParam(value = "size", required = false, defaultValue = "20") size: kotlin.Int, + @Valid @RequestParam(value = "sort", required = false) sort: kotlin.String? + ): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findNonPaginatedWithSortEnum" + value = [PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM], + produces = ["application/json"] + ) + fun findPetsNonPaginatedWithSortEnum( + @Valid @RequestParam(value = "sort", required = false) sort: kotlin.String? + ): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithAllDefaults" + value = [PATH_FIND_PETS_WITH_ALL_DEFAULTS], + produces = ["application/json"] + ) + fun findPetsWithAllDefaults(@PageableDefault(page = 0, size = 10) @SortDefault.SortDefaults(SortDefault(sort = ["name"], direction = Sort.Direction.DESC), SortDefault(sort = ["id"], direction = Sort.Direction.ASC)) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithArraySortEnum" + value = [PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM], + produces = ["application/json"] + ) + fun findPetsWithArraySortEnum(@ValidSort(allowedValues = ["id,asc", "id,desc", "name,asc", "name,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithArraySortRefEnum" + value = [PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM], + produces = ["application/json"] + ) + fun findPetsWithArraySortRefEnum(@ValidSort(allowedValues = ["id,asc", "id,desc", "createdAt,asc", "createdAt,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithExternalParamRefArraySort" + value = [PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT], + produces = ["application/json"] + ) + fun findPetsWithExternalParamRefArraySort(@ValidSort(allowedValues = ["name,asc", "name,desc", "id,asc", "id,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithMixedSortDefaults" + value = [PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS], + produces = ["application/json"] + ) + fun findPetsWithMixedSortDefaults(@SortDefault.SortDefaults(SortDefault(sort = ["name"], direction = Sort.Direction.DESC), SortDefault(sort = ["id"], direction = Sort.Direction.ASC)) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithNonExplodedExternalParamRefArraySort" + value = [PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT], + produces = ["application/json"] + ) + fun findPetsWithNonExplodedExternalParamRefArraySort(@ValidSort(allowedValues = ["name,asc", "name,desc", "id,asc", "id,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithPageAndSizeConstraint" + value = [PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT], + produces = ["application/json"] + ) + fun findPetsWithPageAndSizeConstraint(@ValidPageable(maxSize = 50, maxPage = 999) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithPageSizeDefaultsOnly" + value = [PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY], + produces = ["application/json"] + ) + fun findPetsWithPageSizeDefaultsOnly(@PageableDefault(page = 0, size = 25) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithRefSort" + value = [PATH_FIND_PETS_WITH_REF_SORT], + produces = ["application/json"] + ) + fun findPetsWithRefSort(@ValidSort(allowedValues = ["id,asc", "id,desc", "createdAt,asc", "createdAt,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithSizeConstraint" + value = [PATH_FIND_PETS_WITH_SIZE_CONSTRAINT], + produces = ["application/json"] + ) + fun findPetsWithSizeConstraint(@ValidPageable(maxSize = 100) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithSortDefaultAsc" + value = [PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC], + produces = ["application/json"] + ) + fun findPetsWithSortDefaultAsc(@SortDefault.SortDefaults(SortDefault(sort = ["id"], direction = Sort.Direction.ASC)) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithSortDefaultOnly" + value = [PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY], + produces = ["application/json"] + ) + fun findPetsWithSortDefaultOnly(@SortDefault.SortDefaults(SortDefault(sort = ["name"], direction = Sort.Direction.DESC)) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findByStatusWithSort" + value = [PATH_FIND_PETS_WITH_SORT_ENUM], + produces = ["application/json"] + ) + fun findPetsWithSortEnum( + @Valid @RequestParam(value = "status", required = false) status: kotlin.String?, + @ValidSort(allowedValues = ["id,asc", "id,desc", "name,asc", "name,desc"]) @PageableDefault(page = 0, size = 20) pageable: Pageable + ): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + + @RequestMapping( + method = [RequestMethod.GET], + // "/pet/findWithoutSortEnum" + value = [PATH_FIND_PETS_WITHOUT_SORT_ENUM], + produces = ["application/json"] + ) + fun findPetsWithoutSortEnum(@PageableDefault(page = 0, size = 20) pageable: Pageable): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + companion object { + //for your own safety never directly reuse these path definitions in tests + const val BASE_PATH: String = "/v2" + const val PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT: String = "/pet/findAutoDetectedWithSort" + const val PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM: String = "/pet/findNonPaginatedWithSortEnum" + const val PATH_FIND_PETS_WITH_ALL_DEFAULTS: String = "/pet/findWithAllDefaults" + const val PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM: String = "/pet/findWithArraySortEnum" + const val PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM: String = "/pet/findWithArraySortRefEnum" + const val PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT: String = "/pet/findWithExternalParamRefArraySort" + const val PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS: String = "/pet/findWithMixedSortDefaults" + const val PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT: String = "/pet/findWithNonExplodedExternalParamRefArraySort" + const val PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT: String = "/pet/findWithPageAndSizeConstraint" + const val PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY: String = "/pet/findWithPageSizeDefaultsOnly" + const val PATH_FIND_PETS_WITH_REF_SORT: String = "/pet/findWithRefSort" + const val PATH_FIND_PETS_WITH_SIZE_CONSTRAINT: String = "/pet/findWithSizeConstraint" + const val PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC: String = "/pet/findWithSortDefaultAsc" + const val PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY: String = "/pet/findWithSortDefaultOnly" + const val PATH_FIND_PETS_WITH_SORT_ENUM: String = "/pet/findByStatusWithSort" + const val PATH_FIND_PETS_WITHOUT_SORT_ENUM: String = "/pet/findWithoutSortEnum" + } +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApiImpl.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApiImpl.kt new file mode 100644 index 000000000000..6e7ee15a7a3d --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/api/PetApiImpl.kt @@ -0,0 +1,132 @@ +package org.openapitools.api + +import org.openapitools.model.Pet +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Component +import org.springframework.web.bind.annotation.RestController + +/** + * Sample implementation of [PetApi] demonstrating that the generated + * annotations (@ValidSort, @ValidPageable, @PageableDefault, @SortDefault) + * behave correctly at runtime. + * + * Methods whose endpoint carries pagination/sort defaults assert the exact + * expected values inside the method body. When the Spring argument resolvers + * apply the annotated defaults correctly, the assertions pass and HTTP 200 is + * returned. If a default is missing or wrong, the assertion throws + * [IllegalStateException] and the request fails with HTTP 500, which causes + * any calling test to fail with a clear message. + * + * Methods that only carry @ValidSort / @ValidPageable constraints need no body + * logic — the constraint annotations reject invalid input before this code is + * ever reached, and the [DefaultExceptionHandler] maps the resulting + * [jakarta.validation.ConstraintViolationException] to HTTP 400. + */ +@RestController +class PetApiImpl : PetApi { + + // ── no pageable / no special defaults ──────────────────────────────────── + + override fun findPetsAutoDetectedWithSort( + status: String?, + page: Int, + size: Int, + sort: String?, + ): ResponseEntity> = ResponseEntity.ok(emptyList()) + + override fun findPetsNonPaginatedWithSortEnum( + sort: String?, + ): ResponseEntity> = ResponseEntity.ok(emptyList()) + + // ── @ValidSort only (+ @PageableDefault) ───────────────────────────────── + // Validation rejects bad sort values before the method is called. + + override fun findPetsWithArraySortEnum(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + override fun findPetsWithArraySortRefEnum(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + override fun findPetsWithExternalParamRefArraySort(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + override fun findPetsWithNonExplodedExternalParamRefArraySort(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + override fun findPetsWithRefSort(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + override fun findPetsWithSortEnum(status: String?, pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + override fun findPetsWithoutSortEnum(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + // ── @ValidPageable only ─────────────────────────────────────────────────── + // Validation rejects out-of-range page / size before the method is called. + + override fun findPetsWithSizeConstraint(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + override fun findPetsWithPageAndSizeConstraint(pageable: Pageable): ResponseEntity> = + ResponseEntity.ok(emptyList()) + + // ── @PageableDefault ───────────────────────────────────────────────────── + // @PageableDefault(page = 0, size = 25) + + override fun findPetsWithPageSizeDefaultsOnly(pageable: Pageable): ResponseEntity> { + check(pageable.pageNumber == 0) { "@PageableDefault page: expected 0, got ${pageable.pageNumber}" } + check(pageable.pageSize == 25) { "@PageableDefault size: expected 25, got ${pageable.pageSize}" } + return ResponseEntity.ok(emptyList()) + } + + // ── @SortDefault ───────────────────────────────────────────────────────── + // @SortDefault(sort = ["name"], direction = DESC) + + override fun findPetsWithSortDefaultOnly(pageable: Pageable): ResponseEntity> { + check(pageable.sort.getOrderFor("name")?.direction == Sort.Direction.DESC) { + "@SortDefault sort: expected name DESC, got ${pageable.sort}" + } + return ResponseEntity.ok(emptyList()) + } + + // @SortDefault(sort = ["id"], direction = ASC) + + override fun findPetsWithSortDefaultAsc(pageable: Pageable): ResponseEntity> { + check(pageable.sort.getOrderFor("id")?.direction == Sort.Direction.ASC) { + "@SortDefault sort: expected id ASC, got ${pageable.sort}" + } + return ResponseEntity.ok(emptyList()) + } + + // ── @SortDefault.SortDefaults ───────────────────────────────────────────── + // @SortDefaults(SortDefault(sort = ["name"], direction = DESC), SortDefault(sort = ["id"], direction = ASC)) + + override fun findPetsWithMixedSortDefaults(pageable: Pageable): ResponseEntity> { + check(pageable.sort.getOrderFor("name")?.direction == Sort.Direction.DESC) { + "@SortDefaults sort: expected name DESC, got ${pageable.sort}" + } + check(pageable.sort.getOrderFor("id")?.direction == Sort.Direction.ASC) { + "@SortDefaults sort: expected id ASC, got ${pageable.sort}" + } + return ResponseEntity.ok(emptyList()) + } + + // ── @PageableDefault + @SortDefault.SortDefaults combined ───────────────── + // @PageableDefault(page = 0, size = 10) + // @SortDefaults(SortDefault(sort = ["name"], direction = DESC), SortDefault(sort = ["id"], direction = ASC)) + + override fun findPetsWithAllDefaults(pageable: Pageable): ResponseEntity> { + check(pageable.pageNumber == 0) { "@PageableDefault page: expected 0, got ${pageable.pageNumber}" } + check(pageable.pageSize == 10) { "@PageableDefault size: expected 10, got ${pageable.pageSize}" } + check(pageable.sort.getOrderFor("name")?.direction == Sort.Direction.DESC) { + "@SortDefaults sort: expected name DESC, got ${pageable.sort}" + } + check(pageable.sort.getOrderFor("id")?.direction == Sort.Direction.ASC) { + "@SortDefaults sort: expected id ASC, got ${pageable.sort}" + } + return ResponseEntity.ok(emptyList()) + } +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt new file mode 100644 index 000000000000..131f87bc8c74 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt @@ -0,0 +1,33 @@ +package org.openapitools.configuration + +import org.openapitools.model.PetSort +import org.openapitools.model.PetSortEnum + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter + +/** + * This class provides Spring Converter beans for the enum models in the OpenAPI specification. + * + * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent + * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than + * `original` or the specification has an integer enum. + */ +@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration") +class EnumConverterConfiguration { + + @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.petSortConverter"]) + fun petSortConverter(): Converter { + return object: Converter { + override fun convert(source: kotlin.String): PetSort = PetSort.forValue(source) + } + } + @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.petSortEnumConverter"]) + fun petSortEnumConverter(): Converter { + return object: Converter { + override fun convert(source: kotlin.String): PetSortEnum = PetSortEnum.forValue(source) + } + } + +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidPageable.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidPageable.kt new file mode 100644 index 000000000000..671e682ec6fe --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidPageable.kt @@ -0,0 +1,81 @@ +package org.openapitools.configuration + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import jakarta.validation.Payload +import org.springframework.data.domain.Pageable + +/** + * Validates that the page number and page size in the annotated [Pageable] parameter do not + * exceed their configured maximums. + * + * Apply directly on a `pageable: Pageable` parameter. Each attribute is independently optional: + * - [maxSize] — when set (>= 0), validates `pageable.pageSize <= maxSize` + * - [maxPage] — when set (>= 0), validates `pageable.pageNumber <= maxPage` + * + * Use [NO_LIMIT] (= -1, the default) to leave an attribute unconstrained. + * + * Constraining [maxPage] is useful to prevent deep-pagination attacks, where a large page + * offset (e.g. `?page=100000&size=20`) causes an expensive `OFFSET` query on the database. + * + * @property maxSize Maximum allowed page size, or [NO_LIMIT] if unconstrained + * @property maxPage Maximum allowed page number (0-based), or [NO_LIMIT] if unconstrained + * @property groups Validation groups (optional) + * @property payload Additional payload (optional) + * @property message Validation error message (default: "Invalid page request") + */ +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [PageableConstraintValidator::class]) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class ValidPageable( + val maxSize: Int = ValidPageable.NO_LIMIT, + val maxPage: Int = ValidPageable.NO_LIMIT, + val groups: Array> = [], + val payload: Array> = [], + val message: String = "Invalid page request" +) { + companion object { + const val NO_LIMIT = -1 + } +} + +class PageableConstraintValidator : ConstraintValidator { + + private var maxSize = ValidPageable.NO_LIMIT + private var maxPage = ValidPageable.NO_LIMIT + + override fun initialize(constraintAnnotation: ValidPageable) { + maxSize = constraintAnnotation.maxSize + maxPage = constraintAnnotation.maxPage + } + + override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean { + if (pageable == null) return true + if (!pageable.isPaged) return true + + var valid = true + context.disableDefaultConstraintViolation() + + if (maxSize >= 0 && pageable.pageSize > maxSize) { + context.buildConstraintViolationWithTemplate( + "${context.defaultConstraintMessageTemplate}: page size ${pageable.pageSize} exceeds maximum $maxSize" + ) + .addPropertyNode("size") + .addConstraintViolation() + valid = false + } + + if (maxPage >= 0 && pageable.pageNumber > maxPage) { + context.buildConstraintViolationWithTemplate( + "${context.defaultConstraintMessageTemplate}: page number ${pageable.pageNumber} exceeds maximum $maxPage" + ) + .addPropertyNode("page") + .addConstraintViolation() + valid = false + } + + return valid + } +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidSort.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidSort.kt new file mode 100644 index 000000000000..5c96f1c82814 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/configuration/ValidSort.kt @@ -0,0 +1,87 @@ +package org.openapitools.configuration + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import jakarta.validation.Payload +import org.springframework.data.domain.Pageable + +/** + * Validates that sort properties in the annotated [Pageable] parameter match the allowed values. + * + * Apply directly on a `pageable: Pageable` parameter. The validator checks that each sort + * property and direction combination in the [Pageable] matches one of the strings specified + * in [allowedValues]. + * + * Two formats are accepted in [allowedValues]: + * - `"property,direction"` — permits only the specific direction (e.g. `"id,asc"`, `"name,desc"`). + * Direction matching is case-insensitive: `"id,ASC"` and `"id,asc"` are treated identically. + * - `"property"` — permits any direction for that property (e.g. `"id"` matches `sort=id,asc` + * and `sort=id,desc`). Note: because Spring always normalises a bare `sort=id` to ascending + * before the validator runs, bare property names in [allowedValues] effectively allow all + * directions — the original omission of a direction cannot be detected. + * + * Both formats may be mixed freely. For example `["id", "name,desc"]` allows `id` in any + * direction but restricts `name` to descending only. + * + * @property allowedValues The allowed sort strings (e.g. `["id,asc", "id,desc"]`) + * @property groups Validation groups (optional) + * @property payload Additional payload (optional) + * @property message Validation error message (default: "Invalid sort column") + */ +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [SortValidator::class]) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class ValidSort( + val allowedValues: Array, + val groups: Array> = [], + val payload: Array> = [], + val message: String = "Invalid sort column" +) + +class SortValidator : ConstraintValidator { + + private lateinit var allowedValues: Set + + override fun initialize(constraintAnnotation: ValidSort) { + allowedValues = constraintAnnotation.allowedValues.map { entry -> + DIRECTION_ASC_SUFFIX.replace(entry, ",asc") + .let { DIRECTION_DESC_SUFFIX.replace(it, ",desc") } + }.toSet() + } + + private companion object { + val DIRECTION_ASC_SUFFIX = Regex(",ASC$", RegexOption.IGNORE_CASE) + val DIRECTION_DESC_SUFFIX = Regex(",DESC$", RegexOption.IGNORE_CASE) + } + + override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean { + if (pageable == null || pageable.sort.isUnsorted) return true + + val invalid = pageable.sort + .foldIndexed(emptyMap()) { index, acc, order -> + val sortValue = "${order.property},${order.direction.name.lowercase()}" + // Accept "property,direction" (exact match) OR "property" alone (any direction allowed) + if (sortValue !in allowedValues && order.property !in allowedValues) acc + (index to order.property) + else acc + } + .toSortedMap() + + if (invalid.isNotEmpty()) { + context.disableDefaultConstraintViolation() + invalid.forEach { (index, property) -> + context.buildConstraintViolationWithTemplate( + "${context.defaultConstraintMessageTemplate} [$property]" + ) + .addPropertyNode("sort") + .addPropertyNode("property") + .inIterable() + .atIndex(index) + .addConstraintViolation() + } + } + + return invalid.isEmpty() + } +} diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/Pet.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/Pet.kt new file mode 100644 index 000000000000..5e896ae0469b --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/Pet.kt @@ -0,0 +1,34 @@ +package org.openapitools.model + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid + +/** + * + * @param name + * @param id + * @param status pet status in the store + */ +data class Pet( + + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @get:JsonProperty("id") val id: kotlin.Long? = null, + + @get:JsonProperty("status") val status: kotlin.String? = null +) : java.io.Serializable { + + companion object { + private const val serialVersionUID: kotlin.Long = 1 + } +} + diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSort.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSort.kt new file mode 100644 index 000000000000..06ce004b5a90 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSort.kt @@ -0,0 +1,37 @@ +package org.openapitools.model + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid + +/** +* +* Values: idCommaAsc,idCommaDesc,createdAtCommaAsc,createdAtCommaDesc +*/ +enum class PetSort(@get:JsonValue val value: kotlin.String) : java.io.Serializable { + + idCommaAsc("id,asc"), + idCommaDesc("id,desc"), + createdAtCommaAsc("createdAt,asc"), + createdAtCommaDesc("createdAt,desc"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): PetSort { + return values().firstOrNull{it -> it.value == value} + ?: throw IllegalArgumentException("Unexpected value '$value' for enum 'PetSort'") + } + } +} + diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSortEnum.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSortEnum.kt new file mode 100644 index 000000000000..a8bb6d27cbc9 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/main/kotlin/org/openapitools/model/PetSortEnum.kt @@ -0,0 +1,37 @@ +package org.openapitools.model + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid + +/** +* +* Values: nameCommaAsc,nameCommaDesc,idCommaAsc,idCommaDesc +*/ +enum class PetSortEnum(@get:JsonValue val value: kotlin.String) : java.io.Serializable { + + nameCommaAsc("name,asc"), + nameCommaDesc("name,desc"), + idCommaAsc("id,asc"), + idCommaDesc("id,desc"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): PetSortEnum { + return values().firstOrNull{it -> it.value == value} + ?: throw IllegalArgumentException("Unexpected value '$value' for enum 'PetSortEnum'") + } + } +} + diff --git a/samples/server/petstore/kotlin-springboot-sort-validation/src/test/kotlin/org/openapitools/api/PetApiValidationTest.kt b/samples/server/petstore/kotlin-springboot-sort-validation/src/test/kotlin/org/openapitools/api/PetApiValidationTest.kt new file mode 100644 index 000000000000..e3f24334b53c --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-sort-validation/src/test/kotlin/org/openapitools/api/PetApiValidationTest.kt @@ -0,0 +1,177 @@ +package org.openapitools.api + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get + +/** + * Verifies the runtime behaviour of the annotations generated onto [PetApi]: + * + * - **@ValidSort** — invalid sort field/direction combinations are rejected with 400. + * - **@ValidPageable** — page number or size that exceeds the configured limit is rejected with 400. + * - **@PageableDefault** — when page/size query params are absent, the configured defaults are + * forwarded to the controller method (verified by assertions inside [PetApiImpl]). + * - **@SortDefault / @SortDefaults** — when the sort query param is absent, the configured + * default sort order is forwarded to the controller method (verified inside [PetApiImpl]). + * + * HTTP 200 responses confirm both that the request was accepted *and* that [PetApiImpl]'s + * internal assertions about the received defaults passed. + * HTTP 400 responses confirm that the constraint annotation rejected the invalid input. + */ +@SpringBootTest +@AutoConfigureMockMvc +class PetApiValidationTest { + + @Autowired + lateinit var mockMvc: MockMvc + + // ── @ValidSort ──────────────────────────────────────────────────────────── + // Endpoint: GET /pet/findWithArraySortEnum allowed: id,asc | id,desc | name,asc | name,desc + + @Test + fun `ValidSort - valid sort value returns 200`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") { + param("sort", "id,asc") + }.andExpect { status { isOk() } } + } + + @Test + fun `ValidSort - multiple valid sort values return 200`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") { + param("sort", "id,desc") + param("sort", "name,asc") + }.andExpect { status { isOk() } } + } + + @Test + fun `ValidSort - invalid sort property returns 400`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") { + param("sort", "unknown,asc") + }.andExpect { status { isBadRequest() } } + } + + @Test + fun `ValidSort - invalid sort direction returns 400`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") { + param("sort", "id,random") + }.andExpect { status { isBadRequest() } } + } + + @Test + fun `ValidSort - one invalid sort among multiple valid values returns 400`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM}") { + param("sort", "id,asc") + param("sort", "unknown,desc") + }.andExpect { status { isBadRequest() } } + } + + // ── @ValidPageable — size constraint only ───────────────────────────────── + // Endpoint: GET /pet/findWithSizeConstraint maxSize = 100 + + @Test + fun `ValidPageable - size below maximum returns 200`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}") { + param("size", "50") + }.andExpect { status { isOk() } } + } + + @Test + fun `ValidPageable - size at maximum returns 200`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}") { + param("size", "100") + }.andExpect { status { isOk() } } + } + + @Test + fun `ValidPageable - size exceeds maximum returns 400`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}") { + param("size", "101") + }.andExpect { status { isBadRequest() } } + } + + @Test + fun `ValidPageable - unpaged Pageable is allowed (no params, no PageableDefault)`() { + // When no pagination parameters are supplied and no @PageableDefault is configured, + // Spring resolves Pageable.unpaged(). The validator must not throw and must return valid. + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT}") + .andExpect { status { isOk() } } + } + + // ── @ValidPageable — size and page constraints combined ─────────────────── + // Endpoint: GET /pet/findWithPageAndSizeConstraint maxSize = 50, maxPage = 999 + + @Test + fun `ValidPageable - size and page at their maximums return 200`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT}") { + param("size", "50") + param("page", "999") + }.andExpect { status { isOk() } } + } + + @Test + fun `ValidPageable - size exceeds maximum for combined constraint returns 400`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT}") { + param("size", "51") + }.andExpect { status { isBadRequest() } } + } + + @Test + fun `ValidPageable - page exceeds maximum returns 400`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT}") { + param("page", "1000") + }.andExpect { status { isBadRequest() } } + } + + // ── @PageableDefault ───────────────────────────────────────────────────── + // Endpoint: GET /pet/findWithPageSizeDefaultsOnly @PageableDefault(page = 0, size = 25) + // PetApiImpl asserts page == 0 and size == 25; returns 200 on success, throws on mismatch. + + @Test + fun `PageableDefault - absent params resolve to configured page and size defaults`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY}") + .andExpect { status { isOk() } } + } + + // ── @SortDefault ───────────────────────────────────────────────────────── + // Endpoint: GET /pet/findWithSortDefaultOnly @SortDefault(sort = ["name"], direction = DESC) + // PetApiImpl asserts name DESC; returns 200 on success, throws on mismatch. + + @Test + fun `SortDefault - absent sort param resolves to name DESC default`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY}") + .andExpect { status { isOk() } } + } + + // Endpoint: GET /pet/findWithSortDefaultAsc @SortDefault(sort = ["id"], direction = ASC) + + @Test + fun `SortDefault - absent sort param resolves to id ASC default`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC}") + .andExpect { status { isOk() } } + } + + // ── @SortDefault.SortDefaults ───────────────────────────────────────────── + // Endpoint: GET /pet/findWithMixedSortDefaults + // @SortDefaults(SortDefault(["name"], DESC), SortDefault(["id"], ASC)) + // PetApiImpl asserts both orders; returns 200 on success, throws on mismatch. + + @Test + fun `SortDefaults - absent sort param resolves all configured sort defaults`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS}") + .andExpect { status { isOk() } } + } + + // ── @PageableDefault + @SortDefault.SortDefaults combined ───────────────── + // Endpoint: GET /pet/findWithAllDefaults + // @PageableDefault(page = 0, size = 10) + @SortDefaults(name DESC, id ASC) + // PetApiImpl asserts page, size, and both sort orders. + + @Test + fun `PageableDefault and SortDefaults combined - absent params resolve all defaults`() { + mockMvc.get("${PetApi.BASE_PATH}${PetApi.PATH_FIND_PETS_WITH_ALL_DEFAULTS}") + .andExpect { status { isOk() } } + } +} diff --git a/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts b/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts index cd5e76efe1bd..bbf10f687c00 100644 --- a/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-source-swagger1/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml b/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml index 07c34bb1288e..85b4059abd00 100644 --- a/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml +++ b/samples/server/petstore/kotlin-springboot-source-swagger1/pom.xml @@ -135,6 +135,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts b/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts index bab28e4573e9..fd5cbc3238f8 100644 --- a/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-source-swagger2/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml b/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml index b8f74802da03..121e436cec3e 100644 --- a/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml +++ b/samples/server/petstore/kotlin-springboot-source-swagger2/pom.xml @@ -135,6 +135,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts index 39df365c5d0b..36af32631e9a 100644 --- a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml index abd0f31c98ca..a812b05160ad 100644 --- a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml +++ b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/pom.xml @@ -125,6 +125,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt index 5286ee01a091..46c8f24262b2 100644 --- a/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-springboot-x-kotlin-implements/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -7,6 +7,7 @@ package org.openapitools.api import org.openapitools.model.ModelApiResponse import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault import org.openapitools.model.Pet import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation @@ -170,7 +171,7 @@ interface PetApi { produces = ["application/json"] ) fun listAllPetsPaginated(@ApiParam(hidden = true) request: javax.servlet.http.HttpServletRequest, - @ApiParam(hidden = true) pageable: Pageable): ResponseEntity> + @PageableDefault(page = 0, size = 20) @ApiParam(hidden = true) pageable: Pageable): ResponseEntity> @ApiOperation( diff --git a/samples/server/petstore/kotlin-springboot/build.gradle.kts b/samples/server/petstore/kotlin-springboot/build.gradle.kts index b3086b53f094..7dcfd792a49e 100644 --- a/samples/server/petstore/kotlin-springboot/build.gradle.kts +++ b/samples/server/petstore/kotlin-springboot/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.data:spring-data-commons") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("javax.validation:validation-api") implementation("javax.annotation:javax.annotation-api:1.3.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/samples/server/petstore/kotlin-springboot/pom.xml b/samples/server/petstore/kotlin-springboot/pom.xml index 7a6726d7bbfd..37342a0b679b 100644 --- a/samples/server/petstore/kotlin-springboot/pom.xml +++ b/samples/server/petstore/kotlin-springboot/pom.xml @@ -119,6 +119,10 @@ javax.validation validation-api + + org.springframework.boot + spring-boot-starter-validation + javax.annotation javax.annotation-api diff --git a/samples/server/petstore/springboot-sort-validation/.openapi-generator-ignore b/samples/server/petstore/springboot-sort-validation/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/springboot-sort-validation/.openapi-generator/FILES b/samples/server/petstore/springboot-sort-validation/.openapi-generator/FILES new file mode 100644 index 000000000000..7bb3955083e0 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/.openapi-generator/FILES @@ -0,0 +1,16 @@ +README.md +pom.xml +src/main/java/org/openapitools/OpenApiGeneratorApplication.java +src/main/java/org/openapitools/RFC3339DateFormat.java +src/main/java/org/openapitools/api/ApiUtil.java +src/main/java/org/openapitools/api/PetApi.java +src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java +src/main/java/org/openapitools/configuration/HomeController.java +src/main/java/org/openapitools/configuration/ValidPageable.java +src/main/java/org/openapitools/configuration/ValidSort.java +src/main/java/org/openapitools/model/Pet.java +src/main/java/org/openapitools/model/PetSort.java +src/main/java/org/openapitools/model/PetSortEnum.java +src/main/resources/application.properties +src/main/resources/openapi.yaml +src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java diff --git a/samples/server/petstore/springboot-sort-validation/.openapi-generator/VERSION b/samples/server/petstore/springboot-sort-validation/.openapi-generator/VERSION new file mode 100644 index 000000000000..f7962df3e243 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.22.0-SNAPSHOT diff --git a/samples/server/petstore/springboot-sort-validation/README.md b/samples/server/petstore/springboot-sort-validation/README.md new file mode 100644 index 000000000000..7e955e89350e --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/README.md @@ -0,0 +1,12 @@ +# OpenAPI generated server + +Spring Boot Server + +## Overview +This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. +By using the [OpenAPI-Spec](https://openapis.org), you can easily generate a server stub. +This is an example of building a OpenAPI-enabled server in Java using the SpringBoot framework. + + +Start your server as a simple java application +Change default port value in application.properties \ No newline at end of file diff --git a/samples/server/petstore/springboot-sort-validation/pom.xml b/samples/server/petstore/springboot-sort-validation/pom.xml new file mode 100644 index 000000000000..adb270620c8c --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + org.openapitools + openapi-spring + jar + openapi-spring + 1.0.0 + + 17 + ${java.version} + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.13 + + + + + src/main/java + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.data + spring-data-commons + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.openapitools + jackson-databind-nullable + 0.2.10 + + + + org.springframework.boot + spring-boot-starter-validation + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/OpenApiGeneratorApplication.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/OpenApiGeneratorApplication.java new file mode 100644 index 000000000000..97252a8a9402 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/OpenApiGeneratorApplication.java @@ -0,0 +1,30 @@ +package org.openapitools; + +import com.fasterxml.jackson.databind.Module; +import org.openapitools.jackson.nullable.JsonNullableModule; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator; + +@SpringBootApplication( + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +@ComponentScan( + basePackages = {"org.openapitools", "org.openapitools.api" , "org.openapitools.configuration"}, + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +public class OpenApiGeneratorApplication { + + public static void main(String[] args) { + SpringApplication.run(OpenApiGeneratorApplication.class, args); + } + + @Bean(name = "org.openapitools.OpenApiGeneratorApplication.jsonNullableModule") + public Module jsonNullableModule() { + return new JsonNullableModule(); + } + +} \ No newline at end of file diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/RFC3339DateFormat.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/RFC3339DateFormat.java new file mode 100644 index 000000000000..bcd3936d8b34 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/RFC3339DateFormat.java @@ -0,0 +1,38 @@ +package org.openapitools; + +import com.fasterxml.jackson.databind.util.StdDateFormat; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +public class RFC3339DateFormat extends DateFormat { + private static final long serialVersionUID = 1L; + private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC"); + + private final StdDateFormat fmt = new StdDateFormat() + .withTimeZone(TIMEZONE_Z) + .withColonInTimeZone(true); + + public RFC3339DateFormat() { + this.calendar = new GregorianCalendar(); + } + + @Override + public Date parse(String source, ParsePosition pos) { + return fmt.parse(source, pos); + } + + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + return fmt.format(date, toAppendTo, fieldPosition); + } + + @Override + public Object clone() { + return this; + } +} \ No newline at end of file diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/ApiUtil.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/ApiUtil.java new file mode 100644 index 000000000000..44bf770ccc47 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/ApiUtil.java @@ -0,0 +1,21 @@ +package org.openapitools.api; + +import org.springframework.web.context.request.NativeWebRequest; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ApiUtil { + public static void setExampleResponse(NativeWebRequest req, String contentType, String example) { + try { + HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class); + if (res != null) { + res.setCharacterEncoding("UTF-8"); + res.addHeader("Content-Type", contentType); + res.getWriter().print(example); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApi.java new file mode 100644 index 000000000000..6f6fa07d77c9 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApi.java @@ -0,0 +1,300 @@ +/* + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.22.0-SNAPSHOT). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +package org.openapitools.api; + +import org.springframework.lang.Nullable; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.openapitools.model.Pet; +import org.openapitools.model.PetSort; +import org.openapitools.model.PetSortEnum; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.openapitools.configuration.ValidPageable; +import org.openapitools.configuration.ValidSort; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import jakarta.annotation.Generated; + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT") +@Validated +@RequestMapping("${openapi.openAPIPetstoreSortValidationTest.base-path:/v2}") +public interface PetApi { + + String PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT = "/pet/findAutoDetectedWithSort"; + /** + * GET /pet/findAutoDetectedWithSort : Find pets with auto-detected pagination and sort enum + * + * @param status Status filter (optional) + * @param page (optional, default to 0) + * @param size (optional, default to 20) + * @param sort Sort order (optional) + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_AUTO_DETECTED_WITH_SORT, + produces = { "application/json" } + ) + ResponseEntity> findPetsAutoDetectedWithSort( + @Valid @RequestParam(value = "status", required = false) @Nullable String status, + @Valid @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, + @Valid @RequestParam(value = "size", required = false, defaultValue = "20") Integer size, + @Valid @RequestParam(value = "sort", required = false) @Nullable String sort + ); + + + String PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM = "/pet/findNonPaginatedWithSortEnum"; + /** + * GET /pet/findNonPaginatedWithSortEnum : Find pets without pagination but sort param has enum — no sort validation expected + * + * @param sort Sort order with enum but no pagination (optional) + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_NON_PAGINATED_WITH_SORT_ENUM, + produces = { "application/json" } + ) + ResponseEntity> findPetsNonPaginatedWithSortEnum( + @Valid @RequestParam(value = "sort", required = false) @Nullable String sort + ); + + + String PATH_FIND_PETS_WITH_ALL_DEFAULTS = "/pet/findWithAllDefaults"; + /** + * GET /pet/findWithAllDefaults : Find pets — page, size, and mixed sort defaults all present + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_ALL_DEFAULTS, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithAllDefaults( + @PageableDefault(page = 0, size = 10) @SortDefault.SortDefaults({@SortDefault(sort = {"name"}, direction = Sort.Direction.DESC), @SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM = "/pet/findWithArraySortEnum"; + /** + * GET /pet/findWithArraySortEnum : Find pets with x-spring-paginated and array sort param with inline enum on items + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithArraySortEnum( + @ValidSort(allowedValues = {"id,asc", "id,desc", "name,asc", "name,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM = "/pet/findWithArraySortRefEnum"; + /** + * GET /pet/findWithArraySortRefEnum : Find pets with x-spring-paginated and array sort param whose items use a $ref enum + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_REF_ENUM, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithArraySortRefEnum( + @ValidSort(allowedValues = {"id,asc", "id,desc", "createdAt,asc", "createdAt,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT = "/pet/findWithExternalParamRefArraySort"; + /** + * GET /pet/findWithExternalParamRefArraySort : Find pets with x-spring-paginated and sort param referenced from an external components file + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_EXTERNAL_PARAM_REF_ARRAY_SORT, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithExternalParamRefArraySort( + @ValidSort(allowedValues = {"name,asc", "name,desc", "id,asc", "id,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS = "/pet/findWithMixedSortDefaults"; + /** + * GET /pet/findWithMixedSortDefaults : Find pets — multiple sort defaults with mixed directions (array sort param) + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithMixedSortDefaults( + @SortDefault.SortDefaults({@SortDefault(sort = {"name"}, direction = Sort.Direction.DESC), @SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT = "/pet/findWithNonExplodedExternalParamRefArraySort"; + /** + * GET /pet/findWithNonExplodedExternalParamRefArraySort : Find pets with x-spring-paginated and non-exploded sort param referenced from an external components file + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_NON_EXPLODED_EXTERNAL_PARAM_REF_ARRAY_SORT, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithNonExplodedExternalParamRefArraySort( + @ValidSort(allowedValues = {"name,asc", "name,desc", "id,asc", "id,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT = "/pet/findWithPageAndSizeConstraint"; + /** + * GET /pet/findWithPageAndSizeConstraint : Find pets — both page and size have maximum constraints + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithPageAndSizeConstraint( + @ValidPageable(maxSize = 50, maxPage = 999) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY = "/pet/findWithPageSizeDefaultsOnly"; + /** + * GET /pet/findWithPageSizeDefaultsOnly : Find pets — page and size defaults only, no sort default + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithPageSizeDefaultsOnly( + @PageableDefault(page = 0, size = 25) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_REF_SORT = "/pet/findWithRefSort"; + /** + * GET /pet/findWithRefSort : Find pets with x-spring-paginated and $ref sort enum + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_REF_SORT, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithRefSort( + @ValidSort(allowedValues = {"id,asc", "id,desc", "createdAt,asc", "createdAt,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_SIZE_CONSTRAINT = "/pet/findWithSizeConstraint"; + /** + * GET /pet/findWithSizeConstraint : Find pets — size has maximum constraint only + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithSizeConstraint( + @ValidPageable(maxSize = 100) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC = "/pet/findWithSortDefaultAsc"; + /** + * GET /pet/findWithSortDefaultAsc : Find pets — sort default only (single field, no explicit direction defaults to ASC) + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithSortDefaultAsc( + @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY = "/pet/findWithSortDefaultOnly"; + /** + * GET /pet/findWithSortDefaultOnly : Find pets — sort default only (single field DESC, no page/size defaults) + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithSortDefaultOnly( + @SortDefault.SortDefaults({@SortDefault(sort = {"name"}, direction = Sort.Direction.DESC)}) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITH_SORT_ENUM = "/pet/findByStatusWithSort"; + /** + * GET /pet/findByStatusWithSort : Find pets with explicit x-spring-paginated and inline sort enum + * + * @param status Status filter (optional) + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITH_SORT_ENUM, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithSortEnum( + @Valid @RequestParam(value = "status", required = false) @Nullable String status, + @ValidSort(allowedValues = {"id,asc", "id,desc", "name,asc", "name,desc"}) @PageableDefault(page = 0, size = 20) final Pageable pageable + ); + + + String PATH_FIND_PETS_WITHOUT_SORT_ENUM = "/pet/findWithoutSortEnum"; + /** + * GET /pet/findWithoutSortEnum : Find pets with pagination but sort has no enum constraint + * + * @return successful operation (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = PetApi.PATH_FIND_PETS_WITHOUT_SORT_ENUM, + produces = { "application/json" } + ) + ResponseEntity> findPetsWithoutSortEnum( + @PageableDefault(page = 0, size = 20) final Pageable pageable + ); + +} diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApiController.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApiController.java new file mode 100644 index 000000000000..bef1f3a47ab0 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/api/PetApiController.java @@ -0,0 +1,178 @@ +package org.openapitools.api; + +import org.openapitools.model.Pet; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; + +/** + * Sample implementation of {@link PetApi} demonstrating that the generated + * annotations ({@code @ValidSort}, {@code @ValidPageable}, {@code @PageableDefault}, + * {@code @SortDefault}) behave correctly at runtime. + * + * Methods whose endpoint carries pagination/sort defaults assert the exact expected values + * inside the method body. When the Spring argument resolvers apply the annotated defaults + * correctly, the assertions pass and HTTP 200 is returned. If a default is missing or wrong, + * the assertion throws {@link IllegalStateException} and the request fails with HTTP 500, + * causing any calling test to fail with a clear message. + * + * Methods that only carry {@code @ValidSort} / {@code @ValidPageable} constraints need no body + * logic — the constraint annotations reject invalid input before this code is ever reached, + * and the {@code DefaultExceptionHandler} maps the resulting + * {@link jakarta.validation.ConstraintViolationException} to HTTP 400. + */ +@RestController +public class PetApiController implements PetApi { + + // ── no pageable / no special defaults ──────────────────────────────────── + + @Override + public ResponseEntity> findPetsAutoDetectedWithSort( + String status, Integer page, Integer size, String sort) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsNonPaginatedWithSortEnum(String sort) { + return ResponseEntity.ok(Collections.emptyList()); + } + + // ── @ValidSort only (+ @PageableDefault) ───────────────────────────────── + + @Override + public ResponseEntity> findPetsWithArraySortEnum(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsWithArraySortRefEnum(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsWithExternalParamRefArraySort(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsWithNonExplodedExternalParamRefArraySort(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsWithRefSort(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsWithSortEnum(String status, Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsWithoutSortEnum(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + // ── @ValidPageable only ─────────────────────────────────────────────────── + + @Override + public ResponseEntity> findPetsWithSizeConstraint(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + @Override + public ResponseEntity> findPetsWithPageAndSizeConstraint(Pageable pageable) { + return ResponseEntity.ok(Collections.emptyList()); + } + + // ── @PageableDefault ───────────────────────────────────────────────────── + // @PageableDefault(page = 0, size = 25) + + @Override + public ResponseEntity> findPetsWithPageSizeDefaultsOnly(Pageable pageable) { + if (pageable.getPageNumber() != 0) { + throw new IllegalStateException( + "@PageableDefault page: expected 0, got " + pageable.getPageNumber()); + } + if (pageable.getPageSize() != 25) { + throw new IllegalStateException( + "@PageableDefault size: expected 25, got " + pageable.getPageSize()); + } + return ResponseEntity.ok(Collections.emptyList()); + } + + // ── @SortDefault ───────────────────────────────────────────────────────── + // @SortDefault(sort = {"name"}, direction = DESC) + + @Override + public ResponseEntity> findPetsWithSortDefaultOnly(Pageable pageable) { + Sort.Order nameOrder = pageable.getSort().getOrderFor("name"); + if (nameOrder == null || nameOrder.getDirection() != Sort.Direction.DESC) { + throw new IllegalStateException( + "@SortDefault sort: expected name DESC, got " + pageable.getSort()); + } + return ResponseEntity.ok(Collections.emptyList()); + } + + // @SortDefault(sort = {"id"}, direction = ASC) + + @Override + public ResponseEntity> findPetsWithSortDefaultAsc(Pageable pageable) { + Sort.Order idOrder = pageable.getSort().getOrderFor("id"); + if (idOrder == null || idOrder.getDirection() != Sort.Direction.ASC) { + throw new IllegalStateException( + "@SortDefault sort: expected id ASC, got " + pageable.getSort()); + } + return ResponseEntity.ok(Collections.emptyList()); + } + + // ── @SortDefault.SortDefaults ───────────────────────────────────────────── + // @SortDefaults(SortDefault(sort = {"name"}, direction = DESC), SortDefault(sort = {"id"}, direction = ASC)) + + @Override + public ResponseEntity> findPetsWithMixedSortDefaults(Pageable pageable) { + Sort.Order nameOrder = pageable.getSort().getOrderFor("name"); + if (nameOrder == null || nameOrder.getDirection() != Sort.Direction.DESC) { + throw new IllegalStateException( + "@SortDefaults sort: expected name DESC, got " + pageable.getSort()); + } + Sort.Order idOrder = pageable.getSort().getOrderFor("id"); + if (idOrder == null || idOrder.getDirection() != Sort.Direction.ASC) { + throw new IllegalStateException( + "@SortDefaults sort: expected id ASC, got " + pageable.getSort()); + } + return ResponseEntity.ok(Collections.emptyList()); + } + + // ── @PageableDefault + @SortDefault.SortDefaults combined ───────────────── + // @PageableDefault(page = 0, size = 10) + // @SortDefaults(SortDefault(sort = {"name"}, direction = DESC), SortDefault(sort = {"id"}, direction = ASC)) + + @Override + public ResponseEntity> findPetsWithAllDefaults(Pageable pageable) { + if (pageable.getPageNumber() != 0) { + throw new IllegalStateException( + "@PageableDefault page: expected 0, got " + pageable.getPageNumber()); + } + if (pageable.getPageSize() != 10) { + throw new IllegalStateException( + "@PageableDefault size: expected 10, got " + pageable.getPageSize()); + } + Sort.Order nameOrder = pageable.getSort().getOrderFor("name"); + if (nameOrder == null || nameOrder.getDirection() != Sort.Direction.DESC) { + throw new IllegalStateException( + "@SortDefaults sort: expected name DESC, got " + pageable.getSort()); + } + Sort.Order idOrder = pageable.getSort().getOrderFor("id"); + if (idOrder == null || idOrder.getDirection() != Sort.Direction.ASC) { + throw new IllegalStateException( + "@SortDefaults sort: expected id ASC, got " + pageable.getSort()); + } + return ResponseEntity.ok(Collections.emptyList()); + } +} diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/DefaultExceptionHandler.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/DefaultExceptionHandler.java new file mode 100644 index 000000000000..22425d6b94b0 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/DefaultExceptionHandler.java @@ -0,0 +1,17 @@ +package org.openapitools.configuration; + +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class DefaultExceptionHandler { + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleConstraintViolation(ConstraintViolationException ex) { + return ex.getMessage(); + } +} diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java new file mode 100644 index 000000000000..5abc43a397e2 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/EnumConverterConfiguration.java @@ -0,0 +1,39 @@ +package org.openapitools.configuration; + +import org.openapitools.model.PetSort; +import org.openapitools.model.PetSortEnum; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; + +/** + * This class provides Spring Converter beans for the enum models in the OpenAPI specification. + * + * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent + * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than + * `original` or the specification has an integer enum. + */ +@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration") +public class EnumConverterConfiguration { + + @Bean(name = "org.openapitools.configuration.EnumConverterConfiguration.petSortConverter") + Converter petSortConverter() { + return new Converter() { + @Override + public PetSort convert(String source) { + return PetSort.fromValue(source); + } + }; + } + @Bean(name = "org.openapitools.configuration.EnumConverterConfiguration.petSortEnumConverter") + Converter petSortEnumConverter() { + return new Converter() { + @Override + public PetSortEnum convert(String source) { + return PetSortEnum.fromValue(source); + } + }; + } + +} diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/HomeController.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/HomeController.java new file mode 100644 index 000000000000..707313504790 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/HomeController.java @@ -0,0 +1,13 @@ +package org.openapitools.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Home redirection to OpenAPI api documentation + */ +@Controller +public class HomeController { + +} \ No newline at end of file diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidPageable.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidPageable.java new file mode 100644 index 000000000000..04b2ce26a5fc --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidPageable.java @@ -0,0 +1,99 @@ +package org.openapitools.configuration; + +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import org.springframework.data.domain.Pageable; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Validates that the page number and page size in the annotated {@link Pageable} parameter do not + * exceed their configured maximums. + * + *

Apply directly on a {@code Pageable} parameter. Each attribute is independently optional: + *

    + *
  • {@link #maxSize()} — when set (>= 0), validates {@code pageable.getPageSize() <= maxSize} + *
  • {@link #maxPage()} — when set (>= 0), validates {@code pageable.getPageNumber() <= maxPage} + *
+ * + *

Use {@link #NO_LIMIT} (= {@code -1}, the default) to leave an attribute unconstrained. + * + *

Constraining {@link #maxPage()} is useful to prevent deep-pagination attacks, where a large + * page offset (e.g. {@code ?page=100000&size=20}) causes an expensive {@code OFFSET} query on the + * database. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {ValidPageable.PageableConstraintValidator.class}) +@Target({ElementType.PARAMETER}) +public @interface ValidPageable { + + /** Sentinel value meaning no limit is applied. */ + int NO_LIMIT = -1; + + /** Maximum allowed page size, or {@link #NO_LIMIT} if unconstrained. */ + int maxSize() default NO_LIMIT; + + /** Maximum allowed page number (0-based), or {@link #NO_LIMIT} if unconstrained. */ + int maxPage() default NO_LIMIT; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String message() default "Invalid page request"; + + class PageableConstraintValidator implements ConstraintValidator { + + private int maxSize = NO_LIMIT; + private int maxPage = NO_LIMIT; + + @Override + public void initialize(ValidPageable constraintAnnotation) { + maxSize = constraintAnnotation.maxSize(); + maxPage = constraintAnnotation.maxPage(); + } + + @Override + public boolean isValid(Pageable pageable, ConstraintValidatorContext context) { + if (pageable == null) { + return true; + } + + if (!pageable.isPaged()) { + return true; + } + + boolean valid = true; + context.disableDefaultConstraintViolation(); + + if (maxSize >= 0 && pageable.getPageSize() > maxSize) { + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate() + + ": page size " + pageable.getPageSize() + + " exceeds maximum " + maxSize) + .addPropertyNode("size") + .addConstraintViolation(); + valid = false; + } + + if (maxPage >= 0 && pageable.getPageNumber() > maxPage) { + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate() + + ": page number " + pageable.getPageNumber() + + " exceeds maximum " + maxPage) + .addPropertyNode("page") + .addConstraintViolation(); + valid = false; + } + + return valid; + } + } +} diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidSort.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidSort.java new file mode 100644 index 000000000000..abbd2f533431 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/configuration/ValidSort.java @@ -0,0 +1,102 @@ +package org.openapitools.configuration; + +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import org.springframework.data.domain.Pageable; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Validates that sort properties in the annotated {@link Pageable} parameter match the allowed values. + * + *

Apply directly on a {@code Pageable} parameter. The validator checks that each sort + * property and direction combination in the {@link Pageable} matches one of the strings specified + * in {@link #allowedValues()}. + * + *

Two formats are accepted in {@link #allowedValues()}: + *

    + *
  • {@code "property,direction"} — permits only the specific direction (e.g. {@code "id,asc"}, + * {@code "name,desc"}). Direction matching is case-insensitive. + *
  • {@code "property"} — permits any direction for that property (e.g. {@code "id"} matches + * {@code sort=id,asc} and {@code sort=id,desc}). Note: because Spring always normalises a + * bare {@code sort=id} to ascending before the validator runs, bare property names in + * {@link #allowedValues()} effectively allow all directions. + *
+ * + *

Both formats may be mixed freely. For example {@code {"id", "name,desc"}} allows {@code id} + * in any direction but restricts {@code name} to descending only. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {ValidSort.SortValidator.class}) +@Target({ElementType.PARAMETER}) +public @interface ValidSort { + + /** The allowed sort strings (e.g. {@code {"id,asc", "id,desc"}}). */ + String[] allowedValues(); + + Class[] groups() default {}; + + Class[] payload() default {}; + + String message() default "Invalid sort column"; + + class SortValidator implements ConstraintValidator { + + private Set allowedValues; + + @Override + public void initialize(ValidSort constraintAnnotation) { + allowedValues = Arrays.stream(constraintAnnotation.allowedValues()) + .map(entry -> entry + .replaceAll("(?i),ASC$", ",asc") + .replaceAll("(?i),DESC$", ",desc")) + .collect(Collectors.toSet()); + } + + @Override + public boolean isValid(Pageable pageable, ConstraintValidatorContext context) { + if (pageable == null || pageable.getSort().isUnsorted()) { + return true; + } + + Map invalid = new TreeMap<>(); + int[] index = {0}; + pageable.getSort().forEach(order -> { + String sortValue = order.getProperty() + "," + order.getDirection().name().toLowerCase(java.util.Locale.ROOT); + // Accept "property,direction" (exact match) OR "property" alone (any direction allowed) + if (!allowedValues.contains(sortValue) && !allowedValues.contains(order.getProperty())) { + invalid.put(index[0], order.getProperty()); + } + index[0]++; + }); + + if (!invalid.isEmpty()) { + context.disableDefaultConstraintViolation(); + invalid.forEach((i, property) -> + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate() + " [" + property + "]") + .addPropertyNode("sort") + .addPropertyNode("property") + .inIterable() + .atIndex(i) + .addConstraintViolation()); + } + + return invalid.isEmpty(); + } + } +} diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/Pet.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/Pet.java new file mode 100644 index 000000000000..daf897833d9b --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/Pet.java @@ -0,0 +1,142 @@ +package org.openapitools.model; + +import java.net.URI; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import org.springframework.lang.Nullable; +import org.openapitools.jackson.nullable.JsonNullable; +import java.io.Serializable; +import java.time.OffsetDateTime; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; + + +import java.util.*; +import jakarta.annotation.Generated; + +/** + * Pet + */ + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT") +public class Pet implements Serializable { + + private static final long serialVersionUID = 1L; + + private @Nullable Long id; + + private String name; + + private @Nullable String status; + + public Pet() { + super(); + } + + /** + * Constructor with only required parameters + */ + public Pet(String name) { + this.name = name; + } + + public Pet id(@Nullable Long id) { + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + + @JsonProperty("id") + public @Nullable Long getId() { + return id; + } + + @JsonProperty("id") + public void setId(@Nullable Long id) { + this.id = id; + } + + public Pet name(String name) { + this.name = name; + return this; + } + + /** + * Get name + * @return name + */ + @NotNull + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + public Pet status(@Nullable String status) { + this.status = status; + return this; + } + + /** + * pet status in the store + * @return status + */ + + @JsonProperty("status") + public @Nullable String getStatus() { + return status; + } + + @JsonProperty("status") + public void setStatus(@Nullable String status) { + this.status = status; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pet pet = (Pet) o; + return Objects.equals(this.id, pet.id) && + Objects.equals(this.name, pet.name) && + Objects.equals(this.status, pet.status); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, status); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Pet {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(@Nullable Object o) { + return o == null ? "null" : o.toString().replace("\n", "\n "); + } +} + diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSort.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSort.java new file mode 100644 index 000000000000..818a4c9f1024 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSort.java @@ -0,0 +1,60 @@ +package org.openapitools.model; + +import java.net.URI; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonValue; +import org.openapitools.jackson.nullable.JsonNullable; +import java.io.Serializable; +import java.time.OffsetDateTime; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; + + +import java.util.*; +import jakarta.annotation.Generated; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Gets or Sets PetSort + */ + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT") +public enum PetSort implements Serializable { + + ID_ASC("id,asc"), + + ID_DESC("id,desc"), + + CREATED_AT_ASC("createdAt,asc"), + + CREATED_AT_DESC("createdAt,desc"); + + private final String value; + + PetSort(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static PetSort fromValue(String value) { + for (PetSort b : PetSort.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} + diff --git a/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSortEnum.java b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSortEnum.java new file mode 100644 index 000000000000..53a4aa2146e5 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/java/org/openapitools/model/PetSortEnum.java @@ -0,0 +1,60 @@ +package org.openapitools.model; + +import java.net.URI; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonValue; +import org.openapitools.jackson.nullable.JsonNullable; +import java.io.Serializable; +import java.time.OffsetDateTime; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; + + +import java.util.*; +import jakarta.annotation.Generated; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Gets or Sets PetSortEnum + */ + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.22.0-SNAPSHOT") +public enum PetSortEnum implements Serializable { + + NAME_ASC("name,asc"), + + NAME_DESC("name,desc"), + + ID_ASC("id,asc"), + + ID_DESC("id,desc"); + + private final String value; + + PetSortEnum(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static PetSortEnum fromValue(String value) { + for (PetSortEnum b : PetSortEnum.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} + diff --git a/samples/server/petstore/springboot-sort-validation/src/main/resources/application.properties b/samples/server/petstore/springboot-sort-validation/src/main/resources/application.properties new file mode 100644 index 000000000000..7e90813e59b2 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/resources/application.properties @@ -0,0 +1,3 @@ +server.port=8080 +spring.jackson.date-format=org.openapitools.RFC3339DateFormat +spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false diff --git a/samples/server/petstore/springboot-sort-validation/src/main/resources/openapi.yaml b/samples/server/petstore/springboot-sort-validation/src/main/resources/openapi.yaml new file mode 100644 index 000000000000..a656feda6c06 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/main/resources/openapi.yaml @@ -0,0 +1,816 @@ +openapi: 3.0.1 +info: + description: Test spec for generateSortValidation feature + title: OpenAPI Petstore - Sort Validation Test + version: 1.0.0 +servers: +- url: http://petstore.swagger.io/v2 +tags: +- description: Everything about your Pets + name: pet +paths: + /pet/findByStatusWithSort: + get: + operationId: findPetsWithSortEnum + parameters: + - description: Status filter + explode: true + in: query + name: status + required: false + schema: + type: string + style: form + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: Sort order + explode: true + in: query + name: sort + required: false + schema: + enum: + - "id,asc" + - "id,desc" + - "name,asc" + - "name,desc" + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with explicit x-spring-paginated and inline sort enum + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findAutoDetectedWithSort: + get: + operationId: findPetsAutoDetectedWithSort + parameters: + - description: Status filter + explode: true + in: query + name: status + required: false + schema: + type: string + style: form + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: Sort order + explode: true + in: query + name: sort + required: false + schema: + enum: + - "id,asc" + - "id,desc" + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with auto-detected pagination and sort enum + tags: + - pet + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithRefSort: + get: + operationId: findPetsWithRefSort + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: Sort order + explode: true + in: query + name: sort + required: false + schema: + $ref: "#/components/schemas/PetSort" + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with x-spring-paginated and $ref sort enum + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithoutSortEnum: + get: + operationId: findPetsWithoutSortEnum + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: Sort order (no enum constraint) + explode: true + in: query + name: sort + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with pagination but sort has no enum constraint + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findNonPaginatedWithSortEnum: + get: + operationId: findPetsNonPaginatedWithSortEnum + parameters: + - description: Sort order with enum but no pagination + explode: true + in: query + name: sort + required: false + schema: + enum: + - "id,asc" + - "id,desc" + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets without pagination but sort param has enum — no sort validation + expected + tags: + - pet + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithSortDefaultOnly: + get: + operationId: findPetsWithSortDefaultOnly + parameters: + - explode: true + in: query + name: page + required: false + schema: + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + type: integer + style: form + - explode: true + in: query + name: sort + required: false + schema: + default: "name,desc" + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: "Find pets — sort default only (single field DESC, no page/size defaults)" + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithSortDefaultAsc: + get: + operationId: findPetsWithSortDefaultAsc + parameters: + - explode: true + in: query + name: page + required: false + schema: + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + type: integer + style: form + - explode: true + in: query + name: sort + required: false + schema: + default: id + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: "Find pets — sort default only (single field, no explicit direction\ + \ defaults to ASC)" + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithMixedSortDefaults: + get: + operationId: findPetsWithMixedSortDefaults + parameters: + - explode: true + in: query + name: page + required: false + schema: + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + type: integer + style: form + - explode: true + in: query + name: sort + required: false + schema: + default: + - "name,desc" + - "id,asc" + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets — multiple sort defaults with mixed directions (array sort + param) + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithPageSizeDefaultsOnly: + get: + operationId: findPetsWithPageSizeDefaultsOnly + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 25 + type: integer + style: form + - explode: true + in: query + name: sort + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: "Find pets — page and size defaults only, no sort default" + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithAllDefaults: + get: + operationId: findPetsWithAllDefaults + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 10 + type: integer + style: form + - explode: true + in: query + name: sort + required: false + schema: + default: + - "name,desc" + - "id,asc" + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: "Find pets — page, size, and mixed sort defaults all present" + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithSizeConstraint: + get: + operationId: findPetsWithSizeConstraint + parameters: + - explode: true + in: query + name: page + required: false + schema: + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + maximum: 100 + type: integer + style: form + - explode: true + in: query + name: sort + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets — size has maximum constraint only + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithPageAndSizeConstraint: + get: + operationId: findPetsWithPageAndSizeConstraint + parameters: + - explode: true + in: query + name: page + required: false + schema: + maximum: 999 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + maximum: 50 + type: integer + style: form + - explode: true + in: query + name: sort + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets — both page and size have maximum constraints + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithArraySortEnum: + get: + operationId: findPetsWithArraySortEnum + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: "Sort order (multi-column, each element must be an allowed value)" + explode: true + in: query + name: sort + required: false + schema: + items: + enum: + - "id,asc" + - "id,desc" + - "name,asc" + - "name,desc" + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with x-spring-paginated and array sort param with inline + enum on items + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithArraySortRefEnum: + get: + operationId: findPetsWithArraySortRefEnum + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: "Sort order (multi-column, items $ref to PetSort enum)" + explode: true + in: query + name: sort + required: false + schema: + items: + $ref: "#/components/schemas/PetSort" + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with x-spring-paginated and array sort param whose items + use a $ref enum + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithExternalParamRefArraySort: + get: + operationId: findPetsWithExternalParamRefArraySort + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: "Sort order — multi-column, each value must be one of the allowed\ + \ enum values" + in: query + name: sort + required: false + schema: + default: [] + items: + $ref: "#/components/schemas/PetSortEnum" + type: array + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with x-spring-paginated and sort param referenced from an + external components file + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet + /pet/findWithNonExplodedExternalParamRefArraySort: + get: + operationId: findPetsWithNonExplodedExternalParamRefArraySort + parameters: + - explode: true + in: query + name: page + required: false + schema: + default: 0 + type: integer + style: form + - explode: true + in: query + name: size + required: false + schema: + default: 20 + type: integer + style: form + - description: "Sort order — non-exploded multi-column (e.g. sort=id,asc,name,desc),\ + \ each token pair must match the allowed enum values" + explode: false + in: query + name: sort + required: false + schema: + items: + $ref: "#/components/schemas/PetSortEnum" + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + summary: Find pets with x-spring-paginated and non-exploded sort param referenced + from an external components file + tags: + - pet + x-spring-paginated: true + x-accepts: + - application/json + x-tags: + - tag: pet +components: + parameters: + PetSortParam: + description: "Sort order — multi-column, each value must be one of the allowed\ + \ enum values" + in: query + name: sort + required: false + schema: + default: [] + items: + $ref: "#/components/schemas/PetSortEnum" + type: array + PetSortParamNonExploded: + description: "Sort order — non-exploded multi-column (e.g. sort=id,asc,name,desc),\ + \ each token pair must match the allowed enum values" + explode: false + in: query + name: sort + required: false + schema: + items: + $ref: "#/components/schemas/PetSortEnum" + type: array + style: form + schemas: + PetSort: + enum: + - "id,asc" + - "id,desc" + - "createdAt,asc" + - "createdAt,desc" + type: string + Pet: + example: + name: name + id: 0 + status: status + properties: + id: + format: int64 + type: integer + name: + type: string + status: + description: pet status in the store + type: string + required: + - name + type: object + PetSortEnum: + enum: + - "name,asc" + - "name,desc" + - "id,asc" + - "id,desc" + type: string diff --git a/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java new file mode 100644 index 000000000000..3681f67e7705 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java @@ -0,0 +1,13 @@ +package org.openapitools; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class OpenApiGeneratorApplicationTests { + + @Test + void contextLoads() { + } + +} \ No newline at end of file diff --git a/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/api/PetApiValidationTest.java b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/api/PetApiValidationTest.java new file mode 100644 index 000000000000..7e3bceea5072 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/test/java/org/openapitools/api/PetApiValidationTest.java @@ -0,0 +1,181 @@ +package org.openapitools.api; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Verifies the runtime behaviour of the annotations generated onto {@link PetApi}: + * + *

    + *
  • {@code @ValidSort} — invalid sort field/direction combinations are rejected with 400.
  • + *
  • {@code @ValidPageable} — page number or size exceeding the configured limit is rejected with 400.
  • + *
  • {@code @PageableDefault} — when page/size query params are absent, the configured defaults are + * forwarded to the controller method (verified by assertions inside {@link PetApiController}).
  • + *
  • {@code @SortDefault} / {@code @SortDefaults} — when the sort query param is absent, the configured + * default sort order is forwarded to the controller method (verified inside {@link PetApiController}).
  • + *
+ * + * HTTP 200 responses confirm both that the request was accepted and that {@link PetApiController}'s + * internal assertions about the received defaults passed. + * HTTP 400 responses confirm that the constraint annotation rejected the invalid input. + */ +@SpringBootTest +@AutoConfigureMockMvc +class PetApiValidationTest { + + @Autowired + MockMvc mockMvc; + + // ── @ValidSort ──────────────────────────────────────────────────────────── + // Endpoint: GET /pet/findWithArraySortEnum allowed: id,asc | id,desc | name,asc | name,desc + + @Test + void validSort_validSortValueReturns200() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM) + .param("sort", "id,asc")) + .andExpect(status().isOk()); + } + + @Test + void validSort_multipleValidSortValuesReturn200() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM) + .param("sort", "id,desc") + .param("sort", "name,asc")) + .andExpect(status().isOk()); + } + + @Test + void validSort_invalidSortPropertyReturns400() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM) + .param("sort", "unknown,asc")) + .andExpect(status().isBadRequest()); + } + + @Test + void validSort_invalidSortDirectionReturns400() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM) + .param("sort", "id,random")) + .andExpect(status().isBadRequest()); + } + + @Test + void validSort_oneInvalidSortAmongMultipleValidValuesReturns400() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ARRAY_SORT_ENUM) + .param("sort", "id,asc") + .param("sort", "unknown,desc")) + .andExpect(status().isBadRequest()); + } + + // ── @ValidPageable — size constraint only ───────────────────────────────── + // Endpoint: GET /pet/findWithSizeConstraint maxSize = 100 + + @Test + void validPageable_sizeBelowMaximumReturns200() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT) + .param("size", "50")) + .andExpect(status().isOk()); + } + + @Test + void validPageable_sizeAtMaximumReturns200() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT) + .param("size", "100")) + .andExpect(status().isOk()); + } + + @Test + void validPageable_sizeExceedsMaximumReturns400() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT) + .param("size", "101")) + .andExpect(status().isBadRequest()); + } + + @Test + void validPageable_unpagedPageableIsAllowed() throws Exception { + // When no pagination parameters are supplied and no @PageableDefault is configured, + // Spring resolves Pageable.unpaged(). The validator must not throw and must return valid. + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SIZE_CONSTRAINT)) + .andExpect(status().isOk()); + } + + // ── @ValidPageable — size and page constraints combined ─────────────────── + // Endpoint: GET /pet/findWithPageAndSizeConstraint maxSize = 50, maxPage = 999 + + @Test + void validPageable_sizeAndPageAtTheirMaximumsReturn200() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT) + .param("size", "50") + .param("page", "999")) + .andExpect(status().isOk()); + } + + @Test + void validPageable_sizeExceedsMaximumForCombinedConstraintReturns400() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT) + .param("size", "51")) + .andExpect(status().isBadRequest()); + } + + @Test + void validPageable_pageExceedsMaximumReturns400() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_AND_SIZE_CONSTRAINT) + .param("page", "1000")) + .andExpect(status().isBadRequest()); + } + + // ── @PageableDefault ───────────────────────────────────────────────────── + // Endpoint: GET /pet/findWithPageSizeDefaultsOnly @PageableDefault(page = 0, size = 25) + // PetApiController asserts page == 0 and size == 25; returns 200 on success, throws on mismatch. + + @Test + void pageableDefault_absentParamsResolveToConfiguredPageAndSizeDefaults() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_PAGE_SIZE_DEFAULTS_ONLY)) + .andExpect(status().isOk()); + } + + // ── @SortDefault ───────────────────────────────────────────────────────── + // Endpoint: GET /pet/findWithSortDefaultOnly @SortDefault(sort = {"name"}, direction = DESC) + // PetApiController asserts name DESC; returns 200 on success, throws on mismatch. + + @Test + void sortDefault_absentSortParamResolvesToNameDescDefault() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ONLY)) + .andExpect(status().isOk()); + } + + // Endpoint: GET /pet/findWithSortDefaultAsc @SortDefault(sort = {"id"}, direction = ASC) + + @Test + void sortDefault_absentSortParamResolvesToIdAscDefault() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_SORT_DEFAULT_ASC)) + .andExpect(status().isOk()); + } + + // ── @SortDefault.SortDefaults ───────────────────────────────────────────── + // Endpoint: GET /pet/findWithMixedSortDefaults + // @SortDefaults(SortDefault({"name"}, DESC), SortDefault({"id"}, ASC)) + // PetApiController asserts both orders; returns 200 on success, throws on mismatch. + + @Test + void sortDefaults_absentSortParamResolvesAllConfiguredSortDefaults() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_MIXED_SORT_DEFAULTS)) + .andExpect(status().isOk()); + } + + // ── @PageableDefault + @SortDefault.SortDefaults combined ───────────────── + // Endpoint: GET /pet/findWithAllDefaults + // @PageableDefault(page = 0, size = 10) + @SortDefaults(name DESC, id ASC) + // PetApiController asserts page, size, and both sort orders. + + @Test + void pageableDefaultAndSortDefaults_absentParamsResolveAllDefaults() throws Exception { + mockMvc.perform(get(PetApi.PATH_FIND_PETS_WITH_ALL_DEFAULTS)) + .andExpect(status().isOk()); + } +} diff --git a/samples/server/petstore/springboot-sort-validation/src/test/resources/application.properties b/samples/server/petstore/springboot-sort-validation/src/test/resources/application.properties new file mode 100644 index 000000000000..32b68ede4a53 --- /dev/null +++ b/samples/server/petstore/springboot-sort-validation/src/test/resources/application.properties @@ -0,0 +1 @@ +openapi.openAPIPetstoreSortValidationTest.base-path= diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java index 52dec7c6802c..7164868c12ea 100644 --- a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApi.java @@ -8,8 +8,11 @@ import org.openapitools.model.ModelApiResponse; import org.springframework.lang.Nullable; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springdoc.core.annotations.ParameterObject; import org.openapitools.model.Pet; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -182,7 +185,7 @@ default ResponseEntity> findPetsByStatus( default ResponseEntity> findPetsByTags( @NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags, @Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size, - @ParameterObject final Pageable pageable + @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable ) { return getDelegate().findPetsByTags(tags, size, pageable); } diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java index ab9374eeb0ed..fc1b51ec13fc 100644 --- a/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java +++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern-without-j8/src/main/java/org/openapitools/api/PetApiDelegate.java @@ -3,8 +3,11 @@ import org.openapitools.model.ModelApiResponse; import org.springframework.lang.Nullable; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springdoc.core.annotations.ParameterObject; import org.openapitools.model.Pet; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java index 52dec7c6802c..7164868c12ea 100644 --- a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApi.java @@ -8,8 +8,11 @@ import org.openapitools.model.ModelApiResponse; import org.springframework.lang.Nullable; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springdoc.core.annotations.ParameterObject; import org.openapitools.model.Pet; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -182,7 +185,7 @@ default ResponseEntity> findPetsByStatus( default ResponseEntity> findPetsByTags( @NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags, @Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size, - @ParameterObject final Pageable pageable + @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable ) { return getDelegate().findPetsByTags(tags, size, pageable); } diff --git a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java index ab9374eeb0ed..fc1b51ec13fc 100644 --- a/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java +++ b/samples/server/petstore/springboot-spring-pageable-delegatePattern/src/main/java/org/openapitools/api/PetApiDelegate.java @@ -3,8 +3,11 @@ import org.openapitools.model.ModelApiResponse; import org.springframework.lang.Nullable; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springdoc.core.annotations.ParameterObject; import org.openapitools.model.Pet; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; diff --git a/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java index 953ff2a55663..65ca60f6970b 100644 --- a/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-spring-pageable-without-j8/src/main/java/org/openapitools/api/PetApi.java @@ -8,8 +8,11 @@ import org.openapitools.model.ModelApiResponse; import org.springframework.lang.Nullable; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springdoc.core.annotations.ParameterObject; import org.openapitools.model.Pet; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -203,7 +206,7 @@ default ResponseEntity> findPetsByStatus( default ResponseEntity> findPetsByTags( @NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags, @Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size, - @ParameterObject final Pageable pageable + @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { diff --git a/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java index 953ff2a55663..65ca60f6970b 100644 --- a/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-spring-pageable/src/main/java/org/openapitools/api/PetApi.java @@ -8,8 +8,11 @@ import org.openapitools.model.ModelApiResponse; import org.springframework.lang.Nullable; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springdoc.core.annotations.ParameterObject; import org.openapitools.model.Pet; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -203,7 +206,7 @@ default ResponseEntity> findPetsByStatus( default ResponseEntity> findPetsByTags( @NotNull @Parameter(name = "tags", description = "Tags to filter by", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "tags", required = true) List tags, @Parameter(name = "size", description = "A test HeaderParam for issue #8315 - must NOT be removed when x-spring-paginated:true is used.", in = ParameterIn.HEADER) @RequestHeader(value = "size", required = false) @Nullable String size, - @ParameterObject final Pageable pageable + @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({@SortDefault(sort = {"id"}, direction = Sort.Direction.ASC)}) @ParameterObject final Pageable pageable ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {