From 201c3bc5bd8b1d2258a7de6ab524b5e127f49562 Mon Sep 17 00:00:00 2001 From: altro3 Date: Sun, 16 Jul 2023 13:41:37 +0700 Subject: [PATCH] Add process WildcardElement in generics Fixed #1131 --- .../visitor/AbstractOpenApiVisitor.java | 37 ++++++++--- .../visitor/OpenApiSchemaGenericsSpec.groovy | 61 +++++++++++++++++++ 2 files changed, 91 insertions(+), 7 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 c1241be9c0..a857968130 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java @@ -78,6 +78,7 @@ import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.ast.WildcardElement; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.openapi.javadoc.JavadocDescription; import io.micronaut.openapi.swagger.PrimitiveType; @@ -787,13 +788,28 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen } } - ClassElement componentType = type.getFirstTypeArgument().orElse(null); - Map typeArgs = type.getTypeArguments(); + Boolean isArray = null; + Boolean isIterable = null; + + ClassElement componentType = type != null ? type.getFirstTypeArgument().orElse(null) : null; + if (type instanceof WildcardElement) { + WildcardElement wildcardEl = (WildcardElement) type; + type = CollectionUtils.isNotEmpty(wildcardEl.getUpperBounds()) ? wildcardEl.getUpperBounds().get(0) : null; + } else if (type instanceof GenericPlaceholderElement) { + GenericPlaceholderElement placeholderEl = (GenericPlaceholderElement) type; + isArray = type.isArray(); + isIterable = type.isIterable(); + type = CollectionUtils.isNotEmpty(placeholderEl.getBounds()) ? placeholderEl.getBounds().get(0) : null; + } + Map typeArgs = type != null ? type.getTypeArguments() : null; Schema schema = null; if (type instanceof EnumElement) { schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes, jsonViewClass); + if (isArray != null && isArray) { + schema = SchemaUtils.arraySchema(schema); + } } else { boolean isPublisher = false; @@ -830,6 +846,13 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen if (type != null) { + if (isArray == null) { + isArray = type.isArray(); + } + if (isIterable == null) { + isIterable = type.isIterable(); + } + String typeName = type.getName(); ClassElement customTypeSchema = OpenApiApplicationVisitor.getCustomSchema(typeName, typeArgs, context); if (customTypeSchema != null) { @@ -843,13 +866,13 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen typeName = PrimitiveType.BINARY.name(); } PrimitiveType primitiveType = PrimitiveType.fromName(typeName); - if (!type.isArray() && ClassUtils.isJavaLangType(typeName)) { + if (!isArray && ClassUtils.isJavaLangType(typeName)) { schema = getPrimitiveType(typeName); - } else if (!type.isArray() && primitiveType != null) { + } else if (!isArray && primitiveType != null) { schema = primitiveType.createProperty(); } else if (type.isAssignable(Map.class.getName())) { schema = new MapSchema(); - if (typeArgs.isEmpty()) { + if (CollectionUtils.isEmpty(typeArgs)) { schema.setAdditionalProperties(true); } else { ClassElement valueType = typeArgs.get("V"); @@ -859,8 +882,8 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen schema.setAdditionalProperties(resolveSchema(openAPI, type, valueType, context, mediaTypes, jsonViewClass, null, classJavadoc)); } } - } else if (type.isIterable()) { - if (type.isArray()) { + } else if (isIterable) { + if (isArray) { schema = resolveSchema(openAPI, type, type.fromArray(), context, mediaTypes, jsonViewClass, null, classJavadoc); if (schema != null) { schema = SchemaUtils.arraySchema(schema); diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy index bba4b0c353..7fe8a6d03e 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaGenericsSpec.groovy @@ -486,4 +486,65 @@ class MyBean {} openAPI.components.schemas['TimeUnit'].enum[5] == 'Week' openAPI.components.schemas['Time'].allOf[0].$ref == '#/components/schemas/Quantity_Time.TimeUnit_' } + + + void "test schema with generic wildcard or placeholder"() { + given: + buildBeanDefinition('test.MyBean', ''' + +package test; + +import java.util.Collection; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.QueryValue; + +@Controller +class CommonController { + + @Get("/get1") + public String index1(@Nullable @QueryValue T[] channels) { + return null; + } + + @Get("/get2") + public String index2(@Nullable @QueryValue Collection channels) { + return null; + } + + @Introspected + enum Channel { + SYSTEM1, + SYSTEM2 + } +} + +@jakarta.inject.Singleton +class MyBean {} +''') + + OpenAPI openAPI = Utils.testReference + Operation get1 = openAPI.paths?.get("/get1")?.get + Operation get2 = openAPI.paths?.get("/get2")?.get + + expect: + get1 + get1.parameters.get(0).name == 'channels' + get1.parameters.get(0).in == 'query' + get1.parameters.get(0).schema + get1.parameters.get(0).schema.type == 'array' + get1.parameters.get(0).schema.nullable + get1.parameters.get(0).schema.items.$ref == '#/components/schemas/CommonController.Channel' + + get2 + get2.parameters.get(0).name == 'channels' + get2.parameters.get(0).in == 'query' + get2.parameters.get(0).schema + get2.parameters.get(0).schema.type == 'array' + get2.parameters.get(0).schema.nullable + get2.parameters.get(0).schema.items.$ref == '#/components/schemas/CommonController.Channel' + } }