From ab05120d9961fc243fa5b1a16299e98178bb458c Mon Sep 17 00:00:00 2001 From: altro3 Date: Fri, 30 Aug 2024 14:51:18 +0700 Subject: [PATCH] Add support guava optional, primitive Optionals, AtomicRefernce See: https://github.com/swagger-api/swagger-core/issues/4717 --- gradle/libs.versions.toml | 2 + openapi/build.gradle | 1 + .../openapi/visitor/ElementUtils.java | 171 +++--- .../visitor/SchemaDefinitionUtils.java | 506 +++++++++--------- .../OpenApiControllerVisitorSpec.groovy | 93 +++- 5 files changed, 440 insertions(+), 333 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c234c5f820..56030e7493 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ swagger-parser = "1.0.71" swagger-parser-v3 = "2.1.22" javaparser = "3.26.1" commons-codec = "1.17.1" +guava = "33.3.0-jre" micronaut = "4.6.3" micronaut-platform = "4.5.1" @@ -77,6 +78,7 @@ jdt-annotation = { module = "org.eclipse.jdt:org.eclipse.jdt.annotation", versio android-annotation = { module = "androidx.annotation:annotation", version.ref = "android-annotation" } javaparser = { module = "com.github.javaparser:javaparser-symbol-solver-core", version.ref = "javaparser" } commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } openapi-generator = { module = "org.openapitools:openapi-generator", version.ref = "openapi-generator" } swagger-parser = { module = "io.swagger:swagger-parser", version.ref = "swagger-parser" } diff --git a/openapi/build.gradle b/openapi/build.gradle index 15a537684e..a047f18438 100644 --- a/openapi/build.gradle +++ b/openapi/build.gradle @@ -41,6 +41,7 @@ dependencies { testImplementation(libs.jdt.annotation) testImplementation(libs.android.annotation) testImplementation(libs.spotbugs.annotations) + testImplementation(libs.guava) testImplementation(mn.kotlinx.coroutines.reactor) } 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 9f41ad30cd..86d142d6a0 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java @@ -45,9 +45,13 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.concurrent.Callable; import java.util.concurrent.CompletionStage; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; import static io.micronaut.openapi.visitor.ConfigUtils.isJsonViewEnabled; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_HIDDEN; @@ -63,33 +67,35 @@ public final class ElementUtils { public static final AnnotationValue[] EMPTY_ANNOTATION_VALUES_ARRAY = new AnnotationValue[0]; public static final List CONTAINER_TYPES = List.of( - Optional.class.getName(), - Future.class.getName(), - Callable.class.getName(), - CompletionStage.class.getName(), - "org.reactivestreams.Publisher", - "io.reactivex.Single", - "io.reactivex.Observable", - "io.reactivex.Maybe", - "io.reactivex.rxjava3.core.Single", - "io.reactivex.rxjava3.core.Observable", - "io.reactivex.rxjava3.core.Maybe", - "kotlinx.coroutines.flow.Flow", - "org.springframework.web.context.request.async.DeferredResult" + AtomicReference.class.getName(), + "com.google.common.base.Optional", + Optional.class.getName(), + Future.class.getName(), + Callable.class.getName(), + CompletionStage.class.getName(), + "org.reactivestreams.Publisher", + "io.reactivex.Single", + "io.reactivex.Observable", + "io.reactivex.Maybe", + "io.reactivex.rxjava3.core.Single", + "io.reactivex.rxjava3.core.Observable", + "io.reactivex.rxjava3.core.Maybe", + "kotlinx.coroutines.flow.Flow", + "org.springframework.web.context.request.async.DeferredResult" ); public static final List FILE_TYPES = List.of( - // this class from micronaut-http-server - "io.micronaut.http.server.types.files.FileCustomizableResponseType", - File.class.getName(), - InputStream.class.getName(), - ByteBuffer.class.getName() + // this class from micronaut-http-server + "io.micronaut.http.server.types.files.FileCustomizableResponseType", + File.class.getName(), + InputStream.class.getName(), + ByteBuffer.class.getName() ); public static final List VOID_TYPES = List.of( - void.class.getName(), - Void.class.getName(), - "kotlin.Unit" + void.class.getName(), + Void.class.getName(), + "kotlin.Unit" ); private ElementUtils() { @@ -97,16 +103,16 @@ private ElementUtils() { public static boolean isSingleResponseType(ClassElement returnType) { return (returnType.isAssignable("io.reactivex.Single") - || returnType.isAssignable("io.reactivex.rxjava3.core.Single") - || returnType.isAssignable("org.reactivestreams.Publisher")) - && returnType.getFirstTypeArgument().isPresent() - && isResponseType(returnType.getFirstTypeArgument().orElse(null)); + || returnType.isAssignable("io.reactivex.rxjava3.core.Single") + || returnType.isAssignable("org.reactivestreams.Publisher")) + && returnType.getFirstTypeArgument().isPresent() + && isResponseType(returnType.getFirstTypeArgument().orElse(null)); } public static boolean isResponseType(ClassElement returnType) { return returnType != null - && (returnType.isAssignable(HttpResponse.class) - || returnType.isAssignable("org.springframework.http.HttpEntity")); + && (returnType.isAssignable(HttpResponse.class) + || returnType.isAssignable("org.springframework.http.HttpEntity")); } /** @@ -116,8 +122,18 @@ public static boolean isResponseType(ClassElement returnType) { * @return true if element is nullable, false - otherwise. */ public static boolean isNullable(TypedElement element) { + + var type = element.getType(); + return element.isNullable() - || element.getType().isOptional(); + || type.isOptional() + || type.isAssignable(Optional.class) + || type.isAssignable("com.google.common.base.Optional") + || type.isAssignable(AtomicReference.class) + || type.isAssignable(OptionalInt.class) + || type.isAssignable(OptionalLong.class) + || type.isAssignable(OptionalDouble.class) + ; } /** @@ -135,11 +151,11 @@ public static boolean isFileUpload(ClassElement type) { } String typeName = type.getName(); return type.isAssignable(FileUpload.class) - || "io.micronaut.http.multipart.StreamingFileUpload".equals(typeName) - || "io.micronaut.http.multipart.CompletedFileUpload".equals(typeName) - || "io.micronaut.http.multipart.CompletedPart".equals(typeName) - || "io.micronaut.http.multipart.PartData".equals(typeName) - || "org.springframework.web.multipart.MultipartFile".equals(typeName); + || "io.micronaut.http.multipart.StreamingFileUpload".equals(typeName) + || "io.micronaut.http.multipart.CompletedFileUpload".equals(typeName) + || "io.micronaut.http.multipart.CompletedPart".equals(typeName) + || "io.micronaut.http.multipart.PartData".equals(typeName) + || "org.springframework.web.multipart.MultipartFile".equals(typeName); } /** @@ -150,13 +166,13 @@ public static boolean isFileUpload(ClassElement type) { */ public static boolean isNotNullable(Element element) { return element.isAnnotationPresent("javax.validation.constraints.NotNull$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotNull$List") - || element.isAnnotationPresent("javax.validation.constraints.NotBlank$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotBlank$List") - || element.isAnnotationPresent("javax.validation.constraints.NotEmpty$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotEmpty$List") - || element.isNonNull() - || element.booleanValue(JsonProperty.class, "required").orElse(false); + || element.isAnnotationPresent("jakarta.validation.constraints.NotNull$List") + || element.isAnnotationPresent("javax.validation.constraints.NotBlank$List") + || element.isAnnotationPresent("jakarta.validation.constraints.NotBlank$List") + || element.isAnnotationPresent("javax.validation.constraints.NotEmpty$List") + || element.isAnnotationPresent("jakarta.validation.constraints.NotEmpty$List") + || element.isNonNull() + || element.booleanValue(JsonProperty.class, "required").orElse(false); } /** @@ -197,8 +213,8 @@ public static boolean isVoid(ClassElement type) { */ public static boolean isReactiveAndVoid(ClassElement type) { return type.isAssignable("io.reactivex.Completable") - || type.isAssignable("io.reactivex.rxjava3.core.Completable") - || (isContainerType(type) && type.getFirstTypeArgument().isPresent() && isVoid(type.getFirstTypeArgument().get())); + || type.isAssignable("io.reactivex.rxjava3.core.Completable") + || (isContainerType(type) && type.getFirstTypeArgument().isPresent() && isVoid(type.getFirstTypeArgument().get())); } private static boolean findAnyAssignable(ClassElement type, List typeNames) { @@ -216,42 +232,42 @@ public static boolean isIgnoredParameter(TypedElement parameter) { boolean isHidden = schemaAnn != null && schemaAnn.booleanValue(PROP_HIDDEN).orElse(false); return isHidden - || parameter.isAnnotationPresent(Hidden.class) - || parameter.isAnnotationPresent(JsonIgnore.class) - || parameter.isAnnotationPresent(Header.class) && parameter.getType().isAssignable(Map.class) - || parameter.booleanValue(Parameter.class, PROP_HIDDEN).orElse(false) - || 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()); + || parameter.isAnnotationPresent(Hidden.class) + || parameter.isAnnotationPresent(JsonIgnore.class) + || parameter.isAnnotationPresent(Header.class) && parameter.getType().isAssignable(Map.class) + || parameter.booleanValue(Parameter.class, PROP_HIDDEN).orElse(false) + || 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()); } public static boolean isIgnoredParameterType(ClassElement parameterType) { return parameterType == null - || parameterType.isAssignable(Principal.class) - || parameterType.isAssignable("io.micronaut.session.Session") - || parameterType.isAssignable("io.micronaut.security.authentication.Authentication") - || parameterType.isAssignable("io.micronaut.http.HttpHeaders") - || parameterType.isAssignable("kotlin.coroutines.Continuation") - || parameterType.isAssignable(HttpRequest.class) - || parameterType.isAssignable("io.micronaut.http.BasicAuth") - // servlet API - || parameterType.isAssignable("jakarta.servlet.http.HttpServletRequest") - || parameterType.isAssignable("jakarta.servlet.http.HttpServletResponse") - || parameterType.isAssignable("jakarta.servlet.http.HttpSession") - || parameterType.isAssignable("jakarta.servlet.http.PushBuilder") - // spring - || parameterType.isAssignable("java.io.Reader") - || parameterType.isAssignable("java.io.OutputStream") - || parameterType.isAssignable("java.io.Writer") - || parameterType.isAssignable("org.springframework.web.util.UriComponentsBuilder") - || parameterType.isAssignable("org.springframework.web.bind.support.SessionStatus") - || parameterType.isAssignable("org.springframework.web.context.request.RequestAttributes") - || parameterType.isAssignable("org.springframework.http.HttpEntity") - || parameterType.isAssignable("org.springframework.http.HttpMethod") - || parameterType.isAssignable("org.springframework.validation.BindingResult") - || parameterType.isAssignable("org.springframework.validation.Errors") - ; + || parameterType.isAssignable(Principal.class) + || parameterType.isAssignable("io.micronaut.session.Session") + || parameterType.isAssignable("io.micronaut.security.authentication.Authentication") + || parameterType.isAssignable("io.micronaut.http.HttpHeaders") + || parameterType.isAssignable("kotlin.coroutines.Continuation") + || parameterType.isAssignable(HttpRequest.class) + || parameterType.isAssignable("io.micronaut.http.BasicAuth") + // servlet API + || parameterType.isAssignable("jakarta.servlet.http.HttpServletRequest") + || parameterType.isAssignable("jakarta.servlet.http.HttpServletResponse") + || parameterType.isAssignable("jakarta.servlet.http.HttpSession") + || parameterType.isAssignable("jakarta.servlet.http.PushBuilder") + // spring + || parameterType.isAssignable("java.io.Reader") + || parameterType.isAssignable("java.io.OutputStream") + || parameterType.isAssignable("java.io.Writer") + || parameterType.isAssignable("org.springframework.web.util.UriComponentsBuilder") + || parameterType.isAssignable("org.springframework.web.bind.support.SessionStatus") + || parameterType.isAssignable("org.springframework.web.context.request.RequestAttributes") + || parameterType.isAssignable("org.springframework.http.HttpEntity") + || parameterType.isAssignable("org.springframework.http.HttpMethod") + || parameterType.isAssignable("org.springframework.validation.BindingResult") + || parameterType.isAssignable("org.springframework.validation.Errors") + ; } public static AnnotationMetadata getAnnotationMetadata(Element el) { @@ -407,4 +423,11 @@ public static ClassElement getJsonViewClass(Element element, VisitorContext cont } return null; } + + public static boolean isTypeWithGenericNullable(ClassElement type) { + return type.isAssignable(Optional.class) + || type.isAssignable("com.google.common.base.Optional") + || type.isAssignable(AtomicReference.class) + ; + } } 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 419d7824c0..97918ef7f8 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java @@ -113,6 +113,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; import java.util.UUID; import java.util.function.BiConsumer; @@ -138,6 +141,7 @@ 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.OpenApiApplicationVisitor.expandProperties; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.replacePlaceholders; @@ -247,13 +251,13 @@ public static void clean() { /** * Reads schema. * - * @param schemaValue annotation value - * @param openAPI The OpenApi - * @param context The VisitorContext - * @param type type element - * @param typeArgs type arguments + * @param schemaValue annotation value + * @param openAPI The OpenApi + * @param context The VisitorContext + * @param type type element + * @param typeArgs type arguments * @param definingElement defining element - * @param mediaTypes The media types of schema + * @param mediaTypes The media types of schema * @param jsonViewClass Class from JsonView annotation * @return New schema instance */ @@ -340,8 +344,8 @@ public static Schema getSchemaDefinition(OpenAPI openAPI, final PrimitiveType primitiveType; if (isBasicType) { primitiveType = ClassUtils.forName(type.getName(), SchemaDefinitionUtils.class.getClassLoader()) - .map(PrimitiveType::fromType) - .orElse(null); + .map(PrimitiveType::fromType) + .orElse(null); } else { primitiveType = null; } @@ -403,9 +407,9 @@ public static Schema getSchemaDefinition(OpenAPI openAPI, schema.setDescription(originalTypeSchema.getDescription()); } if ((originalTypeSchema.getNullable() != null && originalTypeSchema.getNullable()) - || (isOpenapi31() - && CollectionUtils.isNotEmpty(originalTypeSchema.getTypes()) - && originalTypeSchema.getTypes().contains(SchemaUtils.TYPE_NULL)) + || (isOpenapi31() + && CollectionUtils.isNotEmpty(originalTypeSchema.getTypes()) + && originalTypeSchema.getTypes().contains(SchemaUtils.TYPE_NULL)) ) { SchemaUtils.setNullable(schema); } @@ -554,8 +558,8 @@ public static List getEnumValues(EnumElement type, String schemaType, St boolean isHidden = schemaAnn != null && schemaAnn.booleanValue(PROP_HIDDEN).orElse(false); if (isHidden - || isAnnotationPresent(element, Hidden.class) - || isAnnotationPresent(element, JsonIgnore.class)) { + || isAnnotationPresent(element, Hidden.class) + || isAnnotationPresent(element, JsonIgnore.class)) { continue; } var jsonPropertyAnn = getAnnotation(element, JsonProperty.class); @@ -578,10 +582,10 @@ public static List getEnumValues(EnumElement type, String schemaType, St * Resolves the schema for the given type element. * * @param definingElement The defining element - * @param type The type element - * @param context The context - * @param mediaTypes An optional media type - * @param jsonViewClass Class from JsonView annotation + * @param type The type element + * @param context The context + * @param mediaTypes An optional media type + * @param jsonViewClass Class from JsonView annotation * @return The schema or null if it cannot be resolved */ @Nullable @@ -592,14 +596,14 @@ public static Schema resolveSchema(@Nullable Element definingElement, ClassEl /** * Resolves the schema for the given type element. * - * @param openApi The OpenAPI object + * @param openApi The OpenAPI object * @param definingElement The defining element - * @param type The type element - * @param context The context - * @param mediaTypes An optional media type - * @param fieldJavadoc Field-level java doc - * @param classJavadoc Class-level java doc - * @param jsonViewClass Class from JsonView annotation + * @param type The type element + * @param context The context + * @param mediaTypes An optional media type + * @param fieldJavadoc Field-level java doc + * @param classJavadoc Class-level java doc + * @param jsonViewClass Class from JsonView annotation * @return The schema or null if it cannot be resolved */ @Nullable @@ -686,23 +690,23 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin if (type.isAssignable("io.micronaut.http.server.multipart.MultipartBody")) { isPublisher = true; type = type.getInterfaces() - .stream() - .filter(i -> i.isAssignable("org.reactivestreams.Publisher")) - .findFirst() - .flatMap(ClassElement::getFirstTypeArgument) - .orElse(null); + .stream() + .filter(i -> i.isAssignable("org.reactivestreams.Publisher")) + .findFirst() + .flatMap(ClassElement::getFirstTypeArgument) + .orElse(null); // StreamingFileUpload implements Publisher, but it should be not considered as a Publisher in the spec file } else if (!type.isAssignable("io.micronaut.http.multipart.StreamingFileUpload") && ElementUtils.isContainerType(type)) { isPublisher = (type.isAssignable("org.reactivestreams.Publisher") || type.isAssignable("kotlinx.coroutines.flow.Flow")) - && !type.isAssignable("reactor.core.publisher.Mono"); + && !type.isAssignable("reactor.core.publisher.Mono"); isObservable = (type.isAssignable("io.reactivex.Observable") || type.isAssignable("io.reactivex.rxjava3.core.Observable")) - && !type.isAssignable("reactor.core.publisher.Mono"); + && !type.isAssignable("reactor.core.publisher.Mono"); type = componentType; if (componentType != null) { typeArgs = componentType.getTypeArguments(); componentType = componentType.getFirstTypeArgument().orElse(null); } - } else if (isTypeNullable(type)) { + } else if (isTypeWithGenericNullable(type)) { isNullable = true; type = componentType; if (componentType != null) { @@ -790,25 +794,31 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin } else if (type.isAssignable(Character.class) || type.isAssignable(char.class)) { schema = setSpecVersion(PrimitiveType.STRING.createProperty()); } else if (type.isAssignable(Integer.class) || type.isAssignable(int.class) - || type.isAssignable(Short.class) || type.isAssignable(short.class)) { + || type.isAssignable(Short.class) || type.isAssignable(short.class) + || type.isAssignable(OptionalInt.class) + ) { schema = setSpecVersion(PrimitiveType.INT.createProperty()); - } else if (type.isAssignable(Long.class) || type.isAssignable(long.class)) { + } else if (type.isAssignable(Long.class) || type.isAssignable(long.class) + || type.isAssignable(OptionalLong.class) + ) { schema = setSpecVersion(PrimitiveType.LONG.createProperty()); } else if (type.isAssignable(Float.class) || type.isAssignable(float.class)) { schema = setSpecVersion(PrimitiveType.FLOAT.createProperty()); - } else if (type.isAssignable(Double.class) || type.isAssignable(double.class)) { + } else if (type.isAssignable(Double.class) || type.isAssignable(double.class) + || type.isAssignable(OptionalDouble.class) + ) { schema = setSpecVersion(PrimitiveType.DOUBLE.createProperty()); } else if (type.isAssignable(BigInteger.class)) { schema = setSpecVersion(PrimitiveType.INTEGER.createProperty()); } else if (type.isAssignable(BigDecimal.class)) { schema = setSpecVersion(PrimitiveType.DECIMAL.createProperty()); } else if (type.isAssignable(Date.class) - || type.isAssignable(Calendar.class) - || type.isAssignable(LocalDateTime.class) - || type.isAssignable(ZonedDateTime.class) - || type.isAssignable(OffsetDateTime.class) - || type.isAssignable(Instant.class) - || type.isAssignable(XMLGregorianCalendar.class)) { + || type.isAssignable(Calendar.class) + || type.isAssignable(LocalDateTime.class) + || type.isAssignable(ZonedDateTime.class) + || type.isAssignable(OffsetDateTime.class) + || type.isAssignable(Instant.class) + || type.isAssignable(XMLGregorianCalendar.class)) { schema = setSpecVersion(new StringSchema().format("date-time")); } else if (type.isAssignable(LocalDate.class)) { schema = setSpecVersion(new StringSchema().format("date")); @@ -866,17 +876,17 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin /** * Binds the schema for the given element. * - * @param context The context - * @param element The element - * @param elementType The element type - * @param schemaToBind The schema to bind + * @param context The context + * @param element The element + * @param elementType The element type + * @param schemaToBind The schema to bind * @param jsonViewClass Class from JsonView annotation * @return The bound schema */ public static Schema bindSchemaForElement(VisitorContext context, TypedElement element, ClassElement elementType, Schema schemaToBind, @Nullable ClassElement jsonViewClass) { var schemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); - Schema originalSchema = schemaToBind; + Schema originalSchema = schemaToBind != null ? schemaToBind : new Schema<>(); if (originalSchema.get$ref() != null) { Schema schemaFromAnn = schemaFromAnnotation(context, element, elementType, schemaAnn); @@ -888,9 +898,9 @@ public static Schema bindSchemaForElement(VisitorContext context, TypedElemen // Apply @Schema annotation only if not $ref since for $ref schemas // we already populated values from right @Schema annotation in previous steps schemaToBind = bindSchemaAnnotationValue(context, element, schemaToBind, schemaAnn, jsonViewClass); - Optional schemaName = schemaAnn.stringValue(PROP_NAME); - if (schemaName.isPresent()) { - schemaToBind.setName(schemaName.get()); + var schemaName = schemaAnn.stringValue(PROP_NAME).orElse(null); + if (schemaName != null) { + schemaToBind.setName(schemaName); } var impl = schemaAnn.stringValue(PROP_IMPLEMENTATION).orElse(null); @@ -925,9 +935,9 @@ public static Schema bindSchemaForElement(VisitorContext context, TypedElemen notOnlyRef = true; } if (isAnnotationPresent(element, Deprecated.class) - && !(element instanceof PropertyElement propertyEl - && isProtobufGenerated(propertyEl.getOwningType()) - && elementType.getName().equals(Map.class.getName()) + && !(element instanceof PropertyElement propertyEl + && isProtobufGenerated(propertyEl.getOwningType()) + && elementType.getName().equals(Map.class.getName()) )) { topLevelSchema.setDeprecated(true); notOnlyRef = true; @@ -976,10 +986,10 @@ && isProtobufGenerated(propertyEl.getOwningType()) } if (!SchemaUtils.isEmptySchema(composedSchema) - && ((CollectionUtils.isNotEmpty(composedSchema.getAllOf()) && composedSchema.getAllOf().size() > 1) - || CollectionUtils.isNotEmpty(composedSchema.getOneOf()) - || CollectionUtils.isNotEmpty(composedSchema.getAnyOf()) - || notOnlyRef)) { + && ((CollectionUtils.isNotEmpty(composedSchema.getAllOf()) && composedSchema.getAllOf().size() > 1) + || CollectionUtils.isNotEmpty(composedSchema.getOneOf()) + || CollectionUtils.isNotEmpty(composedSchema.getAnyOf()) + || notOnlyRef)) { return composedSchema; } if (CollectionUtils.isNotEmpty(composedSchema.getAllOf()) && composedSchema.getAllOf().size() == 1) { @@ -992,10 +1002,10 @@ && isProtobufGenerated(propertyEl.getOwningType()) /** * Binds the array schema for the given element. * - * @param context The context - * @param element The element - * @param schemaToBind The schema to bind - * @param schemaAnn The schema annotation + * @param context The context + * @param element The element + * @param schemaToBind The schema to bind + * @param schemaAnn The schema annotation * @param jsonViewClass Class from JsonView annotation * @return The bound schema */ @@ -1030,8 +1040,8 @@ public static Schema bindArraySchemaAnnotationValue(VisitorContext context, T /** * Convert the values to a map. * - * @param values The values - * @param context The visitor context + * @param values The values + * @param context The visitor context * @param jsonViewClass Class from JsonView annotation * @return The map */ @@ -1192,19 +1202,19 @@ public static Map toValueMap(Map val var variables = new LinkedHashMap>(); for (Object o : a) { var sv = (AnnotationValue) o; - Optional n = sv.stringValue(PROP_NAME); - n.ifPresent(name -> { - Map map = toValueMap(sv.getValues(), context, null); - Object dv = map.get(PROP_DEFAULT_VALUE); - if (dv != null) { - map.put(PROP_DEFAULT, dv); - } - if (map.containsKey(PROP_ALLOWABLE_VALUES)) { - // The key in the generated openapi needs to be "enum" - map.put(PROP_ENUM, map.remove(PROP_ALLOWABLE_VALUES)); - } - variables.put(name, map); - }); + sv.stringValue(PROP_NAME) + .ifPresent(name -> { + Map map = toValueMap(sv.getValues(), context, null); + Object dv = map.get(PROP_DEFAULT_VALUE); + if (dv != null) { + map.put(PROP_DEFAULT, dv); + } + if (map.containsKey(PROP_ALLOWABLE_VALUES)) { + // The key in the generated openapi needs to be "enum" + map.put(PROP_ENUM, map.remove(PROP_ALLOWABLE_VALUES)); + } + variables.put(name, map); + }); } newValues.put(key, variables); } else if (DiscriminatorMapping.class.getName().equals(annotationName)) { @@ -1329,7 +1339,7 @@ public static Map resolveArraySchemaAnnotationValues(Visit var arraySchemaMap = new HashMap(10); // properties av.get(PROP_ARRAY_SCHEMA, AnnotationValue.class).ifPresent(annotationValue -> - processAnnotationValue(context, (AnnotationValue) annotationValue, arraySchemaMap, List.of(PROP_REF, PROP_IMPLEMENTATION), Schema.class, null) + processAnnotationValue(context, (AnnotationValue) annotationValue, arraySchemaMap, List.of(PROP_REF, PROP_IMPLEMENTATION), Schema.class, null) ); // items av.get(PROP_SCHEMA, AnnotationValue.class).ifPresent(annotationValue -> { @@ -1372,10 +1382,10 @@ public static Map resolveArraySchemaAnnotationValues(Visit /** * Binds the schema for the given element. * - * @param context The context - * @param element The element - * @param schemaToBind The schema to bind - * @param schemaAnn The schema annotation + * @param context The context + * @param element The element + * @param schemaToBind The schema to bind + * @param schemaAnn The schema annotation * @param jsonViewClass Class from JsonView annotation * @return The bound schema */ @@ -1395,18 +1405,18 @@ public static Schema bindSchemaAnnotationValue(VisitorContext context, TypedE JsonNode schemaJson = toJson(schemaAnn.getValues(), context, jsonViewClass); return doBindSchemaAnnotationValue(context, element, schemaToBind, schemaJson, - schemaAnn.stringValue(PROP_TYPE).orElse(typeAndFormat.getFirst()), - schemaAnn.stringValue(PROP_ONE_FORMAT).orElse(typeAndFormat.getSecond()), - schemaAnn, jsonViewClass); + schemaAnn.stringValue(PROP_TYPE).orElse(typeAndFormat.getFirst()), + schemaAnn.stringValue(PROP_ONE_FORMAT).orElse(typeAndFormat.getSecond()), + schemaAnn, jsonViewClass); } /** * Convert the given Map to a JSON node and then to the specified type. * - * @param The output class type - * @param values The values - * @param context The visitor context - * @param type The class + * @param The output class type + * @param values The values + * @param context The visitor context + * @param type The class * @param jsonViewClass Class from JsonView annotation * @return The converted instance */ @@ -1423,8 +1433,8 @@ public static Optional toValue(Map values, VisitorC /** * Convert the given map to a JSON node. * - * @param values The values - * @param context The visitor context + * @param values The values + * @param context The visitor context * @param jsonViewClass Class from JsonView annotation * @return The node */ @@ -1436,11 +1446,11 @@ public static JsonNode toJson(Map values, VisitorContext c /** * Processes a schema property. * - * @param context The visitor context - * @param element The element - * @param elementType The element type - * @param classElement The class element - * @param parentSchema The parent schema + * @param context The visitor context + * @param element The element + * @param elementType The element type + * @param classElement The class element + * @param parentSchema The parent schema * @param propertySchema The property schema */ public static void processSchemaProperty(VisitorContext context, TypedElement element, ClassElement elementType, @Nullable ClassElement classElement, @@ -1461,7 +1471,7 @@ public static void processSchemaProperty(VisitorContext context, TypedElement el elementSchemaRequired = schemaAnn.get(PROP_REQUIRED, Argument.BOOLEAN).orElse(null); isRequiredDefaultValueSet = !schemaAnn.contains(PROP_REQUIRED); var requiredMode = schemaAnn.enumValue(PROP_REQUIRED_MODE, io.swagger.v3.oas.annotations.media.Schema.RequiredMode.class) - .orElse(null); + .orElse(null); if (requiredMode == io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED) { elementSchemaRequired = true; isAutoRequiredMode = false; @@ -1489,23 +1499,23 @@ public static void processSchemaProperty(VisitorContext context, TypedElement el addProperty(parentSchema, propertyName, propertySchema, required); if (schemaAnn != null) { schemaAnn.stringValue(PROP_DEFAULT_VALUE) - .ifPresent(value -> { - String elType = schemaAnn.stringValue(PROP_TYPE).orElse(null); - String elFormat = schemaAnn.stringValue(PROP_ONE_FORMAT).orElse(null); - if (elType == null && elementType != null) { - Pair typeAndFormat; - if (elementType instanceof EnumElement enumEl) { - typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, elFormat); - } else { - typeAndFormat = ConvertUtils.getTypeAndFormatByClass(elementType.getName(), elementType.isArray()); - } - elType = typeAndFormat.getFirst(); - if (elFormat == null) { - elFormat = typeAndFormat.getSecond(); - } + .ifPresent(value -> { + String elType = schemaAnn.stringValue(PROP_TYPE).orElse(null); + String elFormat = schemaAnn.stringValue(PROP_ONE_FORMAT).orElse(null); + if (elType == null && elementType != null) { + Pair typeAndFormat; + if (elementType instanceof EnumElement enumEl) { + typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, elFormat); + } else { + typeAndFormat = ConvertUtils.getTypeAndFormatByClass(elementType.getName(), elementType.isArray()); } - setDefaultValueObject(propertySchemaFinal, value, element, elType, elFormat, false, context); - }); + elType = typeAndFormat.getFirst(); + if (elFormat == null) { + elFormat = typeAndFormat.getSecond(); + } + } + setDefaultValueObject(propertySchemaFinal, value, element, elType, elFormat, false, context); + }); } } } @@ -1561,13 +1571,13 @@ private static void populateSchemaProperties(OpenAPI openAPI, VisitorContext con continue; } if (visibilityLevel == VisibilityLevel.PUBLIC - && !field.isPublic()) { + && !field.isPublic()) { continue; } else if (visibilityLevel == VisibilityLevel.PROTECTED - && (!field.isPublic() && !field.isProtected())) { + && (!field.isPublic() && !field.isProtected())) { continue; } else if (visibilityLevel == VisibilityLevel.PACKAGE - && (!field.isPublic() && !field.isProtected() && !field.isPackagePrivate())) { + && (!field.isPublic() && !field.isProtected() && !field.isPackagePrivate())) { continue; } boolean alreadyProcessed = false; @@ -1606,8 +1616,8 @@ private static Schema processSuperTypes(Schema schema, if (classElement.isInterface() && !parentInterfaces.isEmpty()) { for (ClassElement parentInterface : parentInterfaces) { if (ClassUtils.isJavaLangType(parentInterface.getName()) - || isProtobufGenerated(parentInterface) - || parentInterface.getBeanProperties().isEmpty()) { + || isProtobufGenerated(parentInterface) + || parentInterface.getBeanProperties().isEmpty()) { continue; } superTypes.add(parentInterface); @@ -1615,8 +1625,8 @@ private static Schema processSuperTypes(Schema schema, } else { var superType = classElement.getSuperType().orElse(null); if (superType != null - // check protobuf generated classes - && !isProtobufMessageClass(superType)) { + // check protobuf generated classes + && !isProtobufMessageClass(superType)) { superTypes.add(superType); } } @@ -1624,7 +1634,7 @@ private static Schema processSuperTypes(Schema schema, // skip if it is Enum or Object super class String firstSuperTypeName = superTypes.get(0).getName(); if (superTypes.size() == 1 - && (firstSuperTypeName.equals(Enum.class.getName()) || firstSuperTypeName.equals(Object.class.getName()))) { + && (firstSuperTypeName.equals(Enum.class.getName()) || firstSuperTypeName.equals(Object.class.getName()))) { if (schema != null) { schema.setName(schemaName); schemas.put(schemaName, schema); @@ -1666,7 +1676,7 @@ private static void readAllInterfaces(OpenAPI openAPI, VisitorContext context, @ definingElement, superType, superTypeArgs, context, jsonViewClass); if (schemas.get(parentSchemaName) != null - || getSchemaDefinition(openAPI, context, superType, superTypeArgs, null, mediaTypes, jsonViewClass) != null) { + || getSchemaDefinition(openAPI, context, superType, superTypeArgs, null, mediaTypes, jsonViewClass) != null) { var parentSchema = setSpecVersion(new Schema<>()); parentSchema.set$ref(SchemaUtils.schemaRef(parentSchemaName)); if (schema.getAllOf() == null || !schema.getAllOf().contains(parentSchema)) { @@ -1676,7 +1686,7 @@ private static void readAllInterfaces(OpenAPI openAPI, VisitorContext context, @ if (superType.isInterface()) { for (var interfaceEl : superType.getInterfaces()) { if (ClassUtils.isJavaLangType(interfaceEl.getName()) - || interfaceEl.getBeanProperties().isEmpty()) { + || interfaceEl.getBeanProperties().isEmpty()) { continue; } @@ -2129,166 +2139,166 @@ private static void processJakartaValidationAnnotations(Element element, ClassEl if (isIterableOrMap) { if (isAnnotationPresent(element, "javax.validation.constraints.NotEmpty$List") - || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List")) { + || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List")) { schemaToBind.setMinItems(1); } findAnnotation(element, "javax.validation.constraints.Size$List") - .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) - .ifPresent(ann -> ann.intValue("min") - .ifPresent(schemaToBind::setMinItems))); + .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) + .ifPresent(ann -> ann.intValue("min") + .ifPresent(schemaToBind::setMinItems))); findAnnotation(element, "jakarta.validation.constraints.Size$List") - .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) - .ifPresent(ann -> ann.intValue("min") - .ifPresent(schemaToBind::setMinItems))); + .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) + .ifPresent(ann -> ann.intValue("min") + .ifPresent(schemaToBind::setMinItems))); findAnnotation(element, "javax.validation.constraints.Size$List") - .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) - .ifPresent(ann -> ann.intValue("max") - .ifPresent(schemaToBind::setMaxItems))); + .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) + .ifPresent(ann -> ann.intValue("max") + .ifPresent(schemaToBind::setMaxItems))); findAnnotation(element, "jakarta.validation.constraints.Size$List") - .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) - .ifPresent(ann -> ann.intValue("max") - .ifPresent(schemaToBind::setMaxItems))); + .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) + .ifPresent(ann -> ann.intValue("max") + .ifPresent(schemaToBind::setMaxItems))); } else { if (PrimitiveType.STRING.getCommonName().equals(schemaToBind.getType())) { 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")) { + || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List") + || isAnnotationPresent(element, "javax.validation.constraints.NotBlank$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NotBlank$List")) { schemaToBind.setMinLength(1); } findAnnotation(element, "javax.validation.constraints.Size$List") - .ifPresent(listAnn -> { - for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.intValue("min").ifPresent(schemaToBind::setMinLength); - ann.intValue("max").ifPresent(schemaToBind::setMaxLength); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + ann.intValue("min").ifPresent(schemaToBind::setMinLength); + ann.intValue("max").ifPresent(schemaToBind::setMaxLength); + } + }); findAnnotation(element, "jakarta.validation.constraints.Size$List") - .ifPresent(listAnn -> { - for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.intValue("min").ifPresent(schemaToBind::setMinLength); - ann.intValue("max").ifPresent(schemaToBind::setMaxLength); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + ann.intValue("min").ifPresent(schemaToBind::setMinLength); + ann.intValue("max").ifPresent(schemaToBind::setMaxLength); + } + }); } if (isAnnotationPresent(element, "javax.validation.constraints.Negative$List") - || isAnnotationPresent(element, "jakarta.validation.constraints.Negative$List")) { + || isAnnotationPresent(element, "jakarta.validation.constraints.Negative$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); schemaToBind.exclusiveMaximum(true); } if (isAnnotationPresent(element, "javax.validation.constraints.NegativeOrZero$List") - || isAnnotationPresent(element, "jakarta.validation.constraints.NegativeOrZero$List")) { + || isAnnotationPresent(element, "jakarta.validation.constraints.NegativeOrZero$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); } if (isAnnotationPresent(element, "javax.validation.constraints.Positive$List") - || isAnnotationPresent(element, "jakarta.validation.constraints.Positive$List")) { + || isAnnotationPresent(element, "jakarta.validation.constraints.Positive$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); schemaToBind.exclusiveMinimum(true); } if (isAnnotationPresent(element, "javax.validation.constraints.PositiveOrZero$List") - || isAnnotationPresent(element, "jakarta.validation.constraints.PositiveOrZero$List")) { + || isAnnotationPresent(element, "jakarta.validation.constraints.PositiveOrZero$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); } findAnnotation(element, "javax.validation.constraints.Min$List") - .ifPresent(listAnn -> { - for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMinimum); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + 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); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + ann.getValue(BigDecimal.class) + .ifPresent(schemaToBind::setMinimum); + } + }); findAnnotation(element, "javax.validation.constraints.Max$List") - .ifPresent(listAnn -> { - for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMaximum); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + 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); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + ann.getValue(BigDecimal.class) + .ifPresent(schemaToBind::setMaximum); + } + }); findAnnotation(element, "javax.validation.constraints.DecimalMin$List") - .ifPresent(listAnn -> { - for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMinimum); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + 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); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + ann.getValue(BigDecimal.class) + .ifPresent(schemaToBind::setMinimum); + } + }); findAnnotation(element, "javax.validation.constraints.DecimalMax$List") - .ifPresent(listAnn -> { - for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { - ann.getValue(BigDecimal.class) - .ifPresent(schemaToBind::setMaximum); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + 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); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + ann.getValue(BigDecimal.class) + .ifPresent(schemaToBind::setMaximum); + } + }); 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); - } - }); + .ifPresent(listAnn -> { + schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + 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); - } - }); + .ifPresent(listAnn -> { + schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + 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); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + 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); - } - }); + .ifPresent(listAnn -> { + for (AnnotationValue ann : listAnn.getAnnotations(PROP_VALUE)) { + ann.stringValue("regexp") + .ifPresent(schemaToBind::setPattern); + } + }); element.getValue("io.micronaut.http.annotation.Part", String.class) - .ifPresent(schemaToBind::setName); + .ifPresent(schemaToBind::setName); } } @@ -2385,12 +2395,12 @@ private static void processPropertyElements(OpenAPI openAPI, VisitorContext cont Schema propertySchema = resolveSchema(openAPI, publicField, fieldType, context, mediaTypes, jsonViewClass, fieldJavadoc, classJavadoc); processSchemaProperty( - context, - publicField, - fieldType, - classElement, - schema, - propertySchema + context, + publicField, + fieldType, + classElement, + schema, + propertySchema ); } } @@ -2666,9 +2676,9 @@ private static String resolvePropertyName(Element element, Element classElement, String name = propertySchema.getName() != null ? propertySchema.getName() : element.getName(); if (isAnnotationPresent(element, io.swagger.v3.oas.annotations.media.Schema.class)) { - Optional nameFromSchema = stringValue(element, io.swagger.v3.oas.annotations.media.Schema.class, PROP_NAME); - if (nameFromSchema.isPresent()) { - return nameFromSchema.get(); + var nameFromSchema = stringValue(element, io.swagger.v3.oas.annotations.media.Schema.class, PROP_NAME).orElse(null); + if (nameFromSchema != null) { + return nameFromSchema; } } if (isAnnotationPresent(element, JsonProperty.class)) { @@ -2731,10 +2741,10 @@ private static void handleUnwrapped(VisitorContext context, Element element, Cla private static boolean doesParamExistsMandatoryInConstructor(Element element, @Nullable Element classElement) { if (classElement instanceof ClassElement classEl) { return classEl.getPrimaryConstructor().flatMap(methodElement -> Arrays.stream(methodElement.getParameters()) - .filter(parameterElement -> parameterElement.getName().equals(element.getName())) - .map(parameterElement -> !parameterElement.isNullable()) - .findFirst()) - .orElse(false); + .filter(parameterElement -> parameterElement.getName().equals(element.getName())) + .map(parameterElement -> !parameterElement.isNullable()) + .findFirst()) + .orElse(false); } return false; @@ -2751,10 +2761,6 @@ private static void addProperty(Schema parentSchema, String name, Schema p } } - private static boolean isTypeNullable(ClassElement type) { - return type.isAssignable(Optional.class); - } - private static Map getDiscriminatorMap(Map newValues) { return newValues.containsKey(DISCRIMINATOR) ? (Map) newValues.get(DISCRIMINATOR) : new HashMap<>(); } @@ -2763,10 +2769,10 @@ private static > void processAnnotationValue(VisitorContext Map arraySchemaMap, List filters, Class type, @Nullable ClassElement jsonViewClass) { Map values = annotationValue.getValues().entrySet().stream() - .filter(entry -> filters == null || !filters.contains((String) entry.getKey())) - .collect(toMap(e -> e.getKey().equals(PROP_REQUIRED_PROPERTIES) ? PROP_REQUIRED : e.getKey(), Map.Entry::getValue)); + .filter(entry -> filters == null || !filters.contains((String) entry.getKey())) + .collect(toMap(e -> e.getKey().equals(PROP_REQUIRED_PROPERTIES) ? PROP_REQUIRED : e.getKey(), Map.Entry::getValue)); toValue(values, context, type, jsonViewClass) - .ifPresent(s -> schemaToValueMap(arraySchemaMap, s)); + .ifPresent(s -> schemaToValueMap(arraySchemaMap, s)); } private static Map resolveAnnotationValues(VisitorContext context, AnnotationValue av, @Nullable ClassElement jsonViewClass) { @@ -2775,8 +2781,8 @@ private static Map resolveAnnotationValues(VisitorContext final String annotationName = av.getAnnotationName(); if (Parameter.class.getName().equals(annotationName)) { Utils.normalizeEnumValues(valueMap, CollectionUtils.mapOf( - PROP_IN, ParameterIn.class, - PROP_STYLE, ParameterStyle.class + PROP_IN, ParameterIn.class, + PROP_STYLE, ParameterStyle.class )); } return valueMap; 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 47b76459bb..b02ed99369 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiControllerVisitorSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiControllerVisitorSpec.groovy @@ -1713,10 +1713,6 @@ class MyBean {} buildBeanDefinition('test.MyBean', ''' package test; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; - import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; @@ -1724,13 +1720,19 @@ import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Single; - -import org.reactivestreams.Publisher; - import kotlinx.coroutines.flow.Flow; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + @Controller class HelloWorldController { @@ -1815,11 +1817,41 @@ class HelloWorldController { return null; } + @Post("/endpoint29") + public AtomicReference endpoint1(@Body AtomicReference body) { + return null; + } + + @Post("/endpoint210") + public com.google.common.base.Optional endpoint1(@Body com.google.common.base.Optional body) { + return null; + } + + @Post("/optInt") + public OptionalInt optInt(@Body OptionalInt body) { + return null; + } + + @Post("/optLong") + public OptionalLong optLong(@Body OptionalLong body) { + return null; + } + + @Post("/optDouble") + public OptionalDouble optDouble(@Body OptionalDouble body) { + return null; + } } class MyDto { public String field; + public OptionalInt optInt; + public OptionalLong optLong; + public OptionalDouble optDouble; + public Optional optStr; + public com.google.common.base.Optional guavaOptStr; + public AtomicReference referenceStr; } @jakarta.inject.Singleton @@ -1834,16 +1866,59 @@ class MyBean {} for (def i = 1; i < 8; i++) { def operation = openAPI.paths.get("/endpoint1" + i).post assert operation.requestBody.content."application/json".schema.items.$ref == '#/components/schemas/MyDto' - assert operation.requestBody.content."application/json".schema.items.$ref == '#/components/schemas/MyDto' + assert operation.responses."200".content."application/json".schema.items.$ref == '#/components/schemas/MyDto' } // single - for (def i = 1; i < 9; i++) { + for (def i = 1; i < 11; i++) { def operation = openAPI.paths.get("/endpoint2" + i).post assert operation.requestBody.content."application/json".schema.$ref == '#/components/schemas/MyDto' assert operation.responses."200".content."application/json".schema.$ref == '#/components/schemas/MyDto' } + // optional primitives + + def optInt = openAPI.paths."/optInt".post + optInt.requestBody.content."application/json".schema.type == 'integer' + optInt.responses."200".content."application/json".schema.type == 'integer' + + def optLong = openAPI.paths."/optLong".post + optLong.requestBody.content."application/json".schema.type == 'integer' + optLong.requestBody.content."application/json".schema.format == 'int64' + optLong.responses."200".content."application/json".schema.type == 'integer' + optLong.responses."200".content."application/json".schema.format == 'int64' + + def optDouble = openAPI.paths."/optDouble".post + optDouble.requestBody.content."application/json".schema.type == 'number' + optDouble.requestBody.content."application/json".schema.format == 'double' + optDouble.responses."200".content."application/json".schema.type == 'number' + optDouble.responses."200".content."application/json".schema.format == 'double' + + def myDto = openAPI.components.schemas.MyDto + myDto.properties + myDto.properties.field + myDto.properties.field.type == 'string' + + myDto.properties.optInt.type == 'integer' + myDto.properties.optInt.format == 'int32' + myDto.properties.optInt.nullable == true + + myDto.properties.optLong.type == 'integer' + myDto.properties.optLong.format == 'int64' + myDto.properties.optLong.nullable == true + + myDto.properties.optDouble.type == 'number' + myDto.properties.optDouble.format == 'double' + myDto.properties.optDouble.nullable == true + + myDto.properties.optStr.type == 'string' + myDto.properties.optStr.nullable == true + + myDto.properties.guavaOptStr.type == 'string' + myDto.properties.guavaOptStr.nullable == true + + myDto.properties.referenceStr.type == 'string' + myDto.properties.referenceStr.nullable == true } void "test parse @Part StreamingFileUpload parameter data"() {