Skip to content

Commit

Permalink
Support configuration of schemas for complex types (#1609)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Edgar <[email protected]>
  • Loading branch information
MikeEdgar authored Oct 12, 2023
1 parent a89992d commit 3fd711c
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -210,24 +211,31 @@ 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()
.map(ext -> ext.parseSchema(jsonSchema))
.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);
});
}

Expand Down
198 changes: 198 additions & 0 deletions core/src/main/java/io/smallrye/openapi/runtime/util/TypeParser.java
Original file line number Diff line number Diff line change
@@ -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<Type> 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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> {
T value;
boolean isPresent;

boolean isPresent() {
return isPresent;
}
}

@Schema(name = "Bean")
class Bean {
Nullable<String[]> nullableString;
}

Index index = indexOf(Nullable.class, Bean.class);
String nullableStringArySig = Nullable.class.getName() + "<java.lang.String[]>";
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);
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends java.lang.String[]>[][]");
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<? super java.lang.String>",
"java.util.List<? extends java.lang.String>",
"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());
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}

0 comments on commit 3fd711c

Please sign in to comment.