Skip to content

Commit 2b9d5c1

Browse files
authored
fix: add missing constraint scanning for REST responses and types (#2064)
Signed-off-by: Michael Edgar <[email protected]>
1 parent 780153e commit 2b9d5c1

File tree

15 files changed

+407
-40
lines changed

15 files changed

+407
-40
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package io.smallrye.openapi.internal.support;
2+
3+
import java.util.Collection;
4+
import java.util.Collections;
5+
6+
import org.jboss.jandex.AnnotationInstance;
7+
import org.jboss.jandex.AnnotationTarget;
8+
import org.jboss.jandex.ClassInfo;
9+
import org.jboss.jandex.Declaration;
10+
import org.jboss.jandex.DotName;
11+
import org.jboss.jandex.FieldInfo;
12+
import org.jboss.jandex.IndexView;
13+
import org.jboss.jandex.MethodInfo;
14+
import org.jboss.jandex.MethodParameterInfo;
15+
import org.jboss.jandex.RecordComponentInfo;
16+
import org.jboss.jandex.Type;
17+
import org.jboss.jandex.TypeTarget;
18+
19+
/**
20+
* Simple wrapper type that may be used to allow a Type to be accessed like
21+
* an AnnotationTarget.
22+
*/
23+
public final class SimpleTypeTarget implements AnnotationTarget {
24+
25+
public static final SimpleTypeTarget create(Type type) {
26+
return new SimpleTypeTarget(type);
27+
}
28+
29+
private final Type type;
30+
31+
private SimpleTypeTarget(Type type) {
32+
this.type = type;
33+
}
34+
35+
@Override
36+
public Kind kind() {
37+
return Kind.TYPE;
38+
}
39+
40+
@Override
41+
public boolean isDeclaration() {
42+
return false;
43+
}
44+
45+
@Override
46+
public Declaration asDeclaration() {
47+
throw new UnsupportedOperationException();
48+
}
49+
50+
@Override
51+
public ClassInfo asClass() {
52+
throw new UnsupportedOperationException();
53+
}
54+
55+
@Override
56+
public FieldInfo asField() {
57+
throw new UnsupportedOperationException();
58+
}
59+
60+
@Override
61+
public MethodInfo asMethod() {
62+
throw new UnsupportedOperationException();
63+
}
64+
65+
@Override
66+
public MethodParameterInfo asMethodParameter() {
67+
throw new UnsupportedOperationException();
68+
}
69+
70+
@Override
71+
public TypeTarget asType() {
72+
throw new UnsupportedOperationException("Not a TypeTarget");
73+
}
74+
75+
@Override
76+
public RecordComponentInfo asRecordComponent() {
77+
throw new UnsupportedOperationException();
78+
}
79+
80+
@Override
81+
public boolean hasAnnotation(DotName name) {
82+
return type.hasAnnotation(name);
83+
}
84+
85+
@Override
86+
public AnnotationInstance annotation(DotName name) {
87+
return type.annotation(name);
88+
}
89+
90+
@Override
91+
public Collection<AnnotationInstance> annotations(DotName name) {
92+
return Collections.singletonList(type.annotation(name));
93+
}
94+
95+
@Override
96+
public Collection<AnnotationInstance> annotationsWithRepeatable(DotName name, IndexView index) {
97+
return type.annotationsWithRepeatable(name, index);
98+
}
99+
100+
@Override
101+
public Collection<AnnotationInstance> annotations() {
102+
return type.annotations();
103+
}
104+
105+
@Override
106+
public boolean hasDeclaredAnnotation(DotName name) {
107+
return type.hasAnnotation(name);
108+
}
109+
110+
@Override
111+
public AnnotationInstance declaredAnnotation(DotName name) {
112+
return type.annotation(name);
113+
}
114+
115+
@Override
116+
public Collection<AnnotationInstance> declaredAnnotationsWithRepeatable(DotName name, IndexView index) {
117+
return type.annotationsWithRepeatable(name, index);
118+
}
119+
120+
@Override
121+
public Collection<AnnotationInstance> declaredAnnotations() {
122+
return type.annotations();
123+
}
124+
}

core/src/main/java/io/smallrye/openapi/runtime/io/parameters/RequestBodyIO.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ private RequestBody readRequestSchema(AnnotationInstance annotation) {
9393
MediaType type = OASFactory.createMediaType();
9494
type.setSchema(SchemaFactory.typeToSchema(scannerContext(),
9595
value(annotation, PROP_VALUE),
96-
null,
97-
scannerContext().getExtensions()));
96+
null));
9897
content.addMediaType(mediaType, type);
9998
}
10099

core/src/main/java/io/smallrye/openapi/runtime/io/responses/APIResponseIO.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ Map<String, APIResponse> readResponseSchema(AnnotationInstance annotation) {
7070
Content content = OASFactory.createContent();
7171
Schema responseSchema = SchemaFactory.typeToSchema(scannerContext(),
7272
responseType,
73-
null,
74-
scannerContext().getExtensions());
73+
null);
7574

7675
for (String mediaType : mediaTypes) {
7776
content.addMediaType(mediaType, OASFactory.createMediaType().schema(responseSchema));

core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension;
3636
import io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner;
3737
import io.smallrye.openapi.runtime.scanner.SchemaRegistry;
38+
import io.smallrye.openapi.runtime.scanner.dataobject.BeanValidationScanner;
3839
import io.smallrye.openapi.runtime.scanner.dataobject.EnumProcessor;
3940
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner;
4041
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
@@ -544,20 +545,18 @@ static Schema readClassSchema(final AnnotationScannerContext context, Type type,
544545
* @return Schema model
545546
*/
546547
public static Schema typeToSchema(AnnotationScannerContext context, Type type) {
547-
return typeToSchema(context, type, null, context.getExtensions());
548+
return typeToSchema(context, type, null);
548549
}
549550

550551
/**
551552
* Converts a Jandex type to a {@link Schema} model.
552553
*
553554
* @param context scanning context
554555
* @param type the implementation type of the item to scan
555-
* @param extensions list of AnnotationScannerExtensions
556556
* @return Schema model
557557
*/
558558
public static Schema typeToSchema(final AnnotationScannerContext context, Type type,
559-
AnnotationInstance schemaAnnotation,
560-
List<AnnotationScannerExtension> extensions) {
559+
AnnotationInstance schemaAnnotation) {
561560
Schema schema = null;
562561
Schema fromAnnotation = null;
563562

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

575574
if (TypeUtil.isWrappedType(type)) {
576575
// Recurse using the optional's type
577-
schema = typeToSchema(context, TypeUtil.unwrapType(type), null, extensions);
576+
schema = typeToSchema(context, TypeUtil.unwrapType(type), null);
578577
} else if (currentScanner.isPresent() && currentScanner.get().isWrapperType(type)) {
579578
// Recurse using the wrapped type
580-
schema = typeToSchema(context, currentScanner.get().unwrapType(type), null,
581-
extensions);
579+
schema = typeToSchema(context, currentScanner.get().unwrapType(type), null);
582580
} else if (TypeUtil.isTerminalType(type)) {
583581
schema = OASFactory.createSchema();
584582
TypeUtil.applyTypeAttributes(type, schema);
@@ -592,19 +590,21 @@ public static Schema typeToSchema(final AnnotationScannerContext context, Type t
592590
if (dimensions > 1) {
593591
// Recurse using a new array type with dimensions decremented
594592
schema.setItems(
595-
typeToSchema(context, ArrayType.create(componentType, dimensions - 1), null, extensions));
593+
typeToSchema(context, ArrayType.create(componentType, dimensions - 1), null));
596594
} else {
597595
// Recurse using the type of the array elements
598-
schema.setItems(typeToSchema(context, componentType, null, extensions));
596+
schema.setItems(typeToSchema(context, componentType, null));
599597
}
600598
} else if (type.kind() == Type.Kind.CLASS) {
601599
schema = introspectClassToSchema(context, type.asClassType(), true);
602600
} else if (type.kind() == Type.Kind.PRIMITIVE) {
603601
schema = OpenApiDataObjectScanner.process(type.asPrimitiveType());
604602
} else {
605-
schema = otherTypeToSchema(context, type, extensions);
603+
schema = otherTypeToSchema(context, type);
606604
}
607605

606+
BeanValidationScanner.applyConstraints(context, type, schema);
607+
608608
if (fromAnnotation != null) {
609609
// Generate `allOf` ?
610610
schema = MergeUtil.mergeObjects(schema, fromAnnotation);
@@ -768,32 +768,30 @@ private static List<Schema> readClassSchemas(final AnnotationScannerContext cont
768768
.collect(Collectors.toList());
769769
}
770770

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

778777
// Recurse using the type of the array elements
779-
schema.setItems(typeToSchema(context, componentType, null, extensions));
778+
schema.setItems(typeToSchema(context, componentType));
780779
return schema;
781780
} else {
782-
Type asyncType = resolveAsyncType(context, type, extensions);
781+
Type asyncType = resolveAsyncType(context, type);
783782
return schemaRegistration(context, asyncType, OpenApiDataObjectScanner.process(context, asyncType));
784783
}
785784
}
786785

787-
static Type resolveAsyncType(final AnnotationScannerContext context, Type type,
788-
List<AnnotationScannerExtension> extensions) {
786+
static Type resolveAsyncType(final AnnotationScannerContext context, Type type) {
789787
if (type.kind() == Type.Kind.PARAMETERIZED_TYPE) {
790788
ParameterizedType pType = type.asParameterizedType();
791789
if (pType.arguments().size() == 1 &&
792790
(TypeUtil.isA(context, type, JDKConstants.COMPLETION_STAGE_TYPE))) {
793791
return pType.arguments().get(0);
794792
}
795793
}
796-
for (AnnotationScannerExtension extension : extensions) {
794+
for (AnnotationScannerExtension extension : context.getExtensions()) {
797795
Type asyncType = extension.resolveAsyncType(type);
798796
if (asyncType != null)
799797
return asyncType;

core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiAnnotationScanner.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ private void processClassSchemas(final AnnotationScannerContext context) {
313313
.filter(this::annotatedClasses)
314314
.map(annotation -> Type.create(annotation.target().asClass().name(), Type.Kind.CLASS))
315315
.sorted(Comparator.comparing(Type::name)) // Process annotation classes in predictable order
316-
.forEach(type -> SchemaFactory.typeToSchema(context, type, null, context.getExtensions()));
316+
.forEach(type -> SchemaFactory.typeToSchema(context, type, null));
317317
}
318318

319319
private boolean annotatedClasses(AnnotationInstance annotation) {

core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/BeanValidationScanner.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.Arrays;
88
import java.util.HashSet;
99
import java.util.List;
10+
import java.util.Optional;
1011
import java.util.Set;
1112

1213
import org.eclipse.microprofile.openapi.models.media.Schema;
@@ -20,6 +21,7 @@
2021
import io.smallrye.openapi.api.constants.JacksonConstants;
2122
import io.smallrye.openapi.api.constants.KotlinConstants;
2223
import io.smallrye.openapi.internal.models.media.SchemaSupport;
24+
import io.smallrye.openapi.internal.support.SimpleTypeTarget;
2325
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
2426

2527
/**
@@ -202,6 +204,49 @@ public void applyConstraints(AnnotationTarget target,
202204
}
203205
}
204206

207+
/**
208+
* Like {@link #applyConstraints(AnnotationTarget, Schema, String, RequirementHandler)},
209+
* but for constraints on {@link Type}s.
210+
*
211+
* @param target
212+
* the object from which to retrieve the constraint annotations
213+
* @param schema
214+
* the schema to which the constraints will be applied
215+
* @param propertyKey
216+
* the name of the property in parentSchema that refers to the
217+
* schema
218+
* @param handler
219+
* the handler to be called when a
220+
* bean validation @NotNull constraint is encountered.
221+
*/
222+
public void applyConstraints(Type target,
223+
Schema schema,
224+
String propertyKey,
225+
RequirementHandler handler) {
226+
applyConstraints(SimpleTypeTarget.create(target), schema, propertyKey, handler);
227+
}
228+
229+
/**
230+
* Apply validation constraints found on the type to the schema, if bean
231+
* validation scanning is enabled on the given context.
232+
*
233+
* @param context
234+
* current scanning context
235+
* @param type
236+
* the object from which to retrieve the constraint annotations
237+
* @param schema
238+
* the schema to which the constraints will be applied
239+
*/
240+
public static void applyConstraints(AnnotationScannerContext context, Type type, Schema schema) {
241+
Optional<BeanValidationScanner> constraintScanner = context.getBeanValidationScanner();
242+
243+
if (schema != null && constraintScanner.isPresent()) {
244+
constraintScanner.get().applyConstraints(type, schema, null,
245+
(target, name) -> {
246+
/* Nothing additional to do for @NotNull */ });
247+
}
248+
}
249+
205250
private void applyStringConstraints(AnnotationTarget target,
206251
Schema schema,
207252
String propertyKey,

core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ private Schema readGenericValueType(Type valueType) {
297297
valueSchema = resolveParameterizedType(valueType, valueSchema);
298298
}
299299

300+
BeanValidationScanner.applyConstraints(context, valueType, valueSchema);
301+
300302
return valueSchema;
301303
}
302304

core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,22 @@ private static Type[] resolveArguments(ParameterizedType type, UnaryOperator<Typ
414414
*/
415415
private Type getResolvedType(ParameterizedType type) {
416416
if (type.arguments().stream().noneMatch(arg -> arg.kind() == Type.Kind.WILDCARD_TYPE)) {
417-
return ParameterizedType.create(type.name(), resolveArguments(type, this::resolve), null);
417+
ParameterizedType.Builder builder = ParameterizedType.builder(type.name());
418+
419+
Type owner = type.owner();
420+
if (owner != null) {
421+
builder.setOwner(resolve(owner));
422+
}
423+
424+
for (AnnotationInstance anno : type.annotations()) {
425+
builder.addAnnotation(anno);
426+
}
427+
428+
for (Type arg : resolveArguments(type, this::resolve)) {
429+
builder.addArgument(arg);
430+
}
431+
432+
return builder.build();
418433
}
419434

420435
return getResolvedType((Type) type);

core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ void mapParameterSchema(Parameter param, ParameterContext context) {
572572
// readSchema *may* replace the existing schema, so we must assign.
573573
schema = SchemaFactory.readSchema(scannerContext, OASFactory.createSchema(), schemaAnnotation, defaults);
574574
} else {
575-
schema = SchemaFactory.typeToSchema(scannerContext, context.targetType, schemaAnnotation, extensions);
575+
schema = SchemaFactory.typeToSchema(scannerContext, context.targetType, schemaAnnotation);
576576
}
577577

578578
ModelUtil.setParameterSchema(param, schema);
@@ -681,8 +681,7 @@ protected void setSchemaProperties(Schema schema,
681681
if (schemaAnnotationSupported) {
682682
schemaAnnotation = scannerContext.annotations().getAnnotation(paramTarget, SchemaConstant.DOTNAME_SCHEMA);
683683
}
684-
Schema paramSchema = SchemaFactory.typeToSchema(scannerContext, paramType,
685-
schemaAnnotation, extensions);
684+
Schema paramSchema = SchemaFactory.typeToSchema(scannerContext, paramType, schemaAnnotation);
686685

687686
if (paramSchema == null) {
688687
// hidden

0 commit comments

Comments
 (0)