diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 24ba78294eb3..613823e32500 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -95,6 +95,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src/main/java| +|springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null| |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |title|server title name or client service name| |OpenAPI Spring| |unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false| @@ -132,6 +133,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-spring-paginated|Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object.|OPERATION|false |x-version-param|Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false|OPERATION_PARAMETER|null |x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null +|x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null ## IMPORT MAPPING diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 19c889f90916..050a7814b9ee 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -88,6 +88,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src/main/java| +|springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null| |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |title|server title name or client service name| |OpenAPI Spring| |unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false| @@ -125,6 +126,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-spring-paginated|Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object.|OPERATION|false |x-version-param|Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false|OPERATION_PARAMETER|null |x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null +|x-spring-api-version|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).|OPERATION|null ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java index 70aa9ce222b1..eed312856344 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java @@ -13,6 +13,7 @@ public enum VendorExtension { X_KOTLIN_IMPLEMENTS("x-kotlin-implements", ExtensionLevel.MODEL, "Ability to specify interfaces that model must implement", "empty array"), X_KOTLIN_IMPLEMENTS_FIELDS("x-kotlin-implements-fields", ExtensionLevel.MODEL, "Specify attributes that are implemented by the interface(s) added via `x-kotlin-implements`", "empty array"), X_SPRING_PAGINATED("x-spring-paginated", ExtensionLevel.OPERATION, "Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object.", "false"), + X_SPRING_API_VERSION("x-spring-api-version", ExtensionLevel.OPERATION, "Value for 'version' attribute in @RequestMapping (for Spring 7 and above).", null), X_SPRING_PROVIDE_ARGS("x-spring-provide-args", ExtensionLevel.OPERATION, "Allows adding additional hidden parameters in the API specification to allow access to content such as header values or properties", "empty array"), X_DISCRIMINATOR_VALUE("x-discriminator-value", ExtensionLevel.MODEL, "Used with model inheritance to specify value for discriminator that identifies current model", ""), X_SETTER_EXTRA_ANNOTATION("x-setter-extra-annotation", ExtensionLevel.FIELD, "Custom annotation that can be specified over java setter for specific field", "When field is array & uniqueItems, then this extension is used to add `@JsonDeserialize(as = LinkedHashSet.class)` over setter, otherwise no value"), 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 43712e054eab..5c6053474fcf 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 @@ -100,6 +100,7 @@ public class SpringCodegen extends AbstractJavaCodegen public static final String OPTIONAL_ACCEPT_NULLABLE = "optionalAcceptNullable"; public static final String USE_SPRING_BUILT_IN_VALIDATION = "useSpringBuiltInValidation"; public static final String USE_DEDUCTION_FOR_ONE_OF_INTERFACES = "useDeductionForOneOfInterfaces"; + public static final String SPRING_API_VERSION = "springApiVersion"; @Getter public enum RequestMappingMode { @@ -286,6 +287,7 @@ public SpringCodegen() { optionalAcceptNullable)); cliOptions.add(CliOption.newBoolean(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, "whether to use deduction for generated oneOf interfaces", useDeductionForOneOfInterfaces)); + cliOptions.add(CliOption.newString(SPRING_API_VERSION, "Value for 'version' attribute in @RequestMapping (for Spring 7 and above).")); supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application."); supportedLibraries.put(SPRING_CLOUD_LIBRARY, "Spring-Cloud-Feign client with Spring-Boot auto-configured settings."); @@ -855,6 +857,8 @@ private void doDataTypeAssignment(String returnType, DataTypeAssigner dataTypeAs } private void prepareVersioningParameters(List operations) { + Object apiVersion = additionalProperties.get(SPRING_API_VERSION); + boolean hasApiVersion = apiVersion != null; for (CodegenOperation operation : operations) { if (operation.getHasHeaderParams()) { List versionParams = operation.headerParams.stream() @@ -877,6 +881,9 @@ private void prepareVersioningParameters(List operations) { operation.hasVersionQueryParams = !versionParams.isEmpty(); operation.vendorExtensions.put("versionQueryParamsList", versionParams); } + if (hasApiVersion) { + operation.vendorExtensions.putIfAbsent(VendorExtension.X_SPRING_API_VERSION.getName(), apiVersion); + } } } @@ -1205,6 +1212,7 @@ public List getSupportedVendorExtensions() { extensions.add(VendorExtension.X_SPRING_PAGINATED); extensions.add(VendorExtension.X_VERSION_PARAM); extensions.add(VendorExtension.X_PATTERN_MESSAGE); + extensions.add(VendorExtension.X_SPRING_API_VERSION); return extensions; } } diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache index ef8c4f25840b..c3beb18bbde5 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache @@ -252,7 +252,8 @@ public interface {{classname}} { produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}}, consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}}{{#hasVersionHeaders}}, headers = { {{#vendorExtensions.versionHeaderParamsList}}"{{baseName}}{{#defaultValue}}={{{.}}}{{/defaultValue}}"{{^-last}}, {{/-last}}{{/vendorExtensions.versionHeaderParamsList}} } {{/hasVersionHeaders}}{{#hasVersionQueryParams}}, - params = { {{#vendorExtensions.versionQueryParamsList}}"{{baseName}}{{#defaultValue}}={{{.}}}{{/defaultValue}}"{{^-last}}, {{/-last}}{{/vendorExtensions.versionQueryParamsList}} } {{/hasVersionQueryParams}} + params = { {{#vendorExtensions.versionQueryParamsList}}"{{baseName}}{{#defaultValue}}={{{.}}}{{/defaultValue}}"{{^-last}}, {{/-last}}{{/vendorExtensions.versionQueryParamsList}} } {{/hasVersionQueryParams}}{{#vendorExtensions.x-spring-api-version}}{{^empty}}, + version = "{{{vendorExtensions.x-spring-api-version}}}"{{/empty}}{{/vendorExtensions.x-spring-api-version}} ) {{^useResponseEntity}} @ResponseStatus({{#springHttpStatus}}{{#responses.0}}{{{code}}}{{/responses.0}}{{/springHttpStatus}}) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java index fad840acea97..fef36947d8d1 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java @@ -50,23 +50,40 @@ public ACTUAL containsWithNameAndAttributes(final String name, final Map attributes) { + super + .withFailMessage("Should have annotation with name: " + name + " and no attributes: " + attributes + ", but was: " + actual) + .anyMatch(annotation -> annotation.getNameAsString().equals(name) && hasNotAttributes(annotation, attributes)); + return myself(); + } + + private static boolean hasNotAttributes(final AnnotationExpr annotation, final List attributes) { + final Map actualAttributes = getAttributes(annotation); + + return actualAttributes.keySet().stream() + .noneMatch(attribute -> attributes.contains(attribute)); + } + private static boolean hasAttributes(final AnnotationExpr annotation, final Map expectedAttributesToContains) { - final Map actualAttributes; + final Map actualAttributes = getAttributes(annotation); + + return expectedAttributesToContains.entrySet().stream() + .allMatch(expected -> Objects.equals(actualAttributes.get(expected.getKey()), expected.getValue())); + } + + private static Map getAttributes(final AnnotationExpr annotation) { if (annotation instanceof SingleMemberAnnotationExpr) { - actualAttributes = ImmutableMap.of( + return ImmutableMap.of( "value", ((SingleMemberAnnotationExpr) annotation).getMemberValue().toString() ); } else if (annotation instanceof NormalAnnotationExpr) { - actualAttributes = ((NormalAnnotationExpr) annotation).getPairs().stream() + return ((NormalAnnotationExpr) annotation).getPairs().stream() .collect(Collectors.toMap(NodeWithSimpleName::getNameAsString, pair -> pair.getValue().toString())); } else if (annotation instanceof MarkerAnnotationExpr) { - actualAttributes = new HashMap<>(); + return new HashMap<>(); } else { throw new IllegalArgumentException("Unexpected annotation expression type for: " + annotation); } - - return expectedAttributesToContains.entrySet().stream() - .allMatch(expected -> Objects.equals(actualAttributes.get(expected.getKey()), expected.getValue())); } @SuppressWarnings("unchecked") 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 e85833a6caf0..a45c7a8e97e3 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 @@ -5738,4 +5738,25 @@ public void testOneOfInterfaceWithAnnotation() throws IOException { .isInterface() .assertTypeAnnotations().containsWithName("SuppressWarnings"); } + + @Test + public void testApiVersion() throws IOException { + final Map files = generateFromContract("src/test/resources/3_0/spring/apiVersion.yaml", SPRING_BOOT, + Map.of(SpringCodegen.SPRING_API_VERSION, "v1", + USE_TAGS, true)); + JavaFileAssert.assertThat(files.get("TestApi.java")) + .assertMethod("getVersions") + .assertMethodAnnotations() + .containsWithNameAndAttributes("RequestMapping", Map.of("version", "\"v1\"")) + .toMethod().toFileAssert() + + .assertMethod("getOverrides") + .assertMethodAnnotations() + .containsWithNameAndAttributes("RequestMapping", Map.of("version", "\"2+\"")) + .toMethod().toFileAssert() + + .assertMethod("getNones") + .assertMethodAnnotations() + .containsWithNameAndDoesContainAttributes("RequestMapping", List.of("version")); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/apiVersion.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/apiVersion.yaml new file mode 100644 index 000000000000..1d8e9aa1c7fb --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/apiVersion.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: x-spring-api-version test + version: 1.0.0 +paths: + /versions: + get: + tags: + - Test + operationId: getVersions + /overrides: + get: + tags: + - Test + operationId: getOverrides + x-spring-api-version: '2+' + /nones: + get: + tags: + - Test + operationId: getNones + x-spring-api-version: '' +