diff --git a/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocDescription.java b/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocDescription.java index 5c5f94f72a..b875d0afad 100644 --- a/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocDescription.java +++ b/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocDescription.java @@ -31,6 +31,7 @@ public class JavadocDescription { private String methodSummary; private String methodDescription; private String returnDescription; + private String deprecatedDescription; private final Map parameters = new HashMap<>(4); /** @@ -91,4 +92,23 @@ public String getReturnDescription() { public void setReturnDescription(String returnDescription) { this.returnDescription = returnDescription; } + + /** + * The deprecated description. + * + * @return The deprecated description + */ + @Nullable + public String getDeprecatedDescription() { + return deprecatedDescription; + } + + /** + * Sets the deprecated description. + * + * @param deprecatedDescription The deprecated description. + */ + public void setDeprecatedDescription(String deprecatedDescription) { + this.deprecatedDescription = deprecatedDescription; + } } diff --git a/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocParser.java b/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocParser.java index d92636c554..44f05feb3a 100644 --- a/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocParser.java +++ b/openapi/src/main/java/io/micronaut/openapi/javadoc/JavadocParser.java @@ -18,6 +18,7 @@ import com.github.chhorz.javadoc.JavaDoc; import com.github.chhorz.javadoc.JavaDocParserBuilder; import com.github.chhorz.javadoc.OutputType; +import com.github.chhorz.javadoc.tags.DeprecatedTag; import com.github.chhorz.javadoc.tags.ParamTag; import com.github.chhorz.javadoc.tags.PropertyTag; import com.github.chhorz.javadoc.tags.ReturnTag; @@ -35,7 +36,7 @@ */ public class JavadocParser { - private static final Set IGNORED = CollectionUtils.setOf("see", "since", "author", "version", "deprecated", "throws", "exception", "category"); + private static final Set IGNORED = CollectionUtils.setOf("see", "since", "author", "version", "throws", "exception", "category"); private final FlexmarkHtmlConverter htmlToMarkdownConverter = FlexmarkHtmlConverter.builder() .build(); @@ -77,6 +78,8 @@ public JavadocDescription parse(String text) { } else if (tag instanceof PropertyTag propertyTag) { String paramDesc = htmlToMarkdownConverter.convert(propertyTag.getParamDescription()).strip(); javadocDescription.getParameters().put(propertyTag.getPropertyName(), paramDesc); + } else if (tag instanceof DeprecatedTag deprecatedTag) { + javadocDescription.setDeprecatedDescription(htmlToMarkdownConverter.convert(deprecatedTag.getDeprecatedText()).strip()); } } } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java index 97aecf029e..5829a2234c 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java @@ -128,12 +128,15 @@ import static io.micronaut.openapi.visitor.ContextUtils.warn; import static io.micronaut.openapi.visitor.ConvertUtils.MAP_TYPE; import static io.micronaut.openapi.visitor.ElementUtils.getJsonViewClass; +import static io.micronaut.openapi.visitor.ElementUtils.isDeprecated; import static io.micronaut.openapi.visitor.ElementUtils.isFileUpload; import static io.micronaut.openapi.visitor.ElementUtils.isIgnoredParameter; import static io.micronaut.openapi.visitor.ElementUtils.isNotNullable; import static io.micronaut.openapi.visitor.ElementUtils.isNullable; import static io.micronaut.openapi.visitor.ElementUtils.isResponseType; import static io.micronaut.openapi.visitor.ElementUtils.isSingleResponseType; +import static io.micronaut.openapi.visitor.GeneratorUtils.addOperationDeprecatedExtension; +import static io.micronaut.openapi.visitor.GeneratorUtils.addParameterDeprecatedExtension; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ADD_ALWAYS; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ALLOW_EMPTY_VALUE; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ALLOW_RESERVED; @@ -467,7 +470,7 @@ public void visitMethod(MethodElement element, VisitorContext context) { javadocDescription = getMethodDescription(element, swaggerOperation); - if (element.isAnnotationPresent(Deprecated.class)) { + if (isDeprecated(element)) { swaggerOperation.setDeprecated(true); } @@ -838,7 +841,7 @@ private void processParameter(VisitorContext context, OpenAPI openAPI, io.swagger.v3.oas.models.media.MediaType mediaType = entry.getValue(); - Schema propertySchema = bindSchemaForElement(context, parameter, parameterType, mediaType.getSchema(), null); + Schema propertySchema = bindSchemaForElement(context, parameter, parameterType, mediaType.getSchema(), null, false); var bodyAnn = parameter.getAnnotation(Body.class); @@ -918,7 +921,7 @@ private void processParameter(VisitorContext context, OpenAPI openAPI, } if (schema != null) { - schema = bindSchemaForElement(context, parameter, parameterType, schema, null); + schema = bindSchemaForElement(context, parameter, parameterType, schema, null, false); newParameter.setSchema(schema); } } @@ -943,20 +946,21 @@ private void processBodyParameter(VisitorContext context, OpenAPI openAPI, Javad var jsonViewClass = getJsonViewClass(parameter, context); Schema propertySchema = resolveSchema(openAPI, parameter, parameter.getType(), context, Collections.singletonList(mediaType), jsonViewClass, null, null); - if (propertySchema != null) { - - parameter.stringValue(io.swagger.v3.oas.annotations.Parameter.class, PROP_DESCRIPTION) - .ifPresent(propertySchema::setDescription); - processSchemaProperty(context, parameter, parameter.getType(), null, schema, propertySchema); - if (isNullable(parameter) && !isNotNullable(parameter)) { - // Keep null if not - SchemaUtils.setNullable(propertySchema); - } - if (javadocDescription != null && StringUtils.isEmpty(propertySchema.getDescription())) { - String doc = javadocDescription.getParameters().get(parameter.getName()); - if (doc != null) { - propertySchema.setDescription(doc); - } + if (propertySchema == null) { + return; + } + + parameter.stringValue(io.swagger.v3.oas.annotations.Parameter.class, PROP_DESCRIPTION) + .ifPresent(propertySchema::setDescription); + processSchemaProperty(context, parameter, parameter.getType(), null, schema, propertySchema); + if (isNullable(parameter) && !isNotNullable(parameter)) { + // Keep null if not + SchemaUtils.setNullable(propertySchema); + } + if (javadocDescription != null && StringUtils.isEmpty(propertySchema.getDescription())) { + String doc = javadocDescription.getParameters().get(parameter.getName()); + if (doc != null) { + propertySchema.setDescription(doc); } } } @@ -1163,6 +1167,11 @@ private Parameter processMethodParameterAnnotation(VisitorContext context, Opera newParameter.setRequired(null); } + if (newParameter != null && isDeprecated(parameter)) { + newParameter.setDeprecated(true); + addParameterDeprecatedExtension(parameter, newParameter, context); + } + return newParameter; } @@ -1393,6 +1402,8 @@ private Map readOperations(String path, HttpMethod httpMeth swaggerOperation = new Operation(); } + addOperationDeprecatedExtension(element, swaggerOperation, context); + if (CollectionUtils.isNotEmpty(swaggerOperation.getParameters())) { swaggerOperation.getParameters().removeIf(Objects::isNull); } @@ -1439,10 +1450,6 @@ private Map readOperations(String path, HttpMethod httpMeth if (required) { swaggerParam.setRequired(true); } - var deprecated = paramAnn.booleanValue(PROP_DEPRECATED).orElse(false); - if (deprecated) { - swaggerParam.setDeprecated(true); - } var allowEmptyValue = paramAnn.booleanValue(PROP_ALLOW_EMPTY_VALUE).orElse(false); if (allowEmptyValue) { swaggerParam.setAllowEmptyValue(true); @@ -1723,59 +1730,60 @@ private void processResponses(Operation operation, apiResponses = new ApiResponses(); operation.setResponses(apiResponses); } - if (CollectionUtils.isNotEmpty(responseAnns)) { - for (var responseAnn : responseAnns) { - String responseCode = responseAnn.stringValue(PROP_RESPONSE_CODE).orElse("default"); - if (apiResponses.containsKey(responseCode)) { - continue; - } - ApiResponse newApiResponse = toValue(responseAnn.getValues(), context, ApiResponse.class, jsonViewClass); - if (newApiResponse != null) { - if (responseAnn.booleanValue("useReturnTypeSchema").orElse(false) && element != null) { - addResponseContent(element, context, Utils.resolveOpenApi(context), newApiResponse, jsonViewClass); - } else { + if (CollectionUtils.isEmpty(responseAnns)) { + return; + } + for (var responseAnn : responseAnns) { + String responseCode = responseAnn.stringValue(PROP_RESPONSE_CODE).orElse("default"); + if (apiResponses.containsKey(responseCode)) { + continue; + } + ApiResponse newApiResponse = toValue(responseAnn.getValues(), context, ApiResponse.class, jsonViewClass); + if (newApiResponse != null) { + if (responseAnn.booleanValue("useReturnTypeSchema").orElse(false) && element != null) { + addResponseContent(element, context, Utils.resolveOpenApi(context), newApiResponse, jsonViewClass); + } else { - List producesMediaTypes = producesMediaTypes(element); + List producesMediaTypes = producesMediaTypes(element); - var contentAnns = responseAnn.get(PROP_CONTENT, io.swagger.v3.oas.annotations.media.Content[].class).orElse(null); - var mediaTypes = new ArrayList(); - if (ArrayUtils.isNotEmpty(contentAnns)) { - for (io.swagger.v3.oas.annotations.media.Content contentAnn : contentAnns) { - if (StringUtils.isNotEmpty(contentAnn.mediaType())) { - mediaTypes.add(contentAnn.mediaType()); - } else { - mediaTypes.add(MediaType.APPLICATION_JSON); - } + var contentAnns = responseAnn.get(PROP_CONTENT, io.swagger.v3.oas.annotations.media.Content[].class).orElse(null); + var mediaTypes = new ArrayList(); + if (ArrayUtils.isNotEmpty(contentAnns)) { + for (io.swagger.v3.oas.annotations.media.Content contentAnn : contentAnns) { + if (StringUtils.isNotEmpty(contentAnn.mediaType())) { + mediaTypes.add(contentAnn.mediaType()); + } else { + mediaTypes.add(MediaType.APPLICATION_JSON); } } - Content newContent = newApiResponse.getContent(); - if (newContent != null) { - io.swagger.v3.oas.models.media.MediaType defaultMediaType = newContent.get(MediaType.APPLICATION_JSON); - var contentFromProduces = new Content(); - for (String mt : mediaTypes) { - if (mt.equals(MediaType.APPLICATION_JSON)) { - for (MediaType mediaType : producesMediaTypes) { - contentFromProduces.put(mediaType.toString(), defaultMediaType); - } - } else { - contentFromProduces.put(mt, newContent.get(mt)); + } + Content newContent = newApiResponse.getContent(); + if (newContent != null) { + io.swagger.v3.oas.models.media.MediaType defaultMediaType = newContent.get(MediaType.APPLICATION_JSON); + var contentFromProduces = new Content(); + for (String mt : mediaTypes) { + if (mt.equals(MediaType.APPLICATION_JSON)) { + for (MediaType mediaType : producesMediaTypes) { + contentFromProduces.put(mediaType.toString(), defaultMediaType); } + } else { + contentFromProduces.put(mt, newContent.get(mt)); } - newApiResponse.setContent(contentFromProduces); } + newApiResponse.setContent(contentFromProduces); } - try { - if (StringUtils.isEmpty(newApiResponse.getDescription())) { - newApiResponse.setDescription(responseCode.equals(PROP_DEFAULT) ? "OK response" : HttpStatus.getDefaultReason(Integer.parseInt(responseCode))); - } - } catch (Exception e) { - newApiResponse.setDescription("Response " + responseCode); + } + try { + if (StringUtils.isEmpty(newApiResponse.getDescription())) { + newApiResponse.setDescription(responseCode.equals(PROP_DEFAULT) ? "OK response" : HttpStatus.getDefaultReason(Integer.parseInt(responseCode))); } - apiResponses.put(responseCode, newApiResponse); + } catch (Exception e) { + newApiResponse.setDescription("Response " + responseCode); } + apiResponses.put(responseCode, newApiResponse); } - operation.setResponses(apiResponses); } + operation.setResponses(apiResponses); } // boolean - is swagger schema has implementation diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ConfigUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ConfigUtils.java index 19776cc775..8d573f79d2 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ConfigUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ConfigUtils.java @@ -56,18 +56,13 @@ import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_ENVIRONMENT_CREATED; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_EXPANDABLE_PROPERTIES; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_EXPANDABLE_PROPERTIES_LOADED; -import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_EXTRA_SCHEMA_ENABLED; -import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_GENERATION_SPEC_ENABLED; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_GROUPS; -import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_DEFAULT_INCLUSION; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_ENABLED; -import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_OPENAPI_ENABLED; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_OPENAPI_ENDPOINTS; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_OPENAPI_PROJECT_DIR; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_OPENAPI_PROPERTIES; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_ROUTER_VERSIONING_PROPERTIES; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_SCHEMA_DECORATORS; -import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_SCHEMA_NAME_SEPARATOR_EMPTY; import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_SECURITY_PROPERTIES; import static io.micronaut.openapi.visitor.ContextUtils.ARGUMENT_CUSTOM_SCHEMA_MAP; import static io.micronaut.openapi.visitor.ContextUtils.ARGUMENT_GROUP_PROPERTIES_MAP; @@ -80,6 +75,7 @@ import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_ENVIRONMENT_ENABLED; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_JACKSON_JSON_VIEW_ENABLED; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_31_JSON_SCHEMA_DIALECT; +import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_ENABLED; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_OPENAPI_PATH; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_OUTPUT_DIR_PATH; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_OUTPUT_FILENAME; @@ -90,6 +86,7 @@ import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ENABLED; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ENVIRONMENTS; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_EXPAND_PREFIX; +import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_GENERATOR_EXTENSIONS_ENABLED; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_GROUPS; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_JSON_VIEW_DEFAULT_INCLUSION; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_PROJECT_DIR; @@ -255,38 +252,26 @@ private static String getClassNameWithGenerics(String className, Map> getExpandableProperties(VisitorContext context) { @@ -341,7 +326,7 @@ public static List> getExpandableProperties(VisitorContext var expandableProperties = new ArrayList>(); // first, check system properties and environments config files - AnnProcessorEnvironment env = (AnnProcessorEnvironment) getEnv(context); + var env = (AnnProcessorEnvironment) getEnv(context); Map propertiesFromEnv = null; if (env != null) { try { @@ -351,7 +336,7 @@ public static List> getExpandableProperties(VisitorContext } } - Map expandedPropsMap = new HashMap<>(); + var expandedPropsMap = new HashMap(); if (CollectionUtils.isNotEmpty(propertiesFromEnv)) { for (Map.Entry entry : propertiesFromEnv.entrySet()) { expandedPropsMap.put(entry.getKey(), entry.getValue().toString()); @@ -384,7 +369,7 @@ public static List> getExpandableProperties(VisitorContext if (key.startsWith(MICRONAUT_OPENAPI_EXPAND_PREFIX)) { key = key.substring(MICRONAUT_OPENAPI_EXPAND_PREFIX.length()); } - Pair prop = Pair.of("\\$\\{" + key + '}', entry.getValue()); + var prop = Pair.of("\\$\\{" + key + '}', entry.getValue()); if (!expandableProperties.contains(prop)) { expandableProperties.add(prop); } @@ -462,19 +447,6 @@ public static boolean isJsonViewEnabled(VisitorContext context) { return isJsonViewEnabled; } - public static boolean isJsonViewDefaultInclusion(VisitorContext context) { - - Boolean isJsonViewDefaultInclusion = ContextUtils.get(MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_DEFAULT_INCLUSION, Boolean.class, context); - if (isJsonViewDefaultInclusion != null) { - return isJsonViewDefaultInclusion; - } - - isJsonViewDefaultInclusion = getBooleanProperty(MICRONAUT_OPENAPI_JSON_VIEW_DEFAULT_INCLUSION, true, context); - ContextUtils.put(MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_DEFAULT_INCLUSION, isJsonViewDefaultInclusion, context); - - return isJsonViewDefaultInclusion; - } - public static SecurityProperties getSecurityProperties(VisitorContext context) { SecurityProperties securityProperties = ContextUtils.get(MICRONAUT_INTERNAL_SECURITY_PROPERTIES, SecurityProperties.class, context); diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ContextProperty.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ContextProperty.java index ea27331558..6ec2cff339 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ContextProperty.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ContextProperty.java @@ -65,10 +65,6 @@ public interface ContextProperty { * Loaded into context jackson.json-view.enabled property value. */ String MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_ENABLED = "micronaut.internal.jackson.json-view.enabled"; - /** - * Loaded into context micronaut.openapi.json-view.default-inclusion property value. - */ - String MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_DEFAULT_INCLUSION = "micronaut.internal.json-view.default-inclusion"; /** * Loaded schema decorators settings into context. */ @@ -93,30 +89,6 @@ public interface ContextProperty { * Loaded micronaut-router and micronaut-openapi router versioning properties. */ String MICRONAUT_INTERNAL_ROUTER_VERSIONING_PROPERTIES = "micronaut.internal.router.versioning.properties"; - /** - * Loaded micronaut.openapi.enabled property value. - *
- * Default: true - */ - String MICRONAUT_INTERNAL_OPENAPI_ENABLED = "micronaut.internal.openapi.enabled"; - /** - * Loaded micronaut.openapi.schema-name.separator.empty property value. - *
- * Default: false - */ - String MICRONAUT_INTERNAL_SCHEMA_NAME_SEPARATOR_EMPTY = "micronaut.internal.schema.name.separator.empty"; - /** - * Loaded micronaut.openapi.generation.spec.enabled value. - *
- * Default: true - */ - String MICRONAUT_INTERNAL_GENERATION_SPEC_ENABLED = "micronaut.internal.generation.spec.enabled"; - /** - * Loaded micronaut.openapi.schema.extra.enabled value. - *
- * Default: true - */ - String MICRONAUT_INTERNAL_EXTRA_SCHEMA_ENABLED = "micronaut.internal.openapi.extra-schemas.enabled"; /** * Saved generated files. */ diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java index 925f008ff2..154cd65b61 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java @@ -607,21 +607,30 @@ public static void setDefaultValueObject(Schema schema, String defaultValue, } } - /** - * Detect openapi type and format for enums. - * - * @param context visitor context - * @param type enum element - * @param schemaType type from swagger Schema annotation - * @param schemaFormat format from swagger Schema annotation - * - * @return pair with openapi type and format - */ - @NonNull - public static Pair checkEnumJsonValueType(VisitorContext context, @NonNull EnumElement type, @Nullable String schemaType, @Nullable String schemaFormat) { - if (schemaType != null && !schemaType.equals(PrimitiveType.STRING.getCommonName())) { - return Pair.of(schemaType, schemaFormat); + public static ClassElement findJsonValueType(EnumElement enumEl, VisitorContext context) { + MethodElement firstMethod = findJsonValueMethod(enumEl, context); + if (firstMethod != null) { + ClassElement returnType = firstMethod.getReturnType(); + if (isEnum(returnType)) { + return findJsonValueType((EnumElement) returnType, context); + } + return returnType; + } + // check JsonValue field + var fields = enumEl.getEnclosedElements(ElementQuery.ALL_FIELDS.annotated(metadata -> metadata.isAnnotationPresent(JsonValue.class))); + if (CollectionUtils.isNotEmpty(fields)) { + var firstField = fields.get(0); + ClassElement fieldType = firstField.getType(); + if (isEnum(fieldType)) { + return findJsonValueType((EnumElement) fieldType, context); + } + return fieldType; } + return null; + } + + @Nullable + private static MethodElement findJsonValueMethod(@NonNull EnumElement type, VisitorContext context) { MethodElement firstMethod = null; // check JsonValue method List methods = type.getEnclosedElements(ElementQuery.ALL_METHODS.annotated(metadata -> metadata.isAnnotationPresent(JsonValue.class))); @@ -644,8 +653,27 @@ public static Pair checkEnumJsonValueType(VisitorContext context break; } } + return firstMethod; + } + /** + * Detect openapi type and format for enums. + * + * @param context visitor context + * @param type enum element + * @param schemaType type from swagger Schema annotation + * @param schemaFormat format from swagger Schema annotation + * + * @return pair with openapi type and format + */ + @NonNull + public static Pair checkEnumJsonValueType(VisitorContext context, @NonNull EnumElement type, @Nullable String schemaType, @Nullable String schemaFormat) { + if (schemaType != null && !schemaType.equals(PrimitiveType.STRING.getCommonName())) { + return Pair.of(schemaType, schemaFormat); + } Pair result = null; + + MethodElement firstMethod = findJsonValueMethod(type, context); if (firstMethod != null) { ClassElement returnType = firstMethod.getReturnType(); if (isEnum(returnType)) { diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java index fe0a6ee47b..31846ff5d9 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java @@ -36,6 +36,7 @@ import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.visitor.VisitorContext; import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; @@ -65,6 +66,7 @@ import java.util.concurrent.atomic.AtomicReference; import static io.micronaut.openapi.visitor.ConfigUtils.isJsonViewEnabled; +import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_DEPRECATED; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_HIDDEN; /** @@ -472,6 +474,34 @@ public static boolean isTypeWithGenericNullable(ClassElement type) { ; } + public static boolean isDeprecated(Element el) { + if (el == null) { + return false; + } + // schema deprecated flag in swagger annotations is prioritized + var schemaAnn = el.getAnnotation(Schema.class); + var operationAnn = el.getAnnotation(Operation.class); + var parameterAnn = el.getAnnotation(Parameter.class); + var headerAnn = el.getAnnotation(Header.class); + var deprecatedBySchema = schemaAnn != null ? schemaAnn.booleanValue(PROP_DEPRECATED).orElse(null) : null; + var deprecatedByOperation = operationAnn != null ? operationAnn.booleanValue(PROP_DEPRECATED).orElse(null) : null; + var deprecatedByParameter = parameterAnn != null ? parameterAnn.booleanValue(PROP_DEPRECATED).orElse(null) : null; + var deprecatedByHeader = headerAnn != null ? headerAnn.booleanValue(PROP_DEPRECATED).orElse(null) : null; + if ((deprecatedBySchema != null && !deprecatedBySchema) + || (deprecatedByOperation != null && !deprecatedByOperation) + || (deprecatedByParameter != null && !deprecatedByParameter) + || (deprecatedByHeader != null && !deprecatedByHeader) + ) { + return false; + } + return deprecatedBySchema != null + || deprecatedByOperation != null + || deprecatedByParameter != null + || deprecatedByHeader != null + || el.isAnnotationPresent(Deprecated.class) + || el.isAnnotationPresent("kotlin.Deprecated"); + } + public static boolean isEnum(ClassElement classElement) { var isEnum = classElement.isEnum(); diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/GeneratorUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/GeneratorUtils.java new file mode 100644 index 0000000000..b4ab514e87 --- /dev/null +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/GeneratorUtils.java @@ -0,0 +1,251 @@ +/* + * Copyright 2017-2024 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. + */ +package io.micronaut.openapi.visitor; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.util.StringUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.EnumElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.visitor.VisitorContext; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import static io.micronaut.openapi.visitor.ConvertUtils.findJsonValueType; +import static io.micronaut.openapi.visitor.ElementUtils.isAnnotationPresent; +import static io.micronaut.openapi.visitor.ElementUtils.isDeprecated; +import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_DEPRECATED; +import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_DESCRIPTION; +import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_HIDDEN; +import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_NAME; + +/** + * OpenAPI Generator utilities methods. + * + * @since 4.12.4 + */ +@Internal +public final class GeneratorUtils { + + private GeneratorUtils() { + } + + public static void addSchemaDeprecatedExtension(Element el, Schema schema, VisitorContext context) { + if (!ConfigUtils.isGeneratorExtensionsEnabled(context) || schema == null || el == null) { + return; + } + + var extensions = schema.getExtensions() != null ? schema.getExtensions() : new HashMap(); + addDeprecatedMessage(el, extensions, context); + if (schema.getExtensions() == null && !extensions.isEmpty()) { + schema.setExtensions(extensions); + } + } + + public static void addParameterDeprecatedExtension(TypedElement el, Parameter parameter, VisitorContext context) { + if (!ConfigUtils.isGeneratorExtensionsEnabled(context) || parameter == null || el == null) { + return; + } + + var extensions = parameter.getExtensions() != null ? parameter.getExtensions() : new HashMap(); + addDeprecatedMessage(el, extensions, context); + if (parameter.getExtensions() == null && !extensions.isEmpty()) { + parameter.setExtensions(extensions); + } + } + + public static void addOperationDeprecatedExtension(MethodElement el, Operation operation, VisitorContext context) { + if (!ConfigUtils.isGeneratorExtensionsEnabled(context) || operation == null || el == null) { + return; + } + + var extensions = operation.getExtensions() != null ? operation.getExtensions() : new HashMap(); + addDeprecatedMessage(el, extensions, context); + if (!extensions.containsKey("x-deprecated-message")) { + addDeprecatedMessage(el.getOwningType(), extensions, context); + } + if (operation.getExtensions() == null && !extensions.isEmpty()) { + operation.setExtensions(extensions); + } + } + + private static void addDeprecatedMessage(Element el, Map extensions, VisitorContext context) { + if (extensions.containsKey("x-deprecated-message")) { + return; + } + + String deprecatedMessage = null; + var deprecatedAnn = el.getAnnotation("kotlin.Deprecated"); + if (deprecatedAnn != null) { + deprecatedMessage = deprecatedAnn.stringValue().orElse(deprecatedAnn.stringValue("message").orElse(null)); + } + if (deprecatedMessage == null) { + var javadoc = el.getDocumentation().orElse(null); + var javadocDescription = Utils.getJavadocParser().parse(javadoc); + if (javadocDescription != null) { + deprecatedMessage = javadocDescription.getDeprecatedDescription(); + } + } + if (StringUtils.isNotEmpty(deprecatedMessage)) { + extensions.put("x-deprecated-message", deprecatedMessage); + } + } + + public static void addValidationMessages(Element el, Schema schema, Map messages, VisitorContext context) { + if (!ConfigUtils.isGeneratorExtensionsEnabled(context) || schema == null || el == null) { + return; + } + + var extensions = schema.getExtensions() != null ? schema.getExtensions() : new HashMap(); + for (var entry : messages.entrySet()) { + if (!extensions.containsKey(entry.getKey())) { + extensions.put(entry.getKey(), entry.getValue()); + } + } + if (schema.getExtensions() == null && !extensions.isEmpty()) { + schema.setExtensions(extensions); + } + } + + public static void addEnumExtensions(EnumElement enumEl, Schema schema, VisitorContext context) { + + if (!ConfigUtils.isGeneratorExtensionsEnabled(context) || enumEl == null) { + return; + } + + var extensions = schema.getExtensions(); + if (extensions == null) { + extensions = new HashMap<>(); + schema.setExtensions(extensions); + } + + String xType = null; + String xFormat = null; + + ClassElement fieldType = findJsonValueType(enumEl, context); + if (fieldType != null) { + if (fieldType.isPrimitive()) { + xType = fieldType.getSimpleName(); + } else { + xType = fieldType.getSimpleName(); + if (xType.equalsIgnoreCase("byte")) { + xType = null; + xFormat = "int8"; + } else if (xType.equalsIgnoreCase("short")) { + xType = null; + xFormat = "int16"; + } else if (xType.equalsIgnoreCase("int") + || xType.equalsIgnoreCase("integer") + || xType.equalsIgnoreCase("long") + || xType.equalsIgnoreCase("float") + || xType.equalsIgnoreCase("double")) { + xType = null; + // because boolean enums generated as Boolean type + } else if (xType.equalsIgnoreCase("boolean")) { + xType = null; + } else if (xType.equalsIgnoreCase("char") || xType.equalsIgnoreCase("character")) { + xType = "char"; + } + } + } + + if (xType != null && !extensions.containsKey("x-type")) { + extensions.put("x-type", xType); + } + if (xFormat != null && !extensions.containsKey("x-format")) { + extensions.put("x-format", xFormat); + } + + var enumVarNameList = new ArrayList(); + var enumVarDocList = new ArrayList(); + var enumVarDeprecatedList = new ArrayList(); + + for (var enumConstEl : enumEl.elements()) { + + var schemaAnn = enumConstEl.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + boolean isHidden = schemaAnn != null && schemaAnn.booleanValue(PROP_HIDDEN).orElse(false); + + if (isHidden + || isAnnotationPresent(enumConstEl, Hidden.class) + || isAnnotationPresent(enumConstEl, JsonIgnore.class)) { + continue; + } + + String enumVarName = null; + if (schemaAnn != null) { + enumVarName = schemaAnn.stringValue(PROP_NAME).orElse(null); + } + if (enumVarName == null) { + enumVarName = enumConstEl.getName(); + } + enumVarNameList.add(enumVarName); + + String enumVarDoc = null; + if (schemaAnn != null) { + enumVarDoc = schemaAnn.stringValue(PROP_DESCRIPTION).orElse(null); + } + if (enumVarDoc == null) { + var enumConstJavadoc = enumConstEl.getDocumentation().orElse(null); + if (enumConstJavadoc != null) { + var javadocDesc = Utils.getJavadocParser().parse(enumConstJavadoc); + if (javadocDesc != null && StringUtils.isNotEmpty(javadocDesc.getMethodDescription())) { + enumVarDoc = javadocDesc.getMethodDescription(); + } + } + } + enumVarDocList.add(enumVarDoc != null ? enumVarDoc : StringUtils.EMPTY_STRING); + + Boolean isDeprecated = null; + if (schemaAnn != null) { + isDeprecated = schemaAnn.booleanValue(PROP_DEPRECATED).orElse(null); + } + if (isDeprecated == null) { + isDeprecated = isDeprecated(enumConstEl); + } + if (isDeprecated) { + enumVarDeprecatedList.add(enumVarName); + } + } + + if (!enumVarNameList.isEmpty() && !extensions.containsKey("x-enum-varnames")) { + extensions.put("x-enum-varnames", enumVarNameList); + } + if (!enumVarDocList.isEmpty() && !extensions.containsKey("x-enum-descriptions")) { + var foundNotEmpty = false; + for (var enumVarDoc : enumVarDocList) { + if (StringUtils.isNotEmpty(enumVarDoc)) { + foundNotEmpty = true; + break; + } + } + if (foundNotEmpty) { + extensions.put("x-enum-descriptions", enumVarDocList); + } + } + if (!enumVarDeprecatedList.isEmpty() && !extensions.containsKey("x-deprecated")) { + extensions.put("x-deprecated", enumVarDeprecatedList); + } + } +} diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java index 3e0ead0f94..78c771c59f 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java @@ -74,7 +74,6 @@ import static io.micronaut.openapi.visitor.ConfigUtils.endpointsConfiguration; import static io.micronaut.openapi.visitor.ConfigUtils.getAdocProperties; -import static io.micronaut.openapi.visitor.ConfigUtils.getBooleanProperty; import static io.micronaut.openapi.visitor.ConfigUtils.getConfigProperty; import static io.micronaut.openapi.visitor.ConfigUtils.getEnv; import static io.micronaut.openapi.visitor.ConfigUtils.getExpandableProperties; @@ -98,7 +97,6 @@ import static io.micronaut.openapi.visitor.OpenApiConfigProperty.ALL; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_APPLICATION_NAME; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ADDITIONAL_FILES; -import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_ENABLED; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_JSON_FORMAT; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_PROPERTY_NAMING_STRATEGY; @@ -717,6 +715,7 @@ private OpenAPI postProcessOpenApi(OpenAPI openApi, VisitorContext context) { applyPropertyNamingStrategy(openApi, context); applyPropertyServerContextPath(openApi, context); + normalizeOpenApi(openApi, context); normalizeOpenApi(openApi, context); // Process after sorting so order is stable new JacksonDiscriminatorPostProcessor().addMissingDiscriminatorType(openApi); @@ -841,7 +840,7 @@ private void generateViews(@Nullable String documentTitle, @Nullable Map, OpenApiInfo> openApiInfos, String documentTitle, VisitorContext context, boolean isYaml) { var isAdocModuleInClassPath = false; - var isGlobalAdocEnabled = getBooleanProperty(MICRONAUT_OPENAPI_ADOC_ENABLED, true, context); + var isGlobalAdocEnabled = ConfigUtils.isAdocEnabled(context); try { Class.forName("io.micronaut.openapi.adoc.OpenApiToAdocConverter"); diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiConfigProperty.java b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiConfigProperty.java index 471ad75306..4e45c192f6 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiConfigProperty.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiConfigProperty.java @@ -35,13 +35,13 @@ public interface OpenApiConfigProperty { *

* Also, you can set it in your application.yml file like this: *

+ *

      * micronaut:
-     * openapi:
-     * schema:
-     * org.somepackage.MyComplexType: java.lang.String
-     * org.somepackage.MyComplexType2: java.lang.Integer
-     * ...
-     *
+     *   openapi:
+     *     schema:
+     *       org.somepackage.MyComplexType: java.lang.String
+     *       org.somepackage.MyComplexType2: java.lang.Integer
+     * 
* @deprecated Use `micronaut.openapi.schema.mapping` property instead */ @Deprecated(forRemoval = true) @@ -55,12 +55,13 @@ public interface OpenApiConfigProperty { *

* Also, you can set it in your application.yml file like this: *

+ *

      * micronaut:
-     * openapi:
-     * schema-postfix:
-     * org.api.v1_0_0: 1_0_0
-     * org.api.v2_0_0: 2_0_0
-     * ...
+     *   openapi:
+     *     schema-postfix:
+     *       org.api.v1_0_0: 1_0_0
+     *       org.api.v2_0_0: 2_0_0
+     * 
* @deprecated Use `micronaut.openapi.schema.decorator.prefix` property instead */ @Deprecated(forRemoval = true) @@ -71,6 +72,15 @@ public interface OpenApiConfigProperty { @Deprecated(forRemoval = true) String MICRONAUT_OPENAPI_SCHEMA_POSTFIX = "micronaut.openapi.schema-postfix"; + /** + * Loaded micronaut-http server context path property. + */ + String MICRONAUT_SERVER_CONTEXT_PATH = "micronaut.server.context-path"; + /** + * Loaded micronaut-http-server-netty property (json-view.enabled). + */ + String MICRONAUT_JACKSON_JSON_VIEW_ENABLED = "jackson.json-view.enabled"; + /** * System property that enables or disables open api annotation processing. *
@@ -190,19 +200,18 @@ public interface OpenApiConfigProperty { * Default value is "true". */ String MICRONAUT_OPENAPI_JSON_VIEW_DEFAULT_INCLUSION = "micronaut.openapi.json.view.default.inclusion"; - /** - * micronaut-http server context path property. - */ - String MICRONAUT_SERVER_CONTEXT_PATH = "micronaut.server.context-path"; /** * micronaut-context application name property. */ String MICRONAUT_APPLICATION_NAME = "micronaut.application.name"; /** - * micronaut-http-server-netty property (json-view.enabled). + * If this property is 'true', then generated OpenAPI specification will be with extensions for OpenAPI Generator + * and the generated client according to this specification will be much more accurate than without it. + * For example, enumerations will be described with extensions `x-enum-varnames`, `x-enum-descriptions` and `x-deprecated`. + *
+ * Default: false */ - String MICRONAUT_JACKSON_JSON_VIEW_ENABLED = "jackson.json-view.enabled"; - + String MICRONAUT_OPENAPI_SWAGGER_FILE_GENERATION_ENABLED = "micronaut.openapi.swagger.file.generation.enabled"; /** * System property that enables extra schema processing. */ @@ -237,6 +246,16 @@ public interface OpenApiConfigProperty { */ String MICRONAUT_OPENAPI_SCHEMA_NAME_SEPARATOR_INNER_CLASS = "micronaut.openapi.schema.name.separator.inner-class"; + /** + * If this property is 'true', then generated OpenAPI specification will be with extensions for OpenAPI Generator + * and the generated client according to this specification will be much more accurate than without it. + *

+ * For example, enumerations will be described with extensions `x-enum-varnames`, `x-enum-descriptions` and `x-deprecated`. + *
+ * Default: true + */ + String MICRONAUT_OPENAPI_GENERATOR_EXTENSIONS_ENABLED = "micronaut.openapi.generator.extensions.enabled"; + /** * Properties prefix to set custom schema implementations for selected classes. * For example, if you want to set simple 'java.lang.String' class to some complex 'org.somepackage.MyComplexType' class you need to write: @@ -326,10 +345,6 @@ public interface OpenApiConfigProperty { * OpenAPI file path. */ String MICRONAUT_OPENAPI_ADOC_OPENAPI_PATH = "micronaut.openapi.adoc.openapi.path"; - /** - * OpenAPI file path. - */ - String MICRONAUT_OPENAPI_SWAGGER_FILE_GENERATION_ENABLED = "micronaut.openapi.swagger.file.generation.enabled"; /** * Default openapi config file. */ diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiNormalizeUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiNormalizeUtils.java index f1dfeaaa21..0771143ce2 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiNormalizeUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiNormalizeUtils.java @@ -41,10 +41,11 @@ import java.util.function.BiConsumer; import java.util.function.Function; -import static io.micronaut.openapi.visitor.SchemaUtils.EMPTY_SCHEMA; import static io.micronaut.openapi.visitor.SchemaUtils.EMPTY_SIMPLE_SCHEMA; import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_OBJECT; import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_STRING; +import static io.micronaut.openapi.visitor.SchemaUtils.appendSchema; +import static io.micronaut.openapi.visitor.SchemaUtils.isEmptySchema; import static io.micronaut.openapi.visitor.SchemaUtils.setSpecVersion; /** @@ -59,6 +60,19 @@ private OpenApiNormalizeUtils() { } public static void normalizeOpenApi(OpenAPI openAPI, VisitorContext context) { + + if (CollectionUtils.isEmpty(openAPI.getExtensions())) { + openAPI.setExtensions(null); + } + + if (CollectionUtils.isNotEmpty(openAPI.getServers())) { + for (var server : openAPI.getServers()) { + if (CollectionUtils.isEmpty(server.getExtensions())) { + server.setExtensions(null); + } + } + } + // Sort paths if (openAPI.getPaths() != null) { var sortedPaths = new Paths(); @@ -68,6 +82,9 @@ public static void normalizeOpenApi(OpenAPI openAPI, VisitorContext context) { } openAPI.setPaths(sortedPaths); for (PathItem pathItem : sortedPaths.values()) { + if (CollectionUtils.isEmpty(pathItem.getExtensions())) { + pathItem.setExtensions(null); + } normalizeOperation(pathItem.getGet(), context); normalizeOperation(pathItem.getPut(), context); normalizeOperation(pathItem.getPost(), context); @@ -113,6 +130,9 @@ public static void normalizeOperation(Operation operation, VisitorContext contex if (parameter == null) { continue; } + if (CollectionUtils.isEmpty(parameter.getExtensions())) { + parameter.setExtensions(null); + } Schema paramSchema = parameter.getSchema(); if (paramSchema == null) { continue; @@ -131,6 +151,10 @@ public static void normalizeOperation(Operation operation, VisitorContext contex normalizeExamples(parameter.getExamples()); } } + if (CollectionUtils.isEmpty(operation.getExtensions())) { + operation.setExtensions(null); + } + if (operation.getRequestBody() != null) { normalizeContent(operation.getRequestBody().getContent(), context); } @@ -138,6 +162,9 @@ public static void normalizeOperation(Operation operation, VisitorContext contex for (ApiResponse apiResponse : operation.getResponses().values()) { normalizeContent(apiResponse.getContent(), context); normalizeHeaders(apiResponse.getHeaders(), context); + if (CollectionUtils.isEmpty(apiResponse.getExtensions())) { + apiResponse.setExtensions(null); + } } } } @@ -148,6 +175,9 @@ public static void normalizeHeaders(Map headers, VisitorContext } for (var header : headers.values()) { + if (CollectionUtils.isEmpty(header.getExtensions())) { + header.setExtensions(null); + } Schema headerSchema = header.getSchema(); if (headerSchema == null) { headerSchema = setSpecVersion(new StringSchema()); @@ -173,6 +203,9 @@ public static void normalizeContent(Content content, VisitorContext context) { return; } for (var mediaType : content.values()) { + if (CollectionUtils.isEmpty(mediaType.getExtensions())) { + mediaType.setExtensions(null); + } Schema mediaTypeSchema = mediaType.getSchema(); if (mediaTypeSchema == null) { continue; @@ -211,6 +244,10 @@ public static void normalizeExamples(Map examples) { var example = examples.get(exampleName); if (example == null) { iter.remove(); + continue; + } + if (CollectionUtils.isEmpty(example.getExtensions())) { + example.setExtensions(null); } } } @@ -226,11 +263,19 @@ public static Schema normalizeSchema(Schema schema, VisitorContext context if (schema == null) { return null; } + + if (CollectionUtils.isEmpty(schema.getExtensions())) { + schema.setExtensions(null); + } + List allOf = schema.getAllOf(); if (CollectionUtils.isNotEmpty(allOf)) { if (allOf.size() == 1) { Schema allOfSchema = allOf.get(0); + if (CollectionUtils.isEmpty(allOfSchema.getExtensions())) { + allOfSchema.setExtensions(null); + } schema.setAllOf(null); // if schema has only allOf block with one item or only defaultValue property or only type @@ -255,7 +300,7 @@ public static Schema normalizeSchema(Schema schema, VisitorContext context } boolean isSameType = allOfSchema.getType() == null || allOfSchema.getType().equals(type); - if (SchemaUtils.isEmptySchema(schema) + if (isEmptySchema(schema) && (serializedDefaultValue == null || serializedDefaultValue.equals(serializedAllOfDefaultValue)) && (type == null || isSameType)) { normalizedSchema = allOfSchema; @@ -534,6 +579,23 @@ private static void unwrapAllOff(Schema schema) { return; } + // trying to merge allOf schemas + var mergedAllOf = new ArrayList(); + Schema firstAllOfSchema = null; + for (var innerSchema : schema.getAllOf()) { + if (innerSchema.get$ref() != null) { + mergedAllOf.add(innerSchema); + continue; + } + if (firstAllOfSchema == null) { + firstAllOfSchema = innerSchema; + mergedAllOf.add(firstAllOfSchema); + continue; + } + appendSchema(firstAllOfSchema, innerSchema); + } + schema.setAllOf(mergedAllOf); + var index = 0; var innerSchemas = new HashMap(); for (var s : schema.getAllOf()) { @@ -549,6 +611,16 @@ private static void unwrapAllOff(Schema schema) { for (var entry : innerSchemas.entrySet()) { var innerSchema = entry.getValue(); innerSchema.setName(null); + if (CollectionUtils.isNotEmpty(innerSchema.getExtensions())) { + innerSchema.getExtensions().forEach((k, v) -> schema.addExtension(k.toString(), v)); + } + innerSchema.setExtensions(null); +// appendSchema(schema, innerSchema, false, true); + + if (schema.getType() == null && innerSchema.getType() != null) { + schema.setType(innerSchema.getType()); + innerSchema.setType(null); + } if (StringUtils.isNotEmpty(innerSchema.getTitle())) { schema.setTitle(innerSchema.getTitle()); innerSchema.setTitle(null); @@ -609,11 +681,39 @@ private static void unwrapAllOff(Schema schema) { schema.setReadOnly(innerSchema.getReadOnly()); innerSchema.setReadOnly(null); } - if (EMPTY_SCHEMA.equals(innerSchema)) { + if (innerSchema.getAdditionalProperties() != null) { + schema.setAdditionalProperties(innerSchema.getAdditionalProperties()); + innerSchema.setAdditionalProperties(null); + } + + if (isEmptySchema(innerSchema) && CollectionUtils.isNotEmpty(schema.getAllOf())) { schema.getAllOf().remove(entry.getKey().intValue()); } - if (schema.getAllOf().isEmpty()) { + if (CollectionUtils.isEmpty(schema.getAllOf())) { schema.setAllOf(null); + // check case, when schema has only 1 element +// } else if (schema.getAllOf().size() == 1) { +// var allOf = schema.getAllOf(); +// var inner = allOf.get(0); +// schema.setAllOf(null); +// var ref = inner.get$ref(); +// inner.set$ref(null); +// if (isEmptySchema(schema)) { +// schema.set$ref(ref); +//// if (!isEmptySchema(innerSchema)) { +//// appendSchema(schema, innerSchema); +//// } +//// schema.setAllOf(null); +// } else { +// inner.set$ref(ref); +// schema.setAllOf(allOf); +// } +// if (inner.get$ref() == null) { +// appendSchema(schema, inner, true, true); +// if (CollectionUtils.isEmpty(schema.getAllOf())) { +// schema.setAllOf(null); +// } +// } } } } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java index 7cab7e3f01..3c1a27be2e 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java @@ -137,12 +137,16 @@ import static io.micronaut.openapi.visitor.ElementUtils.getAnnotation; import static io.micronaut.openapi.visitor.ElementUtils.getAnnotationMetadata; import static io.micronaut.openapi.visitor.ElementUtils.isAnnotationPresent; +import static io.micronaut.openapi.visitor.ElementUtils.isDeprecated; import static io.micronaut.openapi.visitor.ElementUtils.isEnum; import static io.micronaut.openapi.visitor.ElementUtils.isFileUpload; import static io.micronaut.openapi.visitor.ElementUtils.isNotNullable; import static io.micronaut.openapi.visitor.ElementUtils.isNullable; import static io.micronaut.openapi.visitor.ElementUtils.isTypeWithGenericNullable; import static io.micronaut.openapi.visitor.ElementUtils.stringValue; +import static io.micronaut.openapi.visitor.GeneratorUtils.addEnumExtensions; +import static io.micronaut.openapi.visitor.GeneratorUtils.addSchemaDeprecatedExtension; +import static io.micronaut.openapi.visitor.GeneratorUtils.addValidationMessages; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.expandProperties; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.replacePlaceholders; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.resolvePlaceholders; @@ -199,11 +203,11 @@ import static io.micronaut.openapi.visitor.ProtoUtils.normalizePropertyName; import static io.micronaut.openapi.visitor.ProtoUtils.normalizeProtobufClassName; import static io.micronaut.openapi.visitor.ProtoUtils.protobufTypeSchema; -import static io.micronaut.openapi.visitor.SchemaUtils.EMPTY_SCHEMA; import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_ARRAY; import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_OBJECT; import static io.micronaut.openapi.visitor.SchemaUtils.appendSchema; import static io.micronaut.openapi.visitor.SchemaUtils.getSchemaByRef; +import static io.micronaut.openapi.visitor.SchemaUtils.isEmptySchema; import static io.micronaut.openapi.visitor.SchemaUtils.processExtensions; import static io.micronaut.openapi.visitor.SchemaUtils.setAllowableValues; import static io.micronaut.openapi.visitor.SchemaUtils.setSpecVersion; @@ -291,6 +295,7 @@ public static Schema readSchema(AnnotationValue getSchemaDefinition(OpenAPI openAPI, schema.setFormat(typeAndFormat.getSecond()); if (CollectionUtils.isEmpty(schema.getEnum())) { schema.setEnum(getEnumValues(enumEl, schema.getType(), schema.getFormat(), context)); + addEnumExtensions(enumEl, schema, context); } } else { Schema schemaWithSuperTypes = processSuperTypes(null, schemaName, type, definingElement, openAPI, mediaTypes, schemas, context, jsonViewClass); @@ -383,6 +389,10 @@ public static Schema getSchemaDefinition(OpenAPI openAPI, populateSchemaProperties(openAPI, context, type, typeArgs, schema, mediaTypes, javadoc, jsonViewClass); checkAllOf(schema); } + if (isDeprecated(type) && schema != null) { + schema.setDeprecated(true); + addSchemaDeprecatedExtension(type, schema, context); + } } } else { return setSpecVersion(primitiveType.createProperty()); @@ -756,7 +766,7 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin return schema; } if (!isArray && ClassUtils.isJavaLangType(typeName)) { - schema = getPrimitiveType(type, typeName); + schema = getPrimitiveType(type, typeName, context); } else if (!isArray && primitiveType != null) { schema = setSpecVersion(primitiveType.createProperty()); } else if (type.isAssignable(Map.class)) { @@ -771,11 +781,11 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin if (componentType != null) { schema = resolveSchema(openApi, type, componentType, context, mediaTypes, jsonViewClass, null, classJavadoc); } else { - schema = getPrimitiveType(null, Object.class.getName()); + schema = getPrimitiveType(null, Object.class.getName(), context); } List fields = type.getPackageName().startsWith("java.util") ? Collections.emptyList() : type.getFields(); if (schema != null && fields.isEmpty()) { - schema = processGenericAnnotations(schema, componentType); + schema = processGenericAnnotations(schema, componentType, context); schema = SchemaUtils.arraySchema(schema); } else { schema = getSchemaDefinition(openApi, context, type, typeArgs, definingElement, mediaTypes, jsonViewClass); @@ -832,7 +842,7 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin schema = setSpecVersion(PrimitiveType.OBJECT.createProperty()); } else { schema = getSchemaDefinition(openApi, context, type, typeArgs, definingElement, mediaTypes, jsonViewClass); - schema = processGenericAnnotations(schema, componentType); + schema = processGenericAnnotations(schema, componentType, context); } } @@ -885,10 +895,11 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin * @param elementType The element type * @param schemaToBind The schema to bind * @param jsonViewClass Class from JsonView annotation + * @param withProcessDeprecated Is need to process deprecates annotation * @return The bound schema */ public static Schema bindSchemaForElement(VisitorContext context, TypedElement element, ClassElement elementType, Schema schemaToBind, - @Nullable ClassElement jsonViewClass) { + @Nullable ClassElement jsonViewClass, boolean withProcessDeprecated) { var schemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); Schema originalSchema = schemaToBind != null ? schemaToBind : new Schema<>(); @@ -921,7 +932,7 @@ public static Schema bindSchemaForElement(VisitorContext context, TypedElemen arraySchemaAnn.stringValue(PROP_NAME).ifPresent(schemaToBind::setName); } - processJakartaValidationAnnotations(element, elementType, schemaToBind); + processJakartaValidationAnnotations(element, elementType, schemaToBind, context); final ComposedSchema composedSchema; final Schema topLevelSchema; @@ -938,11 +949,11 @@ public static Schema bindSchemaForElement(VisitorContext context, TypedElemen if (StringUtils.isNotEmpty(topLevelSchema.getDescription())) { notOnlyRef = true; } - if (isAnnotationPresent(element, Deprecated.class) - && !(element instanceof PropertyElement propertyEl - && isProtobufGenerated(propertyEl.getOwningType()) - && elementType.getName().equals(Map.class.getName()) + if (withProcessDeprecated + && (isDeprecated(element) || isAnnotationPresent(element, Deprecated.class) || isAnnotationPresent(element, "kotlin.Deprecated")) + && !(element instanceof PropertyElement propertyEl && isProtobufGenerated(propertyEl.getOwningType()) && elementType.getName().equals(Map.class.getName()) )) { + addSchemaDeprecatedExtension(element, topLevelSchema, context); topLevelSchema.setDeprecated(true); notOnlyRef = true; } @@ -1509,7 +1520,7 @@ public static void processSchemaProperty(VisitorContext context, TypedElement el required = true; } - propertySchema = bindSchemaForElement(context, element, elementType, propertySchema, null); + propertySchema = bindSchemaForElement(context, element, elementType, propertySchema, null, true); String propertyName = resolvePropertyName(element, classElement, propertySchema); propertyName = normalizePropertyName(propertyName, classElement, elementType); propertySchema.setRequired(null); @@ -2123,7 +2134,7 @@ private static Map annotationValueArrayToSubmap(Object[] a, Stri return mediaTypes; } - private static Schema getPrimitiveType(ClassElement type, String typeName) { + private static Schema getPrimitiveType(ClassElement type, String typeName, VisitorContext context) { Schema schema = null; Class aClass = ClassUtils.getPrimitiveType(typeName).orElse(null); if (aClass == null) { @@ -2139,20 +2150,20 @@ private static Schema getPrimitiveType(ClassElement type, String typeName) { } } - processArgTypeAnnotations(type, schema); + processArgTypeAnnotations(type, schema, context); return schema; } - private static Schema processGenericAnnotations(Schema schema, ClassElement componentType) { + private static Schema processGenericAnnotations(Schema schema, ClassElement componentType, VisitorContext context) { if (componentType == null) { return schema; } - var primitiveComponentType = getPrimitiveType(componentType, componentType.getName()); + var primitiveComponentType = getPrimitiveType(componentType, componentType.getName(), context); if (primitiveComponentType == null) { var schemaFromTypeArgAnnotations = setSpecVersion(new Schema<>()); - processArgTypeAnnotations(componentType, schemaFromTypeArgAnnotations); - if (schemaFromTypeArgAnnotations.equals(EMPTY_SCHEMA)) { + processArgTypeAnnotations(componentType, schemaFromTypeArgAnnotations, context); + if (isEmptySchema(schemaFromTypeArgAnnotations)) { return schema; } var composedSchema = setSpecVersion(new ComposedSchema()); @@ -2164,14 +2175,14 @@ private static Schema processGenericAnnotations(Schema schema, ClassElemen return schema; } - private static void processArgTypeAnnotations(ClassElement type, @Nullable Schema schema) { + private static void processArgTypeAnnotations(ClassElement type, @Nullable Schema schema, VisitorContext context) { if (schema == null || type == null || type.getAnnotationNames().isEmpty()) { return; } if (isNullable(type) && !isNotNullable(type)) { SchemaUtils.setNullable(schema); } - processJakartaValidationAnnotations(type, type, schema); + processJakartaValidationAnnotations(type, type, schema, context); } private static boolean processJacksonPropertyAnn(Element element, ClassElement elType, Schema schemaToBind, @@ -2227,16 +2238,27 @@ private static boolean processJacksonPropertyAnn(Element element, ClassElement e return reference.get(); } - private static void processJakartaValidationAnnotations(Element element, ClassElement elementType, Schema schemaToBind) { + private static void processJakartaValidationAnnotations(Element element, ClassElement elementType, Schema schemaToBind, VisitorContext context) { final boolean isIterableOrMap = elementType.isIterable() || elementType.isAssignable(Map.class); + var messages = new HashMap(); + + addValidationAnnMessage(element, "javax.validation.constraints.NotNull$List", "x-not-null-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.NotNull$List", "x-not-null-message", messages, context); + if (isIterableOrMap) { if (isAnnotationPresent(element, "javax.validation.constraints.NotEmpty$List") || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List")) { schemaToBind.setMinItems(1); + + addValidationAnnMessage(element, "javax.validation.constraints.NotEmpty$List", "x-size-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.NotEmpty$List", "x-size-message", messages, context); } + addValidationAnnMessage(element, "javax.validation.constraints.Size$List", "x-size-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Size$List", "x-size-message", messages, context); + findAnnotation(element, "javax.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("min") @@ -2262,6 +2284,12 @@ private static void processJakartaValidationAnnotations(Element element, ClassEl || isAnnotationPresent(element, "javax.validation.constraints.NotBlank$List") || isAnnotationPresent(element, "jakarta.validation.constraints.NotBlank$List")) { schemaToBind.setMinLength(1); + + addValidationAnnMessage(element, "javax.validation.constraints.NotEmpty$List", "x-size-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.NotEmpty$List", "x-size-message", messages, context); + + addValidationAnnMessage(element, "javax.validation.constraints.NotBlank$List", "x-size-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.NotBlank$List", "x-size-message", messages, context); } findAnnotation(element, "javax.validation.constraints.Size$List") @@ -2278,122 +2306,157 @@ private static void processJakartaValidationAnnotations(Element element, ClassEl ann.intValue("max").ifPresent(schemaToBind::setMaxLength); } }); + + addValidationAnnMessage(element, "javax.validation.constraints.Size$List", "x-size-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Size$List", "x-size-message", messages, context); } if (isAnnotationPresent(element, "javax.validation.constraints.Negative$List") || isAnnotationPresent(element, "jakarta.validation.constraints.Negative$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); schemaToBind.exclusiveMaximum(true); + + addValidationAnnMessage(element, "javax.validation.constraints.Negative$List", "x-max-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Negative$List", "x-max-message", messages, context); } if (isAnnotationPresent(element, "javax.validation.constraints.NegativeOrZero$List") || isAnnotationPresent(element, "jakarta.validation.constraints.NegativeOrZero$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); + + addValidationAnnMessage(element, "javax.validation.constraints.NegativeOrZero$List", "x-max-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.NegativeOrZero$List", "x-max-message", messages, context); } if (isAnnotationPresent(element, "javax.validation.constraints.Positive$List") || isAnnotationPresent(element, "jakarta.validation.constraints.Positive$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); schemaToBind.exclusiveMinimum(true); + + addValidationAnnMessage(element, "javax.validation.constraints.Positive$List", "x-min-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Positive$List", "x-min-message", messages, context); } if (isAnnotationPresent(element, "javax.validation.constraints.PositiveOrZero$List") || isAnnotationPresent(element, "jakarta.validation.constraints.PositiveOrZero$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); + + addValidationAnnMessage(element, "javax.validation.constraints.PositiveOrZero$List", "x-min-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.PositiveOrZero$List", "x-min-message", messages, context); } findAnnotation(element, "javax.validation.constraints.Min$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMinimum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMinimum); } }); findAnnotation(element, "jakarta.validation.constraints.Min$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMinimum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMinimum); } }); + addValidationAnnMessage(element, "javax.validation.constraints.Min$List", "x-min-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Min$List", "x-min-message", messages, context); findAnnotation(element, "javax.validation.constraints.Max$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMaximum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMaximum); } }); findAnnotation(element, "jakarta.validation.constraints.Max$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMaximum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMaximum); } }); + addValidationAnnMessage(element, "javax.validation.constraints.Max$List", "x-max-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Max$List", "x-max-message", messages, context); findAnnotation(element, "javax.validation.constraints.DecimalMin$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMinimum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMinimum); } }); findAnnotation(element, "jakarta.validation.constraints.DecimalMin$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMinimum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMinimum); } }); + addValidationAnnMessage(element, "javax.validation.constraints.DecimalMin$List", "x-min-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.DecimalMin$List", "x-min-message", messages, context); findAnnotation(element, "javax.validation.constraints.DecimalMax$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMaximum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMaximum); } }); findAnnotation(element, "jakarta.validation.constraints.DecimalMax$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMaximum); + ann.getValue(BigDecimal.class).ifPresent(schemaToBind::setMaximum); } }); + addValidationAnnMessage(element, "javax.validation.constraints.DecimalMax$List", "x-max-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.DecimalMax$List", "x-max-message", messages, context); findAnnotation(element, "javax.validation.constraints.Email$List") .ifPresent(listAnn -> { schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.stringValue("regexp") - .ifPresent(schemaToBind::setPattern); + ann.stringValue("regexp").ifPresent(schemaToBind::setPattern); } }); findAnnotation(element, "jakarta.validation.constraints.Email$List") .ifPresent(listAnn -> { schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.stringValue("regexp") - .ifPresent(schemaToBind::setPattern); + ann.stringValue("regexp").ifPresent(schemaToBind::setPattern); } }); findAnnotation(element, "javax.validation.constraints.Pattern$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.stringValue("regexp") - .ifPresent(schemaToBind::setPattern); + ann.stringValue("regexp").ifPresent(schemaToBind::setPattern); } }); findAnnotation(element, "jakarta.validation.constraints.Pattern$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.stringValue("regexp") - .ifPresent(schemaToBind::setPattern); + ann.stringValue("regexp").ifPresent(schemaToBind::setPattern); } }); + addValidationAnnMessage(element, "javax.validation.constraints.Email$List", "x-pattern-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Email$List", "x-pattern-message", messages, context); + addValidationAnnMessage(element, "javax.validation.constraints.Pattern$List", "x-pattern-message", messages, context); + addValidationAnnMessage(element, "jakarta.validation.constraints.Pattern$List", "x-pattern-message", messages, context); + element.getValue("io.micronaut.http.annotation.Part", String.class) .ifPresent(schemaToBind::setName); + } + addValidationMessages(element, schemaToBind, messages, context); + } + + private static void addValidationAnnMessage(Element element, String annName, String extName, Map messages, VisitorContext context) { + if (!ConfigUtils.isGeneratorExtensionsEnabled(context)) { + return; + } + + findAnnotation(element, annName) + .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) + .ifPresent(ann -> ann.stringValue("message") + .ifPresent(message -> { + var strMessage = message != null ? message.toString() : null; + if (StringUtils.isEmpty(strMessage)) { + return; + } + messages.put(extName, strMessage); + }))); } private static Schema processMapSchema(ClassElement type, Map typeArgs, @@ -2694,11 +2757,11 @@ private static String computeNameWithGenerics(ClassElement classElement, Map(), typeArgs, context); + computeNameWithGenerics(classElement, builder, new HashSet<>(), typeArgs, context, isProtobufGenerated); return builder.toString(); } - private static void computeNameWithGenerics(ClassElement classElement, StringBuilder builder, Set computed, Map typeArgs, VisitorContext context) { + private static void computeNameWithGenerics(ClassElement classElement, StringBuilder builder, Set computed, Map typeArgs, VisitorContext context, boolean isProtobufGenerated) { var genericSeparator = getGenericSeparator(context); var innerClassSeparator = getInnerClassSeparator(context); @@ -2719,14 +2782,25 @@ private static void computeNameWithGenerics(ClassElement classElement, StringBui } } } - builder.append(ce.getSimpleName()); + var className = ce.getSimpleName(); + if (isProtobufGenerated) { + className = normalizeProtobufClassName(className); + } + builder.append(className); Map ceTypeArgs = ce.getTypeArguments(); ClassElement customElement = getCustomSchema(ce.getName(), ceTypeArgs, context); if (customElement != null) { ce = customElement; } if (!computed.contains(ce.getName())) { - computeNameWithGenerics(ce, builder, computed, ceTypeArgs, context); + computeNameWithGenerics(ce, builder, computed, ceTypeArgs, context, isProtobufGenerated); + } else if (CollectionUtils.isNotEmpty(ceTypeArgs)) { + ce = ceTypeArgs.values().iterator().next(); + className = ce.getSimpleName(); + if (isProtobufGenerated) { + className = normalizeProtobufClassName(className); + } + builder.append(genericSeparator).append(className).append(genericSeparator); } if (i.hasNext()) { builder.append(innerClassSeparator); diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaUtils.java index f399bad0b4..64109f046f 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaUtils.java @@ -666,6 +666,10 @@ public static Schema mergeSchema(Schema s1, Schema s2) { } public static Schema appendSchema(Schema s1, Schema s2) { + return appendSchema(s1, s2, true, false); + } + + public static Schema appendSchema(Schema s1, Schema s2, boolean withBlocks, boolean withErase) { if (s1 == null) { return s2; } @@ -678,201 +682,404 @@ public static Schema appendSchema(Schema s1, Schema s2) { if ((s1.getType() == null || TYPE_OBJECT.equals(s1.getType())) && s2.getType() != null && !TYPE_OBJECT.equals(s2.getType())) { s1.setType(s2.getType()); - if (s1.getFormat() == null && s2.getFormat() != null) { - s1.setFormat(s2.getFormat()); + if (withErase) { + s2.setType(null); + } + } + if (s1.getFormat() == null && s2.getFormat() != null) { + s1.setFormat(s2.getFormat()); + if (withErase) { + s2.setFormat(null); } } if (s1.getName() == null && s2.getName() != null) { s1.setName(s2.getName()); + if (withErase) { + s2.setName(null); + } } if (s1.getTitle() == null && s2.getTitle() != null) { s1.setTitle(s2.getTitle()); + if (withErase) { + s2.setTitle(null); + } } if (s1.getMultipleOf() == null && s2.getMultipleOf() != null) { s1.setMultipleOf(s2.getMultipleOf()); + if (withErase) { + s2.setMultipleOf(null); + } } if (s1.getMaximum() == null && s2.getMaximum() != null) { s1.setMaximum(s2.getMaximum()); + if (withErase) { + s2.setMaximum(null); + } } if (s1.getExclusiveMaximum() == null && s2.getExclusiveMaximum() != null) { s1.setExclusiveMaximum(s2.getExclusiveMaximum()); + if (withErase) { + s2.setExclusiveMaximum(null); + } } if (s1.getMinimum() == null && s2.getMinimum() != null) { s1.setMinimum(s2.getMinimum()); + if (withErase) { + s2.setMinimum(null); + } } if (s1.getExclusiveMinimum() == null && s2.getExclusiveMinimum() != null) { s1.setExclusiveMinimum(s2.getExclusiveMinimum()); + if (withErase) { + s2.setExclusiveMinimum(null); + } } if (s1.getMaxLength() == null && s2.getMaxLength() != null) { s1.setMaxLength(s2.getMaxLength()); + if (withErase) { + s2.setMaxLength(null); + } } if (s1.getMinLength() == null && s2.getMinLength() != null) { s1.setMinLength(s2.getMinLength()); + if (withErase) { + s2.setMinLength(null); + } } if (s1.getPattern() == null && s2.getPattern() != null) { s1.setPattern(s2.getPattern()); + if (withErase) { + s2.setPattern(null); + } } if (s1.getMaxItems() == null && s2.getMaxItems() != null) { s1.setMaxItems(s2.getMaxItems()); + if (withErase) { + s2.setMaxItems(null); + } } if (s1.getMinItems() == null && s2.getMinItems() != null) { s1.setMinItems(s2.getMinItems()); + if (withErase) { + s2.setMinItems(null); + } } if (s1.getUniqueItems() == null && s2.getUniqueItems() != null) { s1.setUniqueItems(s2.getUniqueItems()); + if (withErase) { + s2.setUniqueItems(null); + } } if (s1.getMaxProperties() == null && s2.getMaxProperties() != null) { s1.setMaxProperties(s2.getMaxProperties()); + if (withErase) { + s2.setMaxProperties(null); + } } if (s1.getMinProperties() == null && s2.getMinProperties() != null) { s1.setMinProperties(s2.getMinProperties()); + if (withErase) { + s2.setMinProperties(null); + } } if (s1.getRequired() == null && s2.getRequired() != null) { s1.setRequired(s2.getRequired()); + if (withErase) { + s2.setRequired(null); + } } if (s1.getType() == null && s2.getType() != null) { s1.setType(s2.getType()); + if (withErase) { + s2.setType(null); + } } if (s1.getNot() == null && s2.getNot() != null) { s1.setNot(s2.getNot()); + if (withErase) { + s2.setNot(null); + } } if (s1.getProperties() == null && s2.getProperties() != null) { s1.setProperties(s2.getProperties()); + if (withErase) { + s2.setProperties(null); + } } if (s1.getAdditionalProperties() == null && s2.getAdditionalProperties() != null) { s1.setAdditionalProperties(s2.getAdditionalProperties()); + if (withErase) { + s2.setAdditionalProperties(null); + } } if (s1.getDescription() == null && s2.getDescription() != null) { s1.setDescription(s2.getDescription()); + if (withErase) { + s2.setDescription(null); + } } if (s1.get$ref() == null && s2.get$ref() != null) { s1.set$ref(s2.get$ref()); + if (withErase) { + s2.set$ref(null); + } } if (s1.getNullable() == null && s2.getNullable() != null) { s1.setNullable(s2.getNullable()); + if (withErase) { + s2.setNullable(null); + } } if (s1.getReadOnly() == null && s2.getReadOnly() != null) { s1.setReadOnly(s2.getReadOnly()); + if (withErase) { + s2.setReadOnly(null); + } } if (s1.getWriteOnly() == null && s2.getWriteOnly() != null) { s1.setWriteOnly(s2.getWriteOnly()); + if (withErase) { + s2.setWriteOnly(null); + } + } + if (!s1.getExampleSetFlag() && s2.getExampleSetFlag()) { + s1.setExampleSetFlag(s2.getExampleSetFlag()); + s1.setExample(s2.getExample()); + if (withErase) { + s2.setExample(null); + s2.setExampleSetFlag(false); + } } if (s1.getExample() == null && s2.getExample() != null) { s1.setExample(s2.getExample()); + if (withErase) { + s2.setExample(null); + } } if (s1.getExternalDocs() == null && s2.getExternalDocs() != null) { s1.setExternalDocs(s2.getExternalDocs()); + if (withErase) { + s2.setExternalDocs(null); + } } if (s1.getDeprecated() == null && s2.getDeprecated() != null) { s1.setDeprecated(s2.getDeprecated()); + if (withErase) { + s2.setDeprecated(null); + } } if (s1.getXml() == null && s2.getXml() != null) { s1.setXml(s2.getXml()); + if (withErase) { + s2.setXml(null); + } } if (s1.getExtensions() == null && s2.getExtensions() != null) { s1.setExtensions(s2.getExtensions()); + if (withErase) { + s2.setExtensions(null); + } } if (s1.getDiscriminator() == null && s2.getDiscriminator() != null) { s1.setDiscriminator(s2.getDiscriminator()); + if (withErase) { + s2.setDiscriminator(null); + } } if (s1.getPrefixItems() == null && s2.getPrefixItems() != null) { s1.setPrefixItems(s2.getPrefixItems()); + if (withErase) { + s2.setPrefixItems(null); + } } if (s1.getElse() == null && s2.getElse() != null) { s1.setElse(s2.getElse()); + if (withErase) { + s2.setElse(null); + } } - if (s1.getAnyOf() == null && s2.getAnyOf() != null) { - s1.setAnyOf(s2.getAnyOf()); - } - if (s1.getOneOf() == null && s2.getOneOf() != null) { - s1.setOneOf(s2.getOneOf()); + if (withBlocks) { + if (s1.getAnyOf() == null && s2.getAnyOf() != null) { + s1.setAnyOf(s2.getAnyOf()); + } + if (s1.getOneOf() == null && s2.getOneOf() != null) { + s1.setOneOf(s2.getOneOf()); + } } if (s1.getItems() == null && s2.getItems() != null) { s1.setItems(s2.getItems()); + if (withErase) { + s2.setItems(null); + } } if (s1.getTypes() == null && s2.getTypes() != null) { s1.setTypes(s2.getTypes()); + if (withErase) { + s2.setTypes(null); + } } + if (s1.getPatternProperties() == null && s2.getPatternProperties() != null) { s1.setPatternProperties(s2.getPatternProperties()); + if (withErase) { + s2.setPatternProperties(null); + } } if (s1.getExclusiveMaximumValue() == null && s2.getExclusiveMaximumValue() != null) { s1.setExclusiveMaximumValue(s2.getExclusiveMaximumValue()); + if (withErase) { + s2.setExclusiveMaximumValue(null); + } } if (s1.getExclusiveMinimumValue() == null && s2.getExclusiveMinimumValue() != null) { s1.setExclusiveMinimumValue(s2.getExclusiveMinimumValue()); + if (withErase) { + s2.setExclusiveMinimumValue(null); + } } if (s1.getContains() == null && s2.getContains() != null) { s1.setContains(s2.getContains()); + if (withErase) { + s2.setContains(null); + } } if (s1.get$id() == null && s2.get$id() != null) { s1.set$id(s2.get$id()); + if (withErase) { + s2.set$id(null); + } } if (s1.get$schema() == null && s2.get$schema() != null) { s1.set$schema(s2.get$schema()); + if (withErase) { + s2.set$schema(null); + } } if (s1.get$anchor() == null && s2.get$anchor() != null) { s1.set$anchor(s2.get$anchor()); + if (withErase) { + s2.set$anchor(null); + } } if (s1.get$vocabulary() == null && s2.get$vocabulary() != null) { s1.set$vocabulary(s2.get$vocabulary()); + if (withErase) { + s2.set$vocabulary(null); + } } if (s1.get$dynamicAnchor() == null && s2.get$dynamicAnchor() != null) { s1.set$dynamicAnchor(s2.get$dynamicAnchor()); + if (withErase) { + s2.set$dynamicAnchor(null); + } } if (s1.getContentEncoding() == null && s2.getContentEncoding() != null) { s1.setContentEncoding(s2.getContentEncoding()); + if (withErase) { + s2.setContentEncoding(null); + } } if (s1.getContentMediaType() == null && s2.getContentMediaType() != null) { s1.setContentMediaType(s2.getContentMediaType()); + if (withErase) { + s2.setContentMediaType(null); + } } if (s1.getContentSchema() == null && s2.getContentSchema() != null) { s1.setContentSchema(s2.getContentSchema()); + if (withErase) { + s2.setContentSchema(null); + } } if (s1.getPropertyNames() == null && s2.getPropertyNames() != null) { s1.setPropertyNames(s2.getPropertyNames()); + if (withErase) { + s2.setPropertyNames(null); + } } if (s1.getUnevaluatedProperties() == null && s2.getUnevaluatedProperties() != null) { s1.setUnevaluatedProperties(s2.getUnevaluatedProperties()); + if (withErase) { + s2.setUnevaluatedProperties(null); + } } if (s1.getMaxContains() == null && s2.getMaxContains() != null) { s1.setMaxContains(s2.getMaxContains()); + if (withErase) { + s2.setMaxContains(null); + } } if (s1.getMinContains() == null && s2.getMinContains() != null) { s1.setMinContains(s2.getMinContains()); + if (withErase) { + s2.setMinContains(null); + } } if (s1.getAdditionalItems() == null && s2.getAdditionalItems() != null) { s1.setAdditionalItems(s2.getAdditionalItems()); + if (withErase) { + s2.setAdditionalItems(null); + } } if (s1.getUnevaluatedItems() == null && s2.getUnevaluatedItems() != null) { s1.setUnevaluatedItems(s2.getUnevaluatedItems()); + if (withErase) { + s2.setUnevaluatedItems(null); + } } if (s1.getIf() == null && s2.getIf() != null) { s1.setIf(s2.getIf()); + if (withErase) { + s2.setIf(null); + } } if (s1.getThen() == null && s2.getThen() != null) { s1.setThen(s2.getThen()); + if (withErase) { + s2.setThen(null); + } } if (s1.getDependentSchemas() == null && s2.getDependentSchemas() != null) { s1.setDependentSchemas(s2.getDependentSchemas()); + if (withErase) { + s2.setDependentSchemas(null); + } } if (s1.getDependentRequired() == null && s2.getDependentRequired() != null) { s1.setDependentRequired(s2.getDependentRequired()); + if (withErase) { + s2.setDependentRequired(null); + } } if (s1.get$comment() == null && s2.get$comment() != null) { s1.set$comment(s2.get$comment()); + if (withErase) { + s2.set$comment(null); + } } if (s1.getExamples() == null && s2.getExamples() != null) { s1.setExamples((List) s2.getExamples()); + if (withErase) { + s2.setExamples(null); + } } if (s1.getBooleanSchemaValue() == null && s2.getBooleanSchemaValue() != null) { s1.setBooleanSchemaValue(s2.getBooleanSchemaValue()); + if (withErase) { + s2.setBooleanSchemaValue(null); + } } if (s1.getJsonSchema() == null && s2.getJsonSchema() != null) { s1.setJsonSchema(s2.getJsonSchema()); + if (withErase) { + s2.setJsonSchema(null); + } } if (s1.getJsonSchemaImpl() == null && s2.getJsonSchemaImpl() != null) { s1.setJsonSchemaImpl(s2.getJsonSchemaImpl()); + if (withErase) { + s2.setJsonSchemaImpl(null); + } } return s1; } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/UrlUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/UrlUtils.java index 25bdff9770..3d5ed94374 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/UrlUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/UrlUtils.java @@ -48,6 +48,7 @@ private UrlUtils() { * Construct all possible URL variants by parsed segments. * * @param segments url template segments + * @param context visitor context * @return all possible URL variants by parsed segments. */ public static List buildUrls(List segments, VisitorContext context) { diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApi31Spec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApi31Spec.groovy index be6b3e6b0b..be0732f566 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApi31Spec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApi31Spec.groovy @@ -5,9 +5,11 @@ import io.micronaut.openapi.AbstractOpenApiTypeElementSpec import io.micronaut.openapi.OpenApiUtils import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.Schema +import spock.util.environment.RestoreSystemProperties class OpenApi31Spec extends AbstractOpenApiTypeElementSpec { + @RestoreSystemProperties void "test info OpenAPI 3.1.0"() { setup: @@ -90,12 +92,9 @@ class MyBean {} openApi.info.license.name == 'Apache 2.0' openApi.info.license.url == 'https://foo.bar' openApi.info.license.identifier == 'licenseId' - - cleanup: - System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_ENABLED) - System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_JSON_SCHEMA_DIALECT) } + @RestoreSystemProperties void "test Webhooks OpenAPI 3.1.0"() { setup: @@ -152,11 +151,9 @@ class MyBean {} openApi.webhooks.'controllerWebhook'.post.summary == 'Save a pet' openApi.webhooks.'controllerWebhook'.post.description == "The saved pet information is returned" openApi.webhooks.'controllerWebhook'.post.requestBody.description == "description" - - cleanup: - System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_ENABLED) } + @RestoreSystemProperties void "test min/max contains OpenAPI 3.1.0"() { setup: @@ -222,11 +219,9 @@ class MyBean {} openApi.components.schemas.Pet.properties.attrs.items.types.contains('null') openApi.components.schemas.Pet.properties.attrs.minContains == 10 openApi.components.schemas.Pet.properties.attrs.maxContains == 20 - - cleanup: - System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_ENABLED) } + @RestoreSystemProperties void "test discriminator extensions OpenAPI 3.1.0"() { setup: System.setProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_ENABLED, "true") @@ -453,11 +448,9 @@ class MyBean {} petSchema.discriminator.extensions.'x-myExt22' petSchema.discriminator.extensions.'x-myExt22'.prop221 == 'prop1Val1' petSchema.discriminator.extensions.'x-myExt22'.prop222 == 'prop2Val2' - - cleanup: - System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_ENABLED) } + @RestoreSystemProperties void "test json schema OpenAPI 3.1.0"() { setup: System.setProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_ENABLED, "true") @@ -554,8 +547,5 @@ class MyBean {} thetaSchema.title == 'Theta' thetaSchema.allOf.get(0).$ref == '#/components/schemas/Omega' thetaSchema.$ref == './ThetaSchema.json' - - cleanup: - System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_31_ENABLED) } } diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiGeneratorExtensionsSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiGeneratorExtensionsSpec.groovy new file mode 100644 index 0000000000..170870e5a3 --- /dev/null +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiGeneratorExtensionsSpec.groovy @@ -0,0 +1,316 @@ +package io.micronaut.openapi.visitor + +import io.micronaut.openapi.AbstractOpenApiTypeElementSpec +import spock.util.environment.RestoreSystemProperties + +class OpenApiGeneratorExtensionsSpec extends AbstractOpenApiTypeElementSpec { + + @RestoreSystemProperties + void "test deprecated messages extensions"() { + + setup: + System.setProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_GENERATOR_EXTENSIONS_ENABLED, "true") + + when: + buildBeanDefinition('test.MyBean', ''' +package test; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.QueryValue; + +/** + * @deprecated This is deprecated controller + */ +@Deprecated +@Controller +class DeprecatedApi { + + @Get("/deprecated-op1") + public MyObj deprecatedOp1() { + return null; + } + + @Get("/deprecated-op2") + public MyObj deprecatedOp2() { + return null; + } +} + +@Controller +class HelloWorldApi { + + /** + * This is description. + * + * @param name this is deprecated parameter. + * + * @deprecated This is deprecated operation + */ + @Deprecated + @Get("/get/user/{id}") + public MyObj helloWorld( + String id, + @Deprecated @QueryValue String name) { + return null; + } +} + +/** + * Old schema class. + * + * @deprecated This class is deprecated. + */ +@Deprecated +class MyObj { + + /** + * It's deprecated property. + * + * @deprecated need to use another property. + */ + @Deprecated + public String oldProp; + public String newProp; +} + +@jakarta.inject.Singleton +class MyBean {} +''') + + then: + Utils.testReference != null + + when: + def openApi = Utils.testReference + + then: + openApi.paths."/deprecated-op1".get.deprecated + openApi.paths."/deprecated-op1".get.extensions."x-deprecated-message" == "This is deprecated controller" + openApi.paths."/deprecated-op2".get.deprecated + openApi.paths."/deprecated-op2".get.extensions."x-deprecated-message" == "This is deprecated controller" + + openApi.paths."/get/user/{id}".get.deprecated + openApi.paths."/get/user/{id}".get.extensions."x-deprecated-message" == "This is deprecated operation" + + openApi.paths."/get/user/{id}".get.deprecated + openApi.paths."/get/user/{id}".get.extensions."x-deprecated-message" == "This is deprecated operation" + openApi.paths."/get/user/{id}".get.parameters[1].name == 'name' + openApi.paths."/get/user/{id}".get.parameters[1].deprecated + + openApi.components.schemas.MyObj.deprecated + openApi.components.schemas.MyObj.extensions."x-deprecated-message" == "This class is deprecated." + + openApi.components.schemas.MyObj.properties.oldProp.deprecated + openApi.components.schemas.MyObj.properties.oldProp.extensions."x-deprecated-message" == "need to use another property." + } + + @RestoreSystemProperties + void "test enum extensions"() { + + setup: + System.setProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_GENERATOR_EXTENSIONS_ENABLED, "true") + + when: + buildBeanDefinition('test.MyBean', ''' +package test; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.swagger.v3.oas.annotations.media.Schema; + +@Controller +class HelloWorldApi { + + @Get("/get/user/{id}") + public MyEnum helloWorld(String id) { + return null; + } +} + +/** + * @deprecated deprecated enum. + */ +@Deprecated +enum MyEnum { + + @JsonProperty("12") + ENUM_VAR1((byte) 12), + /** + * Enum const description. + */ + @JsonProperty("13") + ENUM_VAR2((byte) 13), + /** + * @deprecated Enum const deprecated. + */ + @Deprecated + @JsonProperty("14") + ENUM_VAR3((byte) 14), + @Schema(name = "CUSTOM_ENUM_VAR_NAME") + @JsonProperty("15") + ENUM_VAR4((byte) 15), + @Deprecated + @Schema(name = "CUSTOM_ENUM_VAR_NAME2") + @JsonProperty("15") + ENUM_VAR5((byte) 16), + ; + + private final byte value; + + MyEnum(byte value) { + this.value = value; + } + + @JsonValue + public byte getValue() { + return value; + } +} + +@jakarta.inject.Singleton +class MyBean {} +''') + + then: + Utils.testReference != null + + when: + def openApi = Utils.testReference + def schema = openApi.components.schemas.MyEnum + + then: + + schema + schema.deprecated + schema.extensions."x-deprecated-message" == "deprecated enum." + schema.extensions."x-type" == "byte" + + def descriptions = (List) schema.extensions."x-enum-descriptions" + descriptions[0] == "" + descriptions[1] == "Enum const description." + descriptions[2] == "" + descriptions[3] == "" + + def enumVarNames = (List) schema.extensions."x-enum-varnames" + enumVarNames[0] == "ENUM_VAR1" + enumVarNames[1] == "ENUM_VAR2" + enumVarNames[2] == "ENUM_VAR3" + enumVarNames[3] == "CUSTOM_ENUM_VAR_NAME" + + def deprecatedList = (List) schema.extensions."x-deprecated" + deprecatedList.size() == 2 + deprecatedList[0] == "ENUM_VAR3" + deprecatedList[1] == "CUSTOM_ENUM_VAR_NAME2" + } + + @RestoreSystemProperties + void "test validation messages extensions"() { + + setup: + System.setProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_GENERATOR_EXTENSIONS_ENABLED, "true") + + when: + buildBeanDefinition('test.MyBean', ''' +package test; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +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.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; + +import java.util.List; + +@Controller +class HelloWorldApi { + + @Get("/get/user/{id}") + public MyObj helloWorld(String id) { + return null; + } +} + +class MyObj { + + @Size(min = 10, message = "size collection message") + public List<@Pattern(regexp = "rrr", message = "pattern mes") @NotNull(message = "not null mes") String> sizeCollectionProp; + @NotEmpty(message = "not empty collection message") + public List notEmptyCollectionProp; + @NotNull(message = "not null message") + public String notNullProp; + @NotEmpty(message = "not empty message") + public String notEmptyProp; + @NotBlank(message = "not blank message") + public String notBlankProp; + @Size(min = 10, message = "size string message") + public String sizeStringProp; + @Min(value = 10, message = "min int message") + public int minIntProp; + @Max(value = 10, message = "max int message") + public int maxIntProp; + @DecimalMin(value = "10", message = "min decimal message") + public double minDecimalProp; + @DecimalMax(value = "10", message = "max decimal message") + public double maxDecimalProp; + @Negative(message = "negative message") + public int negativeProp; + @NegativeOrZero(message = "negative or zero message") + public int negativeOrZeroProp; + @Positive(message = "positive message") + public int positiveProp; + @PositiveOrZero(message = "positive or zero message") + public int positiveOrZeroProp; + @Pattern(regexp = "reg", message = "pattern message") + public String patternProp; + @Email(regexp = "reg", message = "email message") + public String emailProp; +} + +@jakarta.inject.Singleton +class MyBean {} +''') + + then: + Utils.testReference != null + + when: + def openApi = Utils.testReference + def schema = openApi.components.schemas.MyObj + + then: + + schema + schema.properties.sizeCollectionProp.extensions."x-size-message" == "size collection message" + schema.properties.sizeCollectionProp.items.extensions."x-pattern-message" == "pattern mes" + schema.properties.sizeCollectionProp.items.extensions."x-not-null-message" == "not null mes" + schema.properties.notEmptyCollectionProp.extensions."x-size-message" == "not empty collection message" + schema.properties.notNullProp.extensions."x-not-null-message" == "not null message" + schema.properties.notEmptyProp.extensions."x-size-message" == "not empty message" + schema.properties.notBlankProp.extensions."x-size-message" == "not blank message" + schema.properties.sizeStringProp.extensions."x-size-message" == "size string message" + schema.properties.minIntProp.extensions."x-min-message" == "min int message" + schema.properties.maxIntProp.extensions."x-max-message" == "max int message" + schema.properties.minDecimalProp.extensions."x-min-message" == "min decimal message" + schema.properties.maxDecimalProp.extensions."x-max-message" == "max decimal message" + schema.properties.negativeProp.extensions."x-max-message" == "negative message" + schema.properties.negativeOrZeroProp.extensions."x-max-message" == "negative or zero message" + schema.properties.positiveProp.extensions."x-min-message" == "positive message" + schema.properties.positiveOrZeroProp.extensions."x-min-message" == "positive or zero message" + schema.properties.patternProp.extensions."x-pattern-message" == "pattern message" + schema.properties.emailProp.extensions."x-pattern-message" == "email message" + } +} diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerGroovySpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerGroovySpec.groovy index 114b5d2153..e2f97b3e09 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerGroovySpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerGroovySpec.groovy @@ -11,10 +11,12 @@ class OpenApiPojoControllerGroovySpec extends AbstractBeanDefinitionSpec { def setup() { System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_ENABLED) System.setProperty(Utils.ATTR_TEST_MODE, "true") + System.setProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_ENABLED, "false") } def cleanup() { System.clearProperty(Utils.ATTR_TEST_MODE) + System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_ENABLED) } @Issue("https://github.com/micronaut-projects/micronaut-openapi/issues/561") diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy index 3769c8e7f8..46a13071aa 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy @@ -11,11 +11,13 @@ class OpenApiPojoControllerKotlinSpec extends AbstractKotlinCompilerSpec { Utils.clean() System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_ENABLED) System.setProperty(Utils.ATTR_TEST_MODE, "true") + System.setProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_ENABLED, "false") } def cleanup() { Utils.clean() System.clearProperty(Utils.ATTR_TEST_MODE) + System.clearProperty(OpenApiConfigProperty.MICRONAUT_OPENAPI_ADOC_ENABLED) } void "test kotlin"() { diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy index 5572687b93..0eeb52e45a 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy @@ -6,7 +6,6 @@ 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.ArraySchema -import io.swagger.v3.oas.models.media.MapSchema import io.swagger.v3.oas.models.media.Schema import spock.lang.Issue diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy index bca503a1af..b92113e932 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy @@ -139,6 +139,7 @@ class MyBean {} dtoSchema.properties.test.readOnly dtoSchema.properties.test.description == 'this is description' dtoSchema.properties.test.externalDocs.description == 'external docs' + dtoSchema.properties.test.additionalProperties == true dtoSchema.properties.test.example dtoSchema.properties.test.example.stampWidth == 220 dtoSchema.properties.test.example.stampHeight == 85 @@ -160,7 +161,6 @@ class MyBean {} dtoSchema.properties.test.allOf[1].maxProperties == 100 dtoSchema.properties.test.allOf[1].multipleOf == 1.5 dtoSchema.properties.test.allOf[1].pattern == "ppp" - dtoSchema.properties.test.allOf[1].additionalProperties == true dtoSchema.properties.test.nullable dtoSchema.required.size() == 1 dtoSchema.required[0] == 'test' @@ -568,7 +568,7 @@ class MyBean {} dtoSchema.properties.test instanceof ComposedSchema dtoSchema.properties.test.allOf[0].$ref == '#/components/schemas/GlobalParams' - dtoSchema.properties.test.allOf[1].allOf[0].$ref == '#/components/schemas/LocalParams' + dtoSchema.properties.test.allOf[1].$ref == '#/components/schemas/LocalParams' dtoSchema.properties.test.not dtoSchema.properties.test.not.$ref == '#/components/schemas/LocalParams' dtoSchema.properties.test.oneOf[0].$ref == '#/components/schemas/LocalParams' diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy index 7418402ecd..4de5adcf69 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy @@ -92,25 +92,25 @@ class MyBean {} schema.properties.nestedPrimitivesList.items schema.properties.nestedPrimitivesList.items.nullable schema.properties.nestedPrimitivesList.items.allOf - schema.properties.nestedPrimitivesList.items.allOf.size() == 2 - schema.properties.nestedPrimitivesList.items.allOf[0].type == 'array' + schema.properties.nestedPrimitivesList.items.allOf.size() == 1 + schema.properties.nestedPrimitivesList.items.type == 'array' schema.properties.nestedPrimitivesList.items.allOf[0].items schema.properties.nestedPrimitivesList.items.allOf[0].items.type == 'string' schema.properties.nestedPrimitivesList.items.allOf[0].items.nullable - schema.properties.nestedPrimitivesList.items.allOf[1].maxItems == 10 + schema.properties.nestedPrimitivesList.items.allOf[0].maxItems == 10 schema.properties.nestedObjectsList.type == 'array' schema.properties.nestedObjectsList.items schema.properties.nestedObjectsList.items.nullable schema.properties.nestedObjectsList.items.allOf - schema.properties.nestedObjectsList.items.allOf.size() == 2 - schema.properties.nestedObjectsList.items.allOf[0].type == 'array' + schema.properties.nestedObjectsList.items.allOf.size() == 1 + schema.properties.nestedObjectsList.items.type == 'array' schema.properties.nestedObjectsList.items.allOf[0].items schema.properties.nestedObjectsList.items.allOf[0].items.nullable schema.properties.nestedObjectsList.items.allOf[0].items.allOf schema.properties.nestedObjectsList.items.allOf[0].items.allOf.size() == 1 schema.properties.nestedObjectsList.items.allOf[0].items.allOf[0].$ref == '#/components/schemas/ListItem' - schema.properties.nestedObjectsList.items.allOf[1].maxItems == 10 + schema.properties.nestedObjectsList.items.allOf[0].maxItems == 10 schema.properties.genObjectPrimitive.$ref == '#/components/schemas/GenObject_Size_min_10_NullableString_' schema.properties.genObjectPrimitive2.$ref == '#/components/schemas/GenObject_Size_min_10_NullableString_' @@ -141,7 +141,7 @@ class MyBean {} schema.properties.genObjectObj3.nullable schema.properties.genObjectObj3.allOf schema.properties.genObjectObj3.allOf.size() == 1 - schema.properties.genObjectObj3.allOf[0].$ref == '#/components/schemas/GenObject_GenObject_' + schema.properties.genObjectObj3.allOf[0].$ref == '#/components/schemas/GenObject_GenObject_ListItem__' schema.properties.nestedGenObjectPrimitive.nullable schema.properties.nestedGenObjectPrimitive.allOf @@ -152,7 +152,7 @@ class MyBean {} schema.properties.nestedGenObjectObj.nullable schema.properties.nestedGenObjectObj.allOf schema.properties.nestedGenObjectObj.allOf.size() == 2 - schema.properties.nestedGenObjectObj.allOf[0].$ref == '#/components/schemas/GenObject_List_GenObject__' + schema.properties.nestedGenObjectObj.allOf[0].$ref == '#/components/schemas/GenObject_List_GenObject_ListItem___' schema.properties.nestedGenObjectObj.allOf[1].maxItems == 10 } @@ -910,25 +910,25 @@ class MyBean {} schema.properties.nestedPrimitivesList.items schema.properties.nestedPrimitivesList.items.nullable schema.properties.nestedPrimitivesList.items.allOf - schema.properties.nestedPrimitivesList.items.allOf.size() == 2 - schema.properties.nestedPrimitivesList.items.allOf[0].type == 'array' + schema.properties.nestedPrimitivesList.items.allOf.size() == 1 + schema.properties.nestedPrimitivesList.items.type == 'array' schema.properties.nestedPrimitivesList.items.allOf[0].items schema.properties.nestedPrimitivesList.items.allOf[0].items.type == 'string' schema.properties.nestedPrimitivesList.items.allOf[0].items.nullable - schema.properties.nestedPrimitivesList.items.allOf[1].maxItems == 10 + schema.properties.nestedPrimitivesList.items.allOf[0].maxItems == 10 schema.properties.nestedObjectsList.type == 'array' schema.properties.nestedObjectsList.items schema.properties.nestedObjectsList.items.nullable schema.properties.nestedObjectsList.items.allOf - schema.properties.nestedObjectsList.items.allOf.size() == 2 - schema.properties.nestedObjectsList.items.allOf[0].type == 'array' + schema.properties.nestedObjectsList.items.allOf.size() == 1 + schema.properties.nestedObjectsList.items.type == 'array' schema.properties.nestedObjectsList.items.allOf[0].items schema.properties.nestedObjectsList.items.allOf[0].items.nullable schema.properties.nestedObjectsList.items.allOf[0].items.allOf schema.properties.nestedObjectsList.items.allOf[0].items.allOf.size() == 1 schema.properties.nestedObjectsList.items.allOf[0].items.allOf[0].$ref == '#/components/schemas/ListItem' - schema.properties.nestedObjectsList.items.allOf[1].maxItems == 10 + schema.properties.nestedObjectsList.items.allOf[0].maxItems == 10 schema.properties.genObjectPrimitive.$ref == '#/components/schemas/GenObjectSizemin10NullableString' schema.properties.genObjectPrimitive2.$ref == '#/components/schemas/GenObjectSizemin10NullableString' @@ -959,7 +959,7 @@ class MyBean {} schema.properties.genObjectObj3.nullable schema.properties.genObjectObj3.allOf schema.properties.genObjectObj3.allOf.size() == 1 - schema.properties.genObjectObj3.allOf[0].$ref == '#/components/schemas/GenObjectGenObject' + schema.properties.genObjectObj3.allOf[0].$ref == '#/components/schemas/GenObjectGenObjectListItem' schema.properties.nestedGenObjectPrimitive.nullable schema.properties.nestedGenObjectPrimitive.allOf @@ -970,7 +970,7 @@ class MyBean {} schema.properties.nestedGenObjectObj.nullable schema.properties.nestedGenObjectObj.allOf schema.properties.nestedGenObjectObj.allOf.size() == 2 - schema.properties.nestedGenObjectObj.allOf[0].$ref == '#/components/schemas/GenObjectListGenObject' + schema.properties.nestedGenObjectObj.allOf[0].$ref == '#/components/schemas/GenObjectListGenObjectListItem' schema.properties.nestedGenObjectObj.allOf[1].maxItems == 10 } @@ -1066,25 +1066,25 @@ class MyBean {} schema.properties.nestedPrimitivesList.items schema.properties.nestedPrimitivesList.items.nullable schema.properties.nestedPrimitivesList.items.allOf - schema.properties.nestedPrimitivesList.items.allOf.size() == 2 - schema.properties.nestedPrimitivesList.items.allOf[0].type == 'array' + schema.properties.nestedPrimitivesList.items.allOf.size() == 1 + schema.properties.nestedPrimitivesList.items.type == 'array' schema.properties.nestedPrimitivesList.items.allOf[0].items schema.properties.nestedPrimitivesList.items.allOf[0].items.type == 'string' schema.properties.nestedPrimitivesList.items.allOf[0].items.nullable - schema.properties.nestedPrimitivesList.items.allOf[1].maxItems == 10 + schema.properties.nestedPrimitivesList.items.allOf[0].maxItems == 10 schema.properties.nestedObjectsList.type == 'array' schema.properties.nestedObjectsList.items schema.properties.nestedObjectsList.items.nullable schema.properties.nestedObjectsList.items.allOf - schema.properties.nestedObjectsList.items.allOf.size() == 2 - schema.properties.nestedObjectsList.items.allOf[0].type == 'array' + schema.properties.nestedObjectsList.items.allOf.size() == 1 + schema.properties.nestedObjectsList.items.type == 'array' schema.properties.nestedObjectsList.items.allOf[0].items schema.properties.nestedObjectsList.items.allOf[0].items.nullable schema.properties.nestedObjectsList.items.allOf[0].items.allOf schema.properties.nestedObjectsList.items.allOf[0].items.allOf.size() == 1 schema.properties.nestedObjectsList.items.allOf[0].items.allOf[0].$ref == '#/components/schemas/ListItem' - schema.properties.nestedObjectsList.items.allOf[1].maxItems == 10 + schema.properties.nestedObjectsList.items.allOf[0].maxItems == 10 schema.properties.genObjectPrimitive.$ref == '#/components/schemas/GenObject<<