Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support configuration of schemas for complex types #1609

Merged
merged 1 commit into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
}
}