diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ScannerLogging.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ScannerLogging.java index 34c1456c0..50386324e 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ScannerLogging.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ScannerLogging.java @@ -5,6 +5,7 @@ import org.jboss.jandex.Type; import org.jboss.logging.BasicLogger; import org.jboss.logging.Logger; +import org.jboss.logging.annotations.Cause; import org.jboss.logging.annotations.LogMessage; import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; @@ -37,4 +38,8 @@ interface ScannerLogging extends BasicLogger { @Message(id = 4005, value = "Could not find schema class in index: %s") void schemaTypeNotFound(DotName className); + @LogMessage(level = Logger.Level.WARN) + @Message(id = 4006, value = "Configured schema type %s is not a valid type signature") + void configSchemaTypeInvalid(String typeSignature, @Cause Throwable cause); + } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java index 3f8288863..0d15816e1 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java @@ -32,6 +32,7 @@ import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext; import io.smallrye.openapi.runtime.util.Annotations; import io.smallrye.openapi.runtime.util.ModelUtil; +import io.smallrye.openapi.runtime.util.TypeParser; import io.smallrye.openapi.runtime.util.TypeUtil; /** @@ -210,10 +211,18 @@ public SchemaRegistry(AnnotationScannerContext context) { } config.getSchemas().entrySet().forEach(entry -> { - String className = entry.getKey(); + String typeSignature = entry.getKey(); String jsonSchema = entry.getValue(); + Type type; Schema schema; + try { + type = TypeParser.parse(typeSignature); + } catch (Exception e) { + ScannerLogging.logger.configSchemaTypeInvalid(typeSignature, e); + return; + } + try { schema = context.getExtensions() .stream() @@ -221,13 +230,12 @@ public SchemaRegistry(AnnotationScannerContext context) { .findFirst() .orElseThrow(NoSuchElementException::new); } catch (Exception e) { - ScannerLogging.logger.errorParsingSchema(className); + ScannerLogging.logger.errorParsingSchema(typeSignature); return; } - Type type = Type.create(DotName.createSimple(className), Type.Kind.CLASS); this.register(new TypeKey(type, Collections.emptySet()), schema, ((SchemaImpl) schema).getName()); - ScannerLogging.logger.configSchemaRegistered(className); + ScannerLogging.logger.configSchemaRegistered(typeSignature); }); } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeParser.java b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeParser.java new file mode 100644 index 000000000..e825f1ed8 --- /dev/null +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeParser.java @@ -0,0 +1,198 @@ +package io.smallrye.openapi.runtime.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; +import org.jboss.jandex.WildcardType; + +/** + * Parse a type signature String to a Jandex Type + */ +public class TypeParser { + + private static final WildcardType UNBOUNDED_WILDCARD = WildcardType.createUpperBound((Type) null); + private String signature; + private int pos; + + public static Type parse(String signature) { + return new TypeParser(signature).parse(); + } + + private TypeParser(String signature) { + this.signature = signature; + this.pos = 0; + } + + private Type parse() { + return parseReferenceType(); + } + + private Type parseClassTypeSignature() { + DotName name = parseName(); + Type[] types = parseTypeArguments(); + Type type = null; + + if (types.length > 0) { + type = ParameterizedType.create(name, types, null); + } + + return type != null ? type : ClassType.create(name); + } + + private Type[] parseTypeArguments() { + if (pos >= signature.length() || signature.charAt(pos) != '<') { + return Type.EMPTY_ARRAY; + } + pos++; + + List types = new ArrayList<>(); + for (;;) { + Type t = parseTypeArgument(); + if (t == null) { + break; + } + advanceNot(','); + types.add(t); + } + return types.toArray(new Type[types.size()]); + } + + private Type parseTypeArgument() { + requireIncomplete(); + char c = signature.charAt(pos++); + + if (c == '>') { + return null; + } + if (c == '?') { + if (signature.startsWith(" extends ", pos)) { + pos += " extends ".length(); + return parseWildCard(true); + } else if (signature.startsWith(" super ", pos)) { + pos += " super ".length(); + return parseWildCard(false); + } else { + requireIncomplete(); + if (signature.charAt(pos) != '>') { + throw new IllegalStateException(); + } + return UNBOUNDED_WILDCARD; + } + } + + pos--; + return parseReferenceType(); + } + + private Type parseWildCard(boolean isExtends) { + Type bound = parseReferenceType(); + + return isExtends ? WildcardType.createUpperBound(bound) : WildcardType.createLowerBound(bound); + } + + private Type parseReferenceType() { + int mark = pos; + int typeArgsStart = signature.indexOf('<', mark); + int typeArgsEnd = signature.indexOf('>', mark); + int arrayStart = signature.indexOf('[', mark); + + return Stream.of(typeArgsEnd, typeArgsStart, arrayStart) + .filter(v -> v > -1) + .min(Integer::compare) + .map(firstDelimiter -> { + Type type = null; + + if (firstDelimiter == arrayStart) { + type = parsePrimitive(); + } + + if (type == null) { + type = parseClassTypeSignature(); + } + + if (pos < signature.length() && signature.charAt(pos) == '[') { + type = parseArrayType(type); + } + + return type; + }) + .orElseGet(() -> { + Type primitive = parsePrimitive(); + + if (primitive != null) { + return primitive; + } + + return parseClassTypeSignature(); + }); + } + + private Type parseArrayType(Type type) { + int dimensions = 0; + + while (pos < signature.length() && signature.charAt(pos) == '[') { + pos++; + requireIncomplete(); + + if (signature.charAt(pos++) == ']') { + dimensions++; + } else { + throw new IllegalArgumentException(); + } + } + + return ArrayType.create(type, dimensions); + } + + private Type parsePrimitive() { + int mark = pos; + DotName name = parseName(); + Type type = Type.create(name, Type.Kind.PRIMITIVE); + if (type != null) { + return type; + } + pos = mark; + return null; + } + + private int advanceNot(char c) { + requireIncomplete(); + + while (signature.charAt(pos) == c) { + pos++; + } + + return pos; + } + + private DotName parseName() { + int start = pos; + int end = advanceNameEnd(); + return DotName.createSimple(signature.substring(start, end)); + } + + private int advanceNameEnd() { + int end = pos; + + for (; end < signature.length(); end++) { + char c = signature.charAt(end); + if (c == '[' || c == '<' || c == ',' || c == '>') { + return pos = end; + } + } + + return pos = end; + } + + private void requireIncomplete() { + if (pos >= signature.length()) { + throw new IllegalStateException("Unexpected end of input"); + } + } +} diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java index 928b631ba..ad182bf18 100644 --- a/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java @@ -23,6 +23,7 @@ import java.util.stream.LongStream; import java.util.stream.Stream; +import org.eclipse.microprofile.openapi.OASConfig; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.DiscriminatorMapping; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -791,4 +792,34 @@ class Class1 { printToConsole(result); assertJsonEquals("components.schemas.no-self-ref-for-property-schema.json", result); } + + @Test + @SuppressWarnings("unused") + void testParameterizedTypeSchemaConfig() throws IOException, JSONException { + class Nullable { + T value; + boolean isPresent; + + boolean isPresent() { + return isPresent; + } + } + + @Schema(name = "Bean") + class Bean { + Nullable nullableString; + } + + Index index = indexOf(Nullable.class, Bean.class); + String nullableStringArySig = Nullable.class.getName() + ""; + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(dynamicConfig( + OASConfig.SCHEMA_PREFIX + nullableStringArySig, + "{ \"name\": \"NullableStringArray\", \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"nullable\": true }"), + index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.parameterized-type-schema-config.json", result); + } } diff --git a/core/src/test/java/io/smallrye/openapi/runtime/util/TypeParserTest.java b/core/src/test/java/io/smallrye/openapi/runtime/util/TypeParserTest.java new file mode 100644 index 000000000..10d2f059a --- /dev/null +++ b/core/src/test/java/io/smallrye/openapi/runtime/util/TypeParserTest.java @@ -0,0 +1,61 @@ +package io.smallrye.openapi.runtime.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.jboss.jandex.Type; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TypeParserTest { + + @Test + void testParameterizedArrayComponent() { + Type result = TypeParser.parse("java.util.List[][]"); + assertNotNull(result); + assertEquals(Type.Kind.ARRAY, result.kind()); + assertEquals(2, result.asArrayType().dimensions()); + assertEquals(Type.Kind.PARAMETERIZED_TYPE, result.asArrayType().constituent().kind()); + + Type typeArg = result.asArrayType().constituent().asParameterizedType().arguments().get(0); + + assertEquals(Type.Kind.WILDCARD_TYPE, typeArg.kind()); + assertEquals(Type.Kind.ARRAY, typeArg.asWildcardType().extendsBound().kind()); + assertEquals(1, typeArg.asWildcardType().extendsBound().asArrayType().dimensions()); + assertEquals(Type.Kind.CLASS, typeArg.asWildcardType().extendsBound().asArrayType().constituent().kind()); + assertEquals(String.class.getName(), + typeArg.asWildcardType().extendsBound().asArrayType().constituent().asClassType().name().toString()); + } + + @Test + void testPrimitive() { + Type result = TypeParser.parse("float"); + assertNotNull(result); + assertEquals(Type.Kind.PRIMITIVE, result.kind()); + assertEquals("float", result.name().toString()); + } + + @Test + void testClassType() { + Type result = TypeParser.parse("java.lang.Object"); + assertNotNull(result); + assertEquals(Type.Kind.CLASS, result.kind()); + assertEquals("java.lang.Object", result.name().toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "java.util.List", + "java.util.List", + "java.util.List" + }) + void testWildcards(String typeSignature) { + Type result = TypeParser.parse(typeSignature); + assertNotNull(result); + assertEquals(Type.Kind.PARAMETERIZED_TYPE, result.kind()); + assertEquals("java.util.List", result.name().toString()); + assertEquals(1, result.asParameterizedType().arguments().size()); + assertEquals(Type.Kind.WILDCARD_TYPE, result.asParameterizedType().arguments().get(0).kind()); + } +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.parameterized-type-schema-config.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.parameterized-type-schema-config.json new file mode 100644 index 000000000..472f9e518 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.parameterized-type-schema-config.json @@ -0,0 +1,22 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "Bean" : { + "type" : "object", + "properties" : { + "nullableString" : { + "$ref" : "#/components/schemas/NullableStringArray" + } + } + }, + "NullableStringArray" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "nullable" : true + } + } + } +}