Skip to content

Commit

Permalink
fix: add missing constraint scanning for REST responses and types (#2064
Browse files Browse the repository at this point in the history
)

Signed-off-by: Michael Edgar <[email protected]>
  • Loading branch information
MikeEdgar authored Nov 13, 2024
1 parent 780153e commit 2b9d5c1
Show file tree
Hide file tree
Showing 15 changed files with 407 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package io.smallrye.openapi.internal.support;

import java.util.Collection;
import java.util.Collections;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.Declaration;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.RecordComponentInfo;
import org.jboss.jandex.Type;
import org.jboss.jandex.TypeTarget;

/**
* Simple wrapper type that may be used to allow a Type to be accessed like
* an AnnotationTarget.
*/
public final class SimpleTypeTarget implements AnnotationTarget {

public static final SimpleTypeTarget create(Type type) {
return new SimpleTypeTarget(type);
}

private final Type type;

private SimpleTypeTarget(Type type) {
this.type = type;
}

@Override
public Kind kind() {
return Kind.TYPE;
}

@Override
public boolean isDeclaration() {
return false;
}

@Override
public Declaration asDeclaration() {
throw new UnsupportedOperationException();
}

@Override
public ClassInfo asClass() {
throw new UnsupportedOperationException();
}

@Override
public FieldInfo asField() {
throw new UnsupportedOperationException();
}

@Override
public MethodInfo asMethod() {
throw new UnsupportedOperationException();
}

@Override
public MethodParameterInfo asMethodParameter() {
throw new UnsupportedOperationException();
}

@Override
public TypeTarget asType() {
throw new UnsupportedOperationException("Not a TypeTarget");
}

@Override
public RecordComponentInfo asRecordComponent() {
throw new UnsupportedOperationException();
}

@Override
public boolean hasAnnotation(DotName name) {
return type.hasAnnotation(name);
}

@Override
public AnnotationInstance annotation(DotName name) {
return type.annotation(name);
}

@Override
public Collection<AnnotationInstance> annotations(DotName name) {
return Collections.singletonList(type.annotation(name));
}

@Override
public Collection<AnnotationInstance> annotationsWithRepeatable(DotName name, IndexView index) {
return type.annotationsWithRepeatable(name, index);
}

@Override
public Collection<AnnotationInstance> annotations() {
return type.annotations();
}

@Override
public boolean hasDeclaredAnnotation(DotName name) {
return type.hasAnnotation(name);
}

@Override
public AnnotationInstance declaredAnnotation(DotName name) {
return type.annotation(name);
}

@Override
public Collection<AnnotationInstance> declaredAnnotationsWithRepeatable(DotName name, IndexView index) {
return type.annotationsWithRepeatable(name, index);
}

@Override
public Collection<AnnotationInstance> declaredAnnotations() {
return type.annotations();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ private RequestBody readRequestSchema(AnnotationInstance annotation) {
MediaType type = OASFactory.createMediaType();
type.setSchema(SchemaFactory.typeToSchema(scannerContext(),
value(annotation, PROP_VALUE),
null,
scannerContext().getExtensions()));
null));
content.addMediaType(mediaType, type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ Map<String, APIResponse> readResponseSchema(AnnotationInstance annotation) {
Content content = OASFactory.createContent();
Schema responseSchema = SchemaFactory.typeToSchema(scannerContext(),
responseType,
null,
scannerContext().getExtensions());
null);

for (String mediaType : mediaTypes) {
content.addMediaType(mediaType, OASFactory.createMediaType().schema(responseSchema));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension;
import io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner;
import io.smallrye.openapi.runtime.scanner.SchemaRegistry;
import io.smallrye.openapi.runtime.scanner.dataobject.BeanValidationScanner;
import io.smallrye.openapi.runtime.scanner.dataobject.EnumProcessor;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
Expand Down Expand Up @@ -544,20 +545,18 @@ static Schema readClassSchema(final AnnotationScannerContext context, Type type,
* @return Schema model
*/
public static Schema typeToSchema(AnnotationScannerContext context, Type type) {
return typeToSchema(context, type, null, context.getExtensions());
return typeToSchema(context, type, null);
}

/**
* Converts a Jandex type to a {@link Schema} model.
*
* @param context scanning context
* @param type the implementation type of the item to scan
* @param extensions list of AnnotationScannerExtensions
* @return Schema model
*/
public static Schema typeToSchema(final AnnotationScannerContext context, Type type,
AnnotationInstance schemaAnnotation,
List<AnnotationScannerExtension> extensions) {
AnnotationInstance schemaAnnotation) {
Schema schema = null;
Schema fromAnnotation = null;

Expand All @@ -574,11 +573,10 @@ public static Schema typeToSchema(final AnnotationScannerContext context, Type t

if (TypeUtil.isWrappedType(type)) {
// Recurse using the optional's type
schema = typeToSchema(context, TypeUtil.unwrapType(type), null, extensions);
schema = typeToSchema(context, TypeUtil.unwrapType(type), null);
} else if (currentScanner.isPresent() && currentScanner.get().isWrapperType(type)) {
// Recurse using the wrapped type
schema = typeToSchema(context, currentScanner.get().unwrapType(type), null,
extensions);
schema = typeToSchema(context, currentScanner.get().unwrapType(type), null);
} else if (TypeUtil.isTerminalType(type)) {
schema = OASFactory.createSchema();
TypeUtil.applyTypeAttributes(type, schema);
Expand All @@ -592,19 +590,21 @@ public static Schema typeToSchema(final AnnotationScannerContext context, Type t
if (dimensions > 1) {
// Recurse using a new array type with dimensions decremented
schema.setItems(
typeToSchema(context, ArrayType.create(componentType, dimensions - 1), null, extensions));
typeToSchema(context, ArrayType.create(componentType, dimensions - 1), null));
} else {
// Recurse using the type of the array elements
schema.setItems(typeToSchema(context, componentType, null, extensions));
schema.setItems(typeToSchema(context, componentType, null));
}
} else if (type.kind() == Type.Kind.CLASS) {
schema = introspectClassToSchema(context, type.asClassType(), true);
} else if (type.kind() == Type.Kind.PRIMITIVE) {
schema = OpenApiDataObjectScanner.process(type.asPrimitiveType());
} else {
schema = otherTypeToSchema(context, type, extensions);
schema = otherTypeToSchema(context, type);
}

BeanValidationScanner.applyConstraints(context, type, schema);

if (fromAnnotation != null) {
// Generate `allOf` ?
schema = MergeUtil.mergeObjects(schema, fromAnnotation);
Expand Down Expand Up @@ -768,32 +768,30 @@ private static List<Schema> readClassSchemas(final AnnotationScannerContext cont
.collect(Collectors.toList());
}

private static Schema otherTypeToSchema(final AnnotationScannerContext context, Type type,
List<AnnotationScannerExtension> extensions) {
private static Schema otherTypeToSchema(final AnnotationScannerContext context, Type type) {
if (TypeUtil.isA(context, type, MutinyConstants.MULTI_TYPE)) {
// Treat as an Array
Schema schema = OASFactory.createSchema().addType(SchemaType.ARRAY);
Type componentType = type.asParameterizedType().arguments().get(0);

// Recurse using the type of the array elements
schema.setItems(typeToSchema(context, componentType, null, extensions));
schema.setItems(typeToSchema(context, componentType));
return schema;
} else {
Type asyncType = resolveAsyncType(context, type, extensions);
Type asyncType = resolveAsyncType(context, type);
return schemaRegistration(context, asyncType, OpenApiDataObjectScanner.process(context, asyncType));
}
}

static Type resolveAsyncType(final AnnotationScannerContext context, Type type,
List<AnnotationScannerExtension> extensions) {
static Type resolveAsyncType(final AnnotationScannerContext context, Type type) {
if (type.kind() == Type.Kind.PARAMETERIZED_TYPE) {
ParameterizedType pType = type.asParameterizedType();
if (pType.arguments().size() == 1 &&
(TypeUtil.isA(context, type, JDKConstants.COMPLETION_STAGE_TYPE))) {
return pType.arguments().get(0);
}
}
for (AnnotationScannerExtension extension : extensions) {
for (AnnotationScannerExtension extension : context.getExtensions()) {
Type asyncType = extension.resolveAsyncType(type);
if (asyncType != null)
return asyncType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ private void processClassSchemas(final AnnotationScannerContext context) {
.filter(this::annotatedClasses)
.map(annotation -> Type.create(annotation.target().asClass().name(), Type.Kind.CLASS))
.sorted(Comparator.comparing(Type::name)) // Process annotation classes in predictable order
.forEach(type -> SchemaFactory.typeToSchema(context, type, null, context.getExtensions()));
.forEach(type -> SchemaFactory.typeToSchema(context, type, null));
}

private boolean annotatedClasses(AnnotationInstance annotation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.eclipse.microprofile.openapi.models.media.Schema;
Expand All @@ -20,6 +21,7 @@
import io.smallrye.openapi.api.constants.JacksonConstants;
import io.smallrye.openapi.api.constants.KotlinConstants;
import io.smallrye.openapi.internal.models.media.SchemaSupport;
import io.smallrye.openapi.internal.support.SimpleTypeTarget;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;

/**
Expand Down Expand Up @@ -202,6 +204,49 @@ public void applyConstraints(AnnotationTarget target,
}
}

/**
* Like {@link #applyConstraints(AnnotationTarget, Schema, String, RequirementHandler)},
* but for constraints on {@link Type}s.
*
* @param target
* the object from which to retrieve the constraint annotations
* @param schema
* the schema to which the constraints will be applied
* @param propertyKey
* the name of the property in parentSchema that refers to the
* schema
* @param handler
* the handler to be called when a
* bean validation @NotNull constraint is encountered.
*/
public void applyConstraints(Type target,
Schema schema,
String propertyKey,
RequirementHandler handler) {
applyConstraints(SimpleTypeTarget.create(target), schema, propertyKey, handler);
}

/**
* Apply validation constraints found on the type to the schema, if bean
* validation scanning is enabled on the given context.
*
* @param context
* current scanning context
* @param type
* the object from which to retrieve the constraint annotations
* @param schema
* the schema to which the constraints will be applied
*/
public static void applyConstraints(AnnotationScannerContext context, Type type, Schema schema) {
Optional<BeanValidationScanner> constraintScanner = context.getBeanValidationScanner();

if (schema != null && constraintScanner.isPresent()) {
constraintScanner.get().applyConstraints(type, schema, null,
(target, name) -> {
/* Nothing additional to do for @NotNull */ });
}
}

private void applyStringConstraints(AnnotationTarget target,
Schema schema,
String propertyKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ private Schema readGenericValueType(Type valueType) {
valueSchema = resolveParameterizedType(valueType, valueSchema);
}

BeanValidationScanner.applyConstraints(context, valueType, valueSchema);

return valueSchema;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,22 @@ private static Type[] resolveArguments(ParameterizedType type, UnaryOperator<Typ
*/
private Type getResolvedType(ParameterizedType type) {
if (type.arguments().stream().noneMatch(arg -> arg.kind() == Type.Kind.WILDCARD_TYPE)) {
return ParameterizedType.create(type.name(), resolveArguments(type, this::resolve), null);
ParameterizedType.Builder builder = ParameterizedType.builder(type.name());

Type owner = type.owner();
if (owner != null) {
builder.setOwner(resolve(owner));
}

for (AnnotationInstance anno : type.annotations()) {
builder.addAnnotation(anno);
}

for (Type arg : resolveArguments(type, this::resolve)) {
builder.addArgument(arg);
}

return builder.build();
}

return getResolvedType((Type) type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ void mapParameterSchema(Parameter param, ParameterContext context) {
// readSchema *may* replace the existing schema, so we must assign.
schema = SchemaFactory.readSchema(scannerContext, OASFactory.createSchema(), schemaAnnotation, defaults);
} else {
schema = SchemaFactory.typeToSchema(scannerContext, context.targetType, schemaAnnotation, extensions);
schema = SchemaFactory.typeToSchema(scannerContext, context.targetType, schemaAnnotation);
}

ModelUtil.setParameterSchema(param, schema);
Expand Down Expand Up @@ -681,8 +681,7 @@ protected void setSchemaProperties(Schema schema,
if (schemaAnnotationSupported) {
schemaAnnotation = scannerContext.annotations().getAnnotation(paramTarget, SchemaConstant.DOTNAME_SCHEMA);
}
Schema paramSchema = SchemaFactory.typeToSchema(scannerContext, paramType,
schemaAnnotation, extensions);
Schema paramSchema = SchemaFactory.typeToSchema(scannerContext, paramType, schemaAnnotation);

if (paramSchema == null) {
// hidden
Expand Down
Loading

0 comments on commit 2b9d5c1

Please sign in to comment.