From fdad6a4b0d52b855dc50b3a76fc896b5e0765e6e Mon Sep 17 00:00:00 2001 From: altro3 Date: Sun, 4 Feb 2024 19:23:29 +0700 Subject: [PATCH 1/2] Add support constructor annotations --- .../visitor/AbstractOpenApiVisitor.java | 117 +++++++------ .../openapi/visitor/ElementUtils.java | 160 +++++++++++++++++- .../io/micronaut/openapi/visitor/Utils.java | 9 + .../OpenApiPojoControllerKotlinSpec.groovy | 116 +++++++++++++ 4 files changed, 339 insertions(+), 63 deletions(-) diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java index 957cdd97d7..b2d9deab73 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java @@ -138,9 +138,14 @@ import static io.micronaut.openapi.visitor.ConvertUtils.parseJsonString; import static io.micronaut.openapi.visitor.ConvertUtils.setDefaultValueObject; import static io.micronaut.openapi.visitor.ConvertUtils.toTupleSubMap; -import static io.micronaut.openapi.visitor.ElementUtils.isNotNullable; +import static io.micronaut.openapi.visitor.ElementUtils.findAnnotation; +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.isFileUpload; +import static io.micronaut.openapi.visitor.ElementUtils.isNotNullable; import static io.micronaut.openapi.visitor.ElementUtils.isNullable; +import static io.micronaut.openapi.visitor.ElementUtils.stringValue; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.expandProperties; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.resolvePlaceholders; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_FIELD_VISIBILITY_LEVEL; @@ -765,10 +770,10 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingEle AnnotationValue schemaAnnotationValue = null; if (definingElement != null) { - schemaAnnotationValue = definingElement.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + schemaAnnotationValue = getAnnotation(definingElement, io.swagger.v3.oas.annotations.media.Schema.class); } if (type != null && schemaAnnotationValue == null) { - schemaAnnotationValue = type.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + schemaAnnotationValue = getAnnotation(type, io.swagger.v3.oas.annotations.media.Schema.class); } boolean isSubstitudedType = false; if (schemaAnnotationValue != null) { @@ -1033,7 +1038,7 @@ private Schema processGenericAnnotations(Schema schema, ClassElement compo private void handleUnwrapped(VisitorContext context, Element element, ClassElement elementType, Schema parentSchema, AnnotationValue uw) { Map schemas = SchemaUtils.resolveSchemas(Utils.resolveOpenApi(context)); ClassElement customElementType = getCustomSchema(elementType.getName(), elementType.getTypeArguments(), context); - String schemaName = element.stringValue(io.swagger.v3.oas.annotations.media.Schema.class, "name") + String schemaName = stringValue(element, io.swagger.v3.oas.annotations.media.Schema.class, "name") .orElse(computeDefaultSchemaName(null, customElementType != null ? customElementType : elementType, elementType.getTypeArguments(), context, null)); Schema wrappedPropertySchema = schemas.get(schemaName); Map properties = wrappedPropertySchema.getProperties(); @@ -1074,12 +1079,12 @@ protected void processSchemaProperty(VisitorContext context, TypedElement elemen if (propertySchema == null) { return; } - AnnotationValue uw = element.getAnnotation(JsonUnwrapped.class); + AnnotationValue uw = getAnnotation(element, JsonUnwrapped.class); if (uw != null && uw.booleanValue("enabled").orElse(Boolean.TRUE)) { handleUnwrapped(context, element, elementType, parentSchema, uw); } else { // check schema required flag - AnnotationValue schemaAnnotationValue = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + AnnotationValue schemaAnnotationValue = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); Optional elementSchemaRequired = Optional.empty(); boolean isAutoRequiredMode = true; boolean isRequiredDefaultValueSet = false; @@ -1160,14 +1165,14 @@ private void addProperty(Schema parentSchema, String name, Schema property private String resolvePropertyName(Element element, Element classElement, Schema propertySchema) { String name = Optional.ofNullable(propertySchema.getName()).orElse(element.getName()); - if (element.hasAnnotation(io.swagger.v3.oas.annotations.media.Schema.class)) { - Optional nameFromSchema = element.stringValue(io.swagger.v3.oas.annotations.media.Schema.class, "name"); + if (isAnnotationPresent(element, io.swagger.v3.oas.annotations.media.Schema.class)) { + Optional nameFromSchema = stringValue(element, io.swagger.v3.oas.annotations.media.Schema.class, "name"); if (nameFromSchema.isPresent()) { return nameFromSchema.get(); } } - if (element.hasAnnotation(JsonProperty.class)) { - return element.stringValue(JsonProperty.class, "value").orElse(name); + if (isAnnotationPresent(element, JsonProperty.class)) { + return stringValue(element, JsonProperty.class, "value").orElse(name); } if (classElement != null && classElement.hasAnnotation(JsonNaming.class)) { // INVESTIGATE: "classValue" doesn't work in this case @@ -1202,7 +1207,7 @@ private String resolvePropertyName(Element element, Element classElement, Schema */ protected Schema bindSchemaForElement(VisitorContext context, TypedElement element, ClassElement elementType, Schema schemaToBind, @Nullable ClassElement jsonViewClass) { - AnnotationValue schemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + AnnotationValue schemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); Schema originalSchema = schemaToBind; if (originalSchema.get$ref() != null) { @@ -1228,7 +1233,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement el } } } - AnnotationValue arraySchemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class); + AnnotationValue arraySchemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.ArraySchema.class); if (arraySchemaAnn != null) { schemaToBind = bindArraySchemaAnnotationValue(context, element, schemaToBind, arraySchemaAnn, jsonViewClass); Optional schemaName = arraySchemaAnn.stringValue("name"); @@ -1255,11 +1260,11 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement el if (StringUtils.isNotEmpty(topLevelSchema.getDescription())) { notOnlyRef = true; } - if (element.isAnnotationPresent(Deprecated.class)) { + if (isAnnotationPresent(element, Deprecated.class)) { topLevelSchema.setDeprecated(true); notOnlyRef = true; } - final String defaultValue = element.stringValue(Bindable.class, "defaultValue").orElse(null); + final String defaultValue = stringValue(element, Bindable.class, "defaultValue").orElse(null); if (defaultValue != null && schemaToBind.getDefault() == null) { setDefaultValueObject(schemaToBind, defaultValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), true, context); notOnlyRef = true; @@ -1271,7 +1276,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement el topLevelSchema.setNullable(true); notOnlyRef = true; } - final String defaultJacksonValue = element.stringValue(JsonProperty.class, "defaultValue").orElse(null); + final String defaultJacksonValue = stringValue(element, JsonProperty.class, "defaultValue").orElse(null); if (defaultJacksonValue != null && schemaToBind.getDefault() == null) { setDefaultValueObject(topLevelSchema, defaultJacksonValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), false, context); notOnlyRef = true; @@ -1321,46 +1326,46 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e final boolean isIterableOrMap = elementType.isIterable() || elementType.isAssignable(Map.class); if (isIterableOrMap) { - if (element.isAnnotationPresent("javax.validation.constraints.NotEmpty$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotEmpty$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.NotEmpty$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List")) { schemaToBind.setMinItems(1); } - element.findAnnotation("javax.validation.constraints.Size$List") + findAnnotation(element, "javax.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("min") .ifPresent(schemaToBind::setMinItems))); - element.findAnnotation("jakarta.validation.constraints.Size$List") + findAnnotation(element, "jakarta.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("min") .ifPresent(schemaToBind::setMinItems))); - element.findAnnotation("javax.validation.constraints.Size$List") + findAnnotation(element, "javax.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("max") .ifPresent(schemaToBind::setMaxItems))); - element.findAnnotation("jakarta.validation.constraints.Size$List") + findAnnotation(element, "jakarta.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("max") .ifPresent(schemaToBind::setMaxItems))); } else { if (PrimitiveType.STRING.getCommonName().equals(schemaToBind.getType())) { - if (element.isAnnotationPresent("javax.validation.constraints.NotEmpty$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotEmpty$List") - || element.isAnnotationPresent("javax.validation.constraints.NotBlank$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotBlank$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.NotEmpty$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List") + || isAnnotationPresent(element, "javax.validation.constraints.NotBlank$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NotBlank$List")) { schemaToBind.setMinLength(1); } - element.findAnnotation("javax.validation.constraints.Size$List") + findAnnotation(element, "javax.validation.constraints.Size$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.intValue("min").ifPresent(schemaToBind::setMinLength); ann.intValue("max").ifPresent(schemaToBind::setMaxLength); } }); - element.findAnnotation("jakarta.validation.constraints.Size$List") + findAnnotation(element, "jakarta.validation.constraints.Size$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.intValue("min").ifPresent(schemaToBind::setMinLength); @@ -1369,33 +1374,33 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e }); } - if (element.isAnnotationPresent("javax.validation.constraints.Negative$List") - || element.isAnnotationPresent("jakarta.validation.constraints.Negative$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.Negative$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.Negative$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); schemaToBind.exclusiveMaximum(true); } - if (element.isAnnotationPresent("javax.validation.constraints.NegativeOrZero$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NegativeOrZero$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.NegativeOrZero$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NegativeOrZero$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); } - if (element.isAnnotationPresent("javax.validation.constraints.Positive$List") - || element.isAnnotationPresent("jakarta.validation.constraints.Positive$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.Positive$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.Positive$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); schemaToBind.exclusiveMinimum(true); } - if (element.isAnnotationPresent("javax.validation.constraints.PositiveOrZero$List") - || element.isAnnotationPresent("jakarta.validation.constraints.PositiveOrZero$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.PositiveOrZero$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.PositiveOrZero$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); } - element.findAnnotation("javax.validation.constraints.Min$List") + findAnnotation(element, "javax.validation.constraints.Min$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMinimum); } }); - element.findAnnotation("jakarta.validation.constraints.Min$List") + findAnnotation(element, "jakarta.validation.constraints.Min$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1403,14 +1408,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.Max$List") + findAnnotation(element, "javax.validation.constraints.Max$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMaximum); } }); - element.findAnnotation("jakarta.validation.constraints.Max$List") + findAnnotation(element, "jakarta.validation.constraints.Max$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1418,14 +1423,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.DecimalMin$List") + findAnnotation(element, "javax.validation.constraints.DecimalMin$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMinimum); } }); - element.findAnnotation("jakarta.validation.constraints.DecimalMin$List") + findAnnotation(element, "jakarta.validation.constraints.DecimalMin$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1433,14 +1438,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.DecimalMax$List") + findAnnotation(element, "javax.validation.constraints.DecimalMax$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMaximum); } }); - element.findAnnotation("jakarta.validation.constraints.DecimalMax$List") + findAnnotation(element, "jakarta.validation.constraints.DecimalMax$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1448,7 +1453,7 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.Email$List") + findAnnotation(element, "javax.validation.constraints.Email$List") .ifPresent(listAnn -> { schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); for (AnnotationValue ann : listAnn.getAnnotations("value")) { @@ -1456,7 +1461,7 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e .ifPresent(schemaToBind::setPattern); } }); - element.findAnnotation("jakarta.validation.constraints.Email$List") + findAnnotation(element, "jakarta.validation.constraints.Email$List") .ifPresent(listAnn -> { schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); for (AnnotationValue ann : listAnn.getAnnotations("value")) { @@ -1465,14 +1470,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.Pattern$List") + findAnnotation(element, "javax.validation.constraints.Pattern$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.stringValue("regexp") .ifPresent(schemaToBind::setPattern); } }); - element.findAnnotation("jakarta.validation.constraints.Pattern$List") + findAnnotation(element, "jakarta.validation.constraints.Pattern$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.stringValue("regexp") @@ -2263,15 +2268,15 @@ private List getEnumValues(EnumElement type, String schemaType, String s List enumValues = new ArrayList<>(); for (EnumConstantElement element : type.elements()) { - AnnotationValue schemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + AnnotationValue schemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); boolean isHidden = schemaAnn != null && schemaAnn.booleanValue("hidden").orElse(false); if (isHidden - || element.isAnnotationPresent(Hidden.class) - || element.isAnnotationPresent(JsonIgnore.class)) { + || isAnnotationPresent(element, Hidden.class) + || isAnnotationPresent(element, JsonIgnore.class)) { continue; } - AnnotationValue jsonProperty = element.getAnnotation(JsonProperty.class); + AnnotationValue jsonProperty = getAnnotation(element, JsonProperty.class); String jacksonValue = jsonProperty != null ? jsonProperty.stringValue("value").orElse(null) : null; if (StringUtils.hasText(jacksonValue)) { try { @@ -2520,10 +2525,10 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El } for (TypedElement publicField : publicFields) { - boolean isHidden = publicField.getAnnotationMetadata().booleanValue(io.swagger.v3.oas.annotations.media.Schema.class, "hidden").orElse(false); - AnnotationValue jsonAnySetterAnn = publicField.getAnnotation(JsonAnySetter.class); - if (publicField.isAnnotationPresent(JsonIgnore.class) - || publicField.isAnnotationPresent(Hidden.class) + boolean isHidden = getAnnotationMetadata(publicField).booleanValue(io.swagger.v3.oas.annotations.media.Schema.class, "hidden").orElse(false); + AnnotationValue jsonAnySetterAnn = getAnnotation(publicField, JsonAnySetter.class); + if (isAnnotationPresent(publicField, JsonIgnore.class) + || isAnnotationPresent(publicField, Hidden.class) || (jsonAnySetterAnn != null && jsonAnySetterAnn.booleanValue("enabled").orElse(true)) || isHidden) { continue; @@ -2576,7 +2581,7 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El } private boolean allowedByJsonView(TypedElement publicField, String[] classLvlJsonViewClasses, ClassElement jsonViewClassEl, VisitorContext context) { - String[] fieldJsonViewClasses = publicField.getAnnotationMetadata().stringValues(JsonView.class); + String[] fieldJsonViewClasses = getAnnotationMetadata(publicField).stringValues(JsonView.class); if (ArrayUtils.isEmpty(fieldJsonViewClasses)) { fieldJsonViewClasses = classLvlJsonViewClasses; } 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 ec9074c573..5387d80c55 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java @@ -17,6 +17,7 @@ import java.io.File; import java.io.InputStream; +import java.lang.annotation.Annotation; import java.nio.ByteBuffer; import java.security.Principal; import java.util.List; @@ -25,18 +26,23 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.Future; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.http.HttpRequest; import io.micronaut.http.multipart.FileUpload; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.MemberElement; +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.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -108,10 +114,6 @@ public static boolean isNullable(TypedElement element) { || element.getType().isOptional(); } - public static boolean isAnnotationPresent(Element element, String className) { - return element.findAnnotation(className).isPresent(); - } - /** * Checking if the type is file upload type. * @@ -217,12 +219,16 @@ public static boolean isIgnoredParameter(TypedElement parameter) { || parameter.isAnnotationPresent(Hidden.class) || parameter.isAnnotationPresent(JsonIgnore.class) || parameter.booleanValue(Parameter.class, "hidden").orElse(false) - || isAnnotationPresent(parameter, "io.micronaut.session.annotation.SessionValue") - || isAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttribute") - || isAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttributes") + || isParamAnnotationPresent(parameter, "io.micronaut.session.annotation.SessionValue") + || isParamAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttribute") + || isParamAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttributes") || isIgnoredParameterType(parameter.getType()); } + private static boolean isParamAnnotationPresent(Element element, String className) { + return element.findAnnotation(className).isPresent(); + } + public static boolean isIgnoredParameterType(ClassElement parameterType) { return parameterType == null || parameterType.isAssignable(Principal.class) @@ -250,4 +256,144 @@ public static boolean isIgnoredParameterType(ClassElement parameterType) { || parameterType.isAssignable("org.springframework.validation.Errors") ; } + + public static AnnotationMetadata getAnnotationMetadata(Element el) { + if (el == null) { + return AnnotationMetadata.EMPTY_METADATA; + } + if (el instanceof MemberElement memberEl) { + var propMetadata = memberEl.getAnnotationMetadata(); + AnnotationMetadata constructorMetadata = null; + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + constructorMetadata = constructorParam.getAnnotationMetadata(); + break; + } + } + } + if (constructorMetadata == null || constructorMetadata.isEmpty()) { + return propMetadata; + } + return new AnnotationMetadataHierarchy(true, new AnnotationMetadata[] {propMetadata, constructorMetadata}); + } + return el.getAnnotationMetadata(); + } + + public static Optional> findAnnotation(Element el, String annName) { + if (el == null) { + return Optional.empty(); + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.findAnnotation(annName); + if (result.isPresent()) { + return result; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.findAnnotation(annName); + } + } + } + return Optional.empty(); + } + return el.findAnnotation(annName); + } + + public static boolean isAnnotationPresent(Element el, Class annClass) { + return isAnnotationPresent(el, annClass.getName()); + } + + public static boolean isAnnotationPresent(Element el, String annName) { + if (el == null) { + return false; + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.isAnnotationPresent(annName); + if (result) { + return true; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.isAnnotationPresent(annName); + } + } + } + return false; + } + return el.isAnnotationPresent(annName); + } + + public static Optional stringValue(Element el, Class annClass, String member) { + if (el == null) { + return Optional.empty(); + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.stringValue(annClass, member); + if (result.isPresent()) { + return result; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.stringValue(annClass, member); + } + } + } + return result; + } + return el.stringValue(annClass, member); + } + + public static AnnotationValue getAnnotation(Element el, Class annClass) { + return getAnnotation(el, annClass.getName()); + } + + public static AnnotationValue getAnnotation(Element el, String annName) { + if (el == null) { + return null; + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.getAnnotation(annName); + if (result != null) { + return result; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.getAnnotation(annName); + } + } + } + return result; + } + return el.getAnnotation(annName); + } + + private static MethodElement getCreatorConstructor(ClassElement classEl) { + + var cachedConstructor = Utils.getCreatorConstructorsCache().get(classEl.getName()); + if (cachedConstructor != null) { + return cachedConstructor; + } + + var creatorConstructor = classEl.getPrimaryConstructor().orElse(null); + var constructors = classEl.getAccessibleConstructors(); + if (constructors.size() > 1) { + for (var constructor : constructors) { + if (constructor.isDeclaredAnnotationPresent(JsonCreator.class)) { + creatorConstructor = constructor; + } + } + } + Utils.getCreatorConstructorsCache().put(classEl.getName(), creatorConstructor); + return creatorConstructor; + } } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java index b3caa9a5f1..f9859c9af0 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -36,6 +37,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.value.PropertyResolver; import io.micronaut.http.MediaType; +import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.openapi.javadoc.JavadocParser; import io.micronaut.openapi.visitor.group.EndpointInfo; @@ -60,6 +62,8 @@ public final class Utils { public static final List DEFAULT_MEDIA_TYPES = Collections.singletonList(MediaType.APPLICATION_JSON_TYPE); + private static Map creatorConstructorsCache = new HashMap<>(); + private static Set allKnownVersions; private static Set allKnownGroups; private static Map> endpointInfos; @@ -344,6 +348,10 @@ public static void setIncludedClassesGroupsExcluded(Map> in Utils.includedClassesGroupsExcluded = includedClassesGroupsExcluded; } + public static Map getCreatorConstructorsCache() { + return creatorConstructorsCache; + } + public static void clean() { openApis = null; endpointInfos = null; @@ -358,5 +366,6 @@ public static void clean() { testFileName = null; testYamlReference = null; testJsonReference = null; + creatorConstructorsCache = new HashMap<>(); } } 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 60889ef222..ca59a448de 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy @@ -220,4 +220,120 @@ class MyBean {} schema.required[0] == 'color' schema.required[1] == 'propertyClass' } + + void "test kotlin constructor annotations"() { + + when: + buildBeanDefinition('test.MyBean', ''' +package test + +import com.fasterxml.jackson.annotation.* +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonValue +import io.micronaut.core.annotation.Nullable +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Put +import io.micronaut.serde.annotation.Serdeable +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.* +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import reactor.core.publisher.Mono + +@Controller +class HelloController { + + @Put("/sendModelWithDiscriminator") + fun sendModelWithDiscriminator( + @Body @NotNull @Valid animal: Animal + ): Mono = Mono.empty() +} + +/** + * Animal + * + * @param color + * @param propertyClass + */ +@Serdeable +@JsonPropertyOrder( + Animal.JSON_PROPERTY_PROPERTY_CLASS, + Animal.JSON_PROPERTY_COLOR +) +open class Animal ( + @Nullable + @Schema(name = "color", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @JsonProperty(JSON_PROPERTY_COLOR) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var color: ColorEnum? = null, + @Size(max = 50) + @Nullable + @Schema(name = "class", requiredMode = Schema.RequiredMode.NOT_REQUIRED) +// @JsonProperty(JSON_PROPERTY_PROPERTY_CLASS) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var propertyClass: String? = null, +) { + + companion object { + + const val JSON_PROPERTY_PROPERTY_CLASS = "class" + const val JSON_PROPERTY_COLOR = "color" + } +} + +@Serdeable +enum class ColorEnum ( + @get:JsonValue val value: String +) { + + @JsonProperty("red") + RED("red"), + @JsonProperty("blue") + BLUE("blue"), + @JsonProperty("green") + GREEN("green"), + @JsonProperty("light-blue") + LIGHT_BLUE("light-blue"), + @JsonProperty("dark-green") + DARK_GREEN("dark-green"); + + override fun toString(): String { + return value + } + + companion object { + + @JvmField + val VALUE_MAPPING = entries.associateBy { it.value } + + @JsonCreator + @JvmStatic + fun fromValue(value: String): ColorEnum { + require(VALUE_MAPPING.containsKey(value)) { "Unexpected value '$value'" } + return VALUE_MAPPING[value]!! + } + } +} + +@jakarta.inject.Singleton +class MyBean {} +''') + then: "the state is correct" + Utils.testReference != null + + when: "The OpenAPI is retrieved" + OpenAPI openAPI = Utils.testReference + Schema schema = openAPI.components.schemas.Animal + + then: "the components are valid" + schema + schema.properties.size() == 2 + schema.properties.class + schema.properties.class.maxLength == 50 + } } From bfddeaf528743fb404de8f75ff0ba6be1bf6369e Mon Sep 17 00:00:00 2001 From: altro3 Date: Mon, 5 Feb 2024 13:53:39 +0700 Subject: [PATCH 2/2] Remove default ApiResponse (with code '0') from generated swagger annotations --- gradle/libs.versions.toml | 2 + .../AbstractMicronautJavaCodegen.java | 8 + .../AbstractMicronautKotlinCodegen.java | 8 + openapi/build.gradle | 1 + .../AbstractOpenApiEndpointVisitor.java | 36 +++-- .../visitor/AbstractOpenApiVisitor.java | 2 +- .../openapi/visitor/ElementUtils.java | 15 +- .../OpenApiControllerVisitorSpec.groovy | 46 ++++++ .../OpenApiPojoControllerKotlinSpec.groovy | 4 +- .../visitor/OpenApiSchemaFieldSpec.groovy | 139 ++++++++++++++++++ settings.gradle | 1 + 11 files changed, 239 insertions(+), 23 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a1efb0985..1ea71cc94e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,7 @@ micronaut-data = "4.6.2" micronaut-test = "4.2.1" micronaut-kotlin = "4.2.0" micronaut-logging = "1.2.3" +micronaut-session = "4.2.0" micronaut-docs = "2.0.0" [libraries] @@ -49,6 +50,7 @@ micronaut-reactor = { module = "io.micronaut.reactor:micronaut-reactor-bom", ver micronaut-gradle-plugin = { module = "io.micronaut.gradle:micronaut-minimal-plugin", version.ref = "micronaut-gradle-plugin"} micronaut-groovy = { module = "io.micronaut.groovy:micronaut-groovy-bom", version.ref = "micronaut-groovy" } micronaut-validation = { module = "io.micronaut.validation:micronaut-validation-bom", version.ref = "micronaut-validation" } +micronaut-session = { module = "io.micronaut.session:micronaut-session-bom", version.ref = "micronaut-session" } micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version.ref = "micronaut-data" } micronaut-test = { module = "io.micronaut.test:micronaut-test-bom", version.ref = "micronaut-test" } micronaut-kotlin = { module = "io.micronaut.kotlin:micronaut-kotlin-bom", version.ref = "micronaut-kotlin" } diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java index ba9834cd18..f2a9a56448 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java @@ -841,6 +841,14 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation param.vendorExtensions.put("hasMultipleParams", hasMultipleParams); } op.vendorExtensions.put("originReturnProperty", op.returnProperty); + if (op.responses != null && !op.responses.isEmpty()) { + for (var resp : op.responses) { + if (resp.isDefault) { + resp.code = "default"; + } + } + } + processParametersWithAdditionalMappings(op.allParams, op.imports); processWithResponseBodyMapping(op); processOperationWithResponseWrappers(op); diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java index 3fc78b59ff..35716f629b 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java @@ -938,6 +938,14 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation param.vendorExtensions.put("hasMultipleParams", hasMultipleParams); } op.vendorExtensions.put("originReturnProperty", op.returnProperty); + if (op.responses != null && !op.responses.isEmpty()) { + for (var resp : op.responses) { + if (resp.isDefault) { + resp.code = "default"; + } + } + } + processParametersWithAdditionalMappings(op.allParams, op.imports); processWithResponseBodyMapping(op); processOperationWithResponseWrappers(op); diff --git a/openapi/build.gradle b/openapi/build.gradle index c576712851..257d6c5d00 100644 --- a/openapi/build.gradle +++ b/openapi/build.gradle @@ -22,6 +22,7 @@ dependencies { // this dependency needs to be updated manually. It's used by html2md api(libs.managed.jsoup) + testImplementation(mnSession.micronaut.session) testImplementation(mn.micronaut.management) testImplementation(mn.micronaut.inject.kotlin.test) testImplementation(mn.micronaut.inject.groovy.test) 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 07b517c4be..c33691de63 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java @@ -883,26 +883,18 @@ private Parameter processMethodParameterAnnotation(VisitorContext context, io.sw newParameter.setExplode(true); } } else if (parameter.isAnnotationPresent(Header.class)) { - String headerName = parameter.stringValue(Header.class, "name") - .orElse(parameter.stringValue(Header.class) - .orElseGet(() -> NameUtils.hyphenate(parameterName))); - - if (isIgnoredHeader(headerName)) { + var headerName = getHeaderName(parameter, parameterName); + if (headerName == null) { return null; } - newParameter = new HeaderParameter(); newParameter.setName(headerName); } else if (parameter.isAnnotationPresent(Headers.class)) { List> headerAnnotations = parameter.getAnnotationValuesByType(Header.class); if (CollectionUtils.isNotEmpty(headerAnnotations)) { - var headerAnn = headerAnnotations.get(0); - var headerName = headerAnn.stringValue("name") - .orElse(headerAnn.stringValue() - .orElseGet(() -> NameUtils.hyphenate(parameterName))); - - if (isIgnoredHeader(headerName)) { + var headerName = getHeaderName(parameter, parameterName); + if (headerName == null) { return null; } newParameter = new HeaderParameter(); @@ -1067,6 +1059,26 @@ private Parameter processMethodParameterAnnotation(VisitorContext context, io.sw return newParameter; } + private String getHeaderName(TypedElement parameter, String parameterName) { + // skip params like this: @Header Map + if (isIgnoredParameter(parameter)) { + return null; + } + String headerName = parameter.stringValue(Header.class, "name") + .orElse(parameter.stringValue(Header.class) + .orElseGet(() -> NameUtils.hyphenate(parameterName))); + + if (isIgnoredHeader(headerName)) { + return null; + } + + return headerName; + } + + private boolean isIgnoredHeaderParameter(TypedElement parameter) { + return parameter.getType().isAssignable(Map.class); + } + private void processBody(VisitorContext context, OpenAPI openAPI, io.swagger.v3.oas.models.Operation swaggerOperation, JavadocDescription javadocDescription, boolean permitsRequestBody, List consumesMediaTypes, TypedElement parameter, diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java index b2d9deab73..f4cd9574af 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java @@ -897,7 +897,7 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingEle schema = getPrimitiveType(type, typeName); } else if (!isArray && primitiveType != null) { schema = primitiveType.createProperty(); - } else if (type.isAssignable(Map.class.getName())) { + } else if (type.isAssignable(Map.class)) { schema = new MapSchema(); if (CollectionUtils.isEmpty(typeArgs)) { schema.setAdditionalProperties(true); 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 5387d80c55..90641021b7 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java @@ -21,6 +21,7 @@ import java.nio.ByteBuffer; import java.security.Principal; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.CompletionStage; @@ -30,6 +31,7 @@ import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.http.HttpRequest; +import io.micronaut.http.annotation.Header; import io.micronaut.http.multipart.FileUpload; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; @@ -218,17 +220,14 @@ public static boolean isIgnoredParameter(TypedElement parameter) { return isHidden || parameter.isAnnotationPresent(Hidden.class) || parameter.isAnnotationPresent(JsonIgnore.class) + || parameter.isAnnotationPresent(Header.class) && parameter.getType().isAssignable(Map.class) || parameter.booleanValue(Parameter.class, "hidden").orElse(false) - || isParamAnnotationPresent(parameter, "io.micronaut.session.annotation.SessionValue") - || isParamAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttribute") - || isParamAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttributes") + || parameter.hasAnnotation("io.micronaut.session.annotation.SessionValue") + || parameter.hasAnnotation("org.springframework.web.bind.annotation.SessionAttribute") + || parameter.hasAnnotation("org.springframework.web.bind.annotation.SessionAttributes") || isIgnoredParameterType(parameter.getType()); } - private static boolean isParamAnnotationPresent(Element element, String className) { - return element.findAnnotation(className).isPresent(); - } - public static boolean isIgnoredParameterType(ClassElement parameterType) { return parameterType == null || parameterType.isAssignable(Principal.class) @@ -276,7 +275,7 @@ public static AnnotationMetadata getAnnotationMetadata(Element el) { if (constructorMetadata == null || constructorMetadata.isEmpty()) { return propMetadata; } - return new AnnotationMetadataHierarchy(true, new AnnotationMetadata[] {propMetadata, constructorMetadata}); + return new AnnotationMetadataHierarchy(true, propMetadata, constructorMetadata); } return el.getAnnotationMetadata(); } diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiControllerVisitorSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiControllerVisitorSpec.groovy index 6d638dbd0a..65380a874f 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiControllerVisitorSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiControllerVisitorSpec.groovy @@ -11,6 +11,52 @@ import spock.lang.Issue class OpenApiControllerVisitorSpec extends AbstractOpenApiTypeElementSpec { + void "test some ignored parameters"() { + + given: + buildBeanDefinition('test.MyBean', ''' +package test; + +import java.util.Map; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; +import io.micronaut.session.annotation.SessionValue; + +@Controller +class ControllerThree { + + @Get("/myObj") + @SessionValue("myAttr") + MyObj myMethod( + @Header("my-header") String header, + @Header Map allHeaders, + @SessionValue @Nullable MyObj myObj) { + return null; + } +} + +class MyObj { + + public String myProp; +} + +@jakarta.inject.Singleton +class MyBean {} +''') + when: + def operation = Utils.testReference?.paths?."/myObj"?.get + + then: + operation + operation.parameters + operation.parameters.size() == 1 + operation.parameters[0].name == "my-header" + operation.parameters[0].in == "header" + } + void "test hidden endpoint with inheritance"() { given: 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 ca59a448de..16fcf2dd1b 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy @@ -274,7 +274,7 @@ open class Animal ( @Size(max = 50) @Nullable @Schema(name = "class", requiredMode = Schema.RequiredMode.NOT_REQUIRED) -// @JsonProperty(JSON_PROPERTY_PROPERTY_CLASS) + @JsonProperty(JSON_PROPERTY_PROPERTY_CLASS) @JsonInclude(JsonInclude.Include.USE_DEFAULTS) open var propertyClass: String? = null, ) { @@ -327,7 +327,7 @@ class MyBean {} Utils.testReference != null when: "The OpenAPI is retrieved" - OpenAPI openAPI = Utils.testReference + def openAPI = Utils.testReference Schema schema = openAPI.components.schemas.Animal then: "the components are valid" 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 6016cb0d88..edc313d97a 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy @@ -929,4 +929,143 @@ class MyBean {} dtoSchema.properties.zone_id.type == 'string' } + void "test annotations on constructor parameters level"() { + + when: + buildBeanDefinition('test.MyBean', ''' +package test; + +import java.math.BigDecimal; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Put; +import io.micronaut.serde.annotation.Serdeable; +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import reactor.core.publisher.Mono; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@Controller +class HelloController { + + @Put("/sendModelWithDiscriminator") + Mono sendModelWithDiscriminator( + @Body @NotNull @Valid Animal animal + ) { + return Mono.empty(); + } +} + +@Serdeable +@JsonIgnoreProperties( + value = "class", // ignore manually set class, it will be automatically generated by Jackson during serialization + allowSetters = true // allows the class to be set during deserialization +) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "class", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = Bird.class, name = "ave"), +}) +class Animal { + + @JsonProperty("class") + protected String propertyClass; + @Schema(name = "color", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @Nullable + private ColorEnum color; + + public String getPropertyClass() { + return propertyClass; + } + + public void setPropertyClass(String propertyClass) { + this.propertyClass = propertyClass; + } + + public ColorEnum getColor() { + return color; + } + + public void setColor(ColorEnum color) { + this.color = color; + } +} + +@Serdeable +class Bird extends Animal { + + private Integer numWings; + @DecimalMax("123.78") + private BigDecimal beakLength; + private String featherDescription; + + Bird( + @Min(10) Integer numWings, + @JsonProperty("myLength") BigDecimal beakLength, + String featherDescription) { + + } + + public Integer getNumWings() { + return numWings; + } + + public void setNumWings(Integer numWings) { + this.numWings = numWings; + } + + public BigDecimal getBeakLength() { + return beakLength; + } + + public void setBeakLength(BigDecimal beakLength) { + this.beakLength = beakLength; + } + + public String getFeatherDescription() { + return featherDescription; + } + + public void setFeatherDescription(String featherDescription) { + this.featherDescription = featherDescription; + } +} + +@Serdeable +enum ColorEnum { + + @JsonProperty("red") + RED +} + +@jakarta.inject.Singleton +class MyBean {} +''') + then: "the state is correct" + Utils.testReference != null + + when: "The OpenAPI is retrieved" + def openApi = Utils.testReference + def schemas = openApi.components.schemas + + then: "the components are valid" + schemas.Animal + schemas.Bird + schemas.ColorEnum + + !schemas.Bird.allOf[1].properties.beakLength + schemas.Bird.allOf[1].properties.myLength + schemas.Bird.allOf[1].properties.myLength.maximum == 123.78 + schemas.Bird.allOf[1].properties.numWings.minimum == 10 + } + } diff --git a/settings.gradle b/settings.gradle index 3a88eb13bc..8f5df090ef 100644 --- a/settings.gradle +++ b/settings.gradle @@ -48,4 +48,5 @@ micronautBuild { importMicronautCatalog("micronaut-validation") importMicronautCatalog("micronaut-data") importMicronautCatalog("micronaut-kotlin") + importMicronautCatalog("micronaut-session") }