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

fix: add missing constraint scanning for REST responses and types #2064

Merged
merged 1 commit into from
Nov 13, 2024
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
@@ -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