Skip to content

Commit

Permalink
schema resolution options - Phase 2: global allOf
Browse files Browse the repository at this point in the history
  • Loading branch information
frantuma committed Sep 23, 2024
1 parent e924632 commit 5fc5d0e
Show file tree
Hide file tree
Showing 17 changed files with 607 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public ModelConverters(boolean openapi31) {
public ModelConverters(boolean openapi31, Schema.SchemaResolution schemaResolution) {
converters = new CopyOnWriteArrayList<>();
if (openapi31) {
converters.add(new ModelResolver(Json31.mapper()).openapi31(true));
converters.add(new ModelResolver(Json31.mapper()).openapi31(true).schemaResolution(schemaResolution));
} else {
converters.add(new ModelResolver(Json.mapper()).schemaResolution(schemaResolution));
}
Expand Down Expand Up @@ -81,7 +81,7 @@ public static ModelConverters getInstance(boolean openapi31, Schema.SchemaResolu
synchronized (ModelConverters.class) {
if (openapi31) {
if (SINGLETON31 == null) {
SINGLETON31 = new ModelConverters(openapi31);
SINGLETON31 = new ModelConverters(openapi31, Schema.SchemaResolution.DEFAULT);
init(SINGLETON31);
}
return SINGLETON31;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context

Annotation[] ctxAnnotation31 = null;

if (openapi31) {
if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) || openapi31) {
List<Annotation> ctxAnnotations31List = new ArrayList<>();
if (annotations != null) {
for (Annotation a : annotations) {
Expand All @@ -701,15 +701,18 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context

AnnotatedType aType = new AnnotatedType()
.type(propType)
.ctxAnnotations(openapi31 ? ctxAnnotation31 : annotations)
.parent(model)
.resolveAsRef(annotatedType.isResolveAsRef())
.jsonViewAnnotation(annotatedType.getJsonViewAnnotation())
.skipSchemaName(true)
.schemaProperty(true)
.components(annotatedType.getComponents())
.propertyName(propName);

if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) || openapi31) {
aType.ctxAnnotations(ctxAnnotation31);
} else {
aType.ctxAnnotations(annotations);
}
final AnnotatedMember propMember = member;
aType.jsonUnwrappedHandler(t -> {
JsonUnwrapped uw = propMember.getAnnotation(JsonUnwrapped.class);
Expand All @@ -726,6 +729,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
});
property = context.resolve(aType);
property = clone(property);
Schema ctxProperty = null;
if (openapi31) {
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, schemaResolution, context);
if (reResolvedProperty.isPresent()) {
Expand All @@ -736,6 +740,16 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
property = reResolvedProperty.get();
}

} else if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution)) {
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, null, schemaResolution, context);
if (reResolvedProperty.isPresent()) {
ctxProperty = reResolvedProperty.get();
}
reResolvedProperty = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null, openapi31, ctxProperty);
if (reResolvedProperty.isPresent()) {
ctxProperty = reResolvedProperty.get();
}

}
if (property != null) {
Boolean required = md.getRequired();
Expand Down Expand Up @@ -777,10 +791,15 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
if (context.getDefinedModels().containsKey(pName)) {
if (Schema.SchemaResolution.INLINE.equals(this.schemaResolution)) {
property = context.getDefinedModels().get(pName);
} else if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) && ctxProperty != null) {
property = new Schema()
.addAllOfItem(ctxProperty)
.addAllOfItem(new Schema().$ref(constructRef(pName)));
} else {
property = new Schema().$ref(constructRef(pName));
}
property = clone(property);
// TODO: why is this needed? is it not handled before?
if (openapi31 || Schema.SchemaResolution.INLINE.equals(this.schemaResolution)) {
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, this.schemaResolution, context);
if (reResolvedProperty.isPresent()) {
Expand All @@ -794,7 +813,13 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
}
} else if (property.get$ref() != null) {
if (!openapi31) {
property = new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName());
if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) && ctxProperty != null) {
property = new Schema()
.addAllOfItem(ctxProperty)
.addAllOfItem(new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName()));
} else {
property = new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName());
}
} else {
if (StringUtils.isEmpty(property.get$ref())) {
property.$ref(property.getName());
Expand All @@ -807,9 +832,12 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
if (property != null && io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED.equals(requiredMode)) {
addRequiredItem(model, property.getName());
}
if (ctxProperty == null) {
ctxProperty = property;
}
final boolean applyNotNullAnnotations = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.AUTO.equals(requiredMode);
annotations = addGenericTypeArgumentAnnotationsForOptionalField(propDef, annotations);
applyBeanValidatorAnnotations(propDef, property, annotations, model, applyNotNullAnnotations);
applyBeanValidatorAnnotations(propDef, ctxProperty, annotations, model, applyNotNullAnnotations);

props.add(property);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,12 @@ public static Optional<Schema> getSchemaFromAnnotation(
} else {
schemaObject = new Schema();
}
} else if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
if (existingSchema == null) {
schemaObject = new Schema();
} else {
schemaObject = existingSchema;
}
}
} else {
if (existingSchema == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ public static Parameter applyAnnotations(
String[] methodTypes,
JsonView jsonViewAnnotation,
boolean openapi31) {
return applyAnnotations(parameter, type, annotations, components, classTypes, methodTypes, jsonViewAnnotation, openapi31, null);
}

public static Parameter applyAnnotations(
Parameter parameter,
Type type,
List<Annotation> annotations,
Components components,
String[] classTypes,
String[] methodTypes,
JsonView jsonViewAnnotation,
boolean openapi31,
Schema.SchemaResolution schemaResolution) {

final AnnotationsHelper helper = new AnnotationsHelper(annotations, type);
if (helper.isContext()) {
Expand All @@ -59,17 +72,57 @@ public static Parameter applyAnnotations(
if (paramSchemaOrArrayAnnotation != null) {
reworkedAnnotations.add(paramSchemaOrArrayAnnotation);
}
io.swagger.v3.oas.annotations.media.Schema ctxSchema = AnnotationsUtils.getSchemaAnnotation(annotations.toArray(new Annotation[0]));
io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema = AnnotationsUtils.getArraySchemaAnnotation(annotations.toArray(new Annotation[0]));
Annotation[] ctxAnnotation31 = null;

if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
List<Annotation> ctxAnnotations31List = new ArrayList<>();
if (annotations != null) {
for (Annotation a : annotations) {
if (
!(a instanceof io.swagger.v3.oas.annotations.media.Schema) &&
!(a instanceof io.swagger.v3.oas.annotations.media.ArraySchema)) {
ctxAnnotations31List.add(a);
}
}
ctxAnnotation31 = ctxAnnotations31List.toArray(new Annotation[ctxAnnotations31List.size()]);
}
}
AnnotatedType annotatedType = new AnnotatedType()
.type(type)
.resolveAsRef(true)
.skipOverride(true)
.jsonViewAnnotation(jsonViewAnnotation)
.ctxAnnotations(reworkedAnnotations.toArray(new Annotation[reworkedAnnotations.size()]));
.jsonViewAnnotation(jsonViewAnnotation);

if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
annotatedType.ctxAnnotations(ctxAnnotation31);
} else {
annotatedType.ctxAnnotations(reworkedAnnotations.toArray(new Annotation[reworkedAnnotations.size()]));
}

final ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31).resolveAsResolvedSchema(annotatedType);
final ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31, schemaResolution).resolveAsResolvedSchema(annotatedType);

if (resolvedSchema.schema != null) {
parameter.setSchema(resolvedSchema.schema);
Schema resSchema = AnnotationsUtils.clone(resolvedSchema.schema, openapi31);
Schema ctxSchemaObject = null;
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
Optional<Schema> reResolvedSchema = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, null, schemaResolution, null);
if (reResolvedSchema.isPresent()) {
ctxSchemaObject = reResolvedSchema.get();
}
reResolvedSchema = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null, openapi31, ctxSchemaObject);
if (reResolvedSchema.isPresent()) {
ctxSchemaObject = reResolvedSchema.get();
}

}
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution) && ctxSchemaObject != null) {
resSchema = new Schema()
.addAllOfItem(ctxSchemaObject)
.addAllOfItem(resolvedSchema.schema);
}
parameter.setSchema(resSchema);
}
resolvedSchema.referencedSchemas.forEach(components::addSchemas);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package io.swagger.v3.core.resolving;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverterContextImpl;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.matchers.SerializationMatchers;
import io.swagger.v3.oas.annotations.media.Schema;
import org.testng.annotations.Test;

public class AllofResolvingTest extends SwaggerTestBase {

@Test
public void testAllofResolving() {

final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false).schemaResolution(io.swagger.v3.oas.models.media.Schema.SchemaResolution.ALL_OF);
final ModelConverterContextImpl c = new ModelConverterContextImpl(modelResolver);
// ModelConverters c = ModelConverters.getInstance(false, io.swagger.v3.oas.models.media.Schema.SchemaResolution.INLINE);
c.resolve(new AnnotatedType(UserSchema.class));

String expectedYaml = "UserProperty:\n" +
" type: object\n" +
" description: Represents a user-specific property\n" +
" example: User-specific example value\n" +
"UserSchema:\n" +
" type: object\n" +
" properties:\n" +
" propertyOne:\n" +
" allOf:\n" +
" - type: object\n" +
" description: First user schema property\n" +
" nullable: true\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyTwo:\n" +
" allOf:\n" +
" - type: object\n" +
" description: Second user schema property\n" +
" example: example value for propertyTwo\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyThree:\n" +
" allOf:\n" +
" - type: object\n" +
" description: \"Third user schema property, with example for testing\"\n" +
" example: example value for propertyThree\n" +
" - $ref: '#/components/schemas/UserProperty'\n";

SerializationMatchers.assertEqualsToYaml(c.getDefinedModels(), expectedYaml);
// stringSchemaMap = c.readAll(InlineSchemaSecond.class);
c.resolve(new AnnotatedType(OrderSchema.class));
expectedYaml = "BasicProperty:\n" +
" type: object\n" +
" description: Represents a basic schema property\n" +
"OrderProperty:\n" +
" type: object\n" +
" properties:\n" +
" basicProperty:\n" +
" $ref: '#/components/schemas/BasicProperty'\n" +
" description: Represents an order-specific property\n" +
" example: Order-specific example value\n" +
"OrderSchema:\n" +
" type: object\n" +
" properties:\n" +
" propertyOne:\n" +
" allOf:\n" +
" - type: object\n" +
" description: First order schema property\n" +
" nullable: true\n" +
" - $ref: '#/components/schemas/OrderProperty'\n" +
" userProperty:\n" +
" allOf:\n" +
" - type: object\n" +
" description: \"Order schema property, references UserProperty\"\n" +
" example: example value for userProperty\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
"UserProperty:\n" +
" type: object\n" +
" description: Represents a user-specific property\n" +
" example: User-specific example value\n" +
"UserSchema:\n" +
" type: object\n" +
" properties:\n" +
" propertyOne:\n" +
" allOf:\n" +
" - type: object\n" +
" description: First user schema property\n" +
" nullable: true\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyTwo:\n" +
" allOf:\n" +
" - type: object\n" +
" description: Second user schema property\n" +
" example: example value for propertyTwo\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyThree:\n" +
" allOf:\n" +
" - type: object\n" +
" description: \"Third user schema property, with example for testing\"\n" +
" example: example value for propertyThree\n" +
" - $ref: '#/components/schemas/UserProperty'\n";
SerializationMatchers.assertEqualsToYaml(c.getDefinedModels(), expectedYaml);
}

// Renamed class to better describe what it represents
static class UserSchema {

@Schema(description = "First user schema property", nullable = true)
public UserProperty propertyOne;

private UserProperty propertyTwo;

@Schema(description = "Second user schema property", example = "example value for propertyTwo")
public UserProperty getPropertyTwo() {
return propertyTwo;
}

// Third property with no specific annotation. It's good to add some description or example for clarity
@Schema(description = "Third user schema property, with example for testing", example = "example value for propertyThree")
public UserProperty getPropertyThree() {
return null; // returning null as per the test scenario
}
}

// Renamed class to represent a different entity for the schema test
static class OrderSchema {

@Schema(description = "First order schema property", nullable = true)
public OrderProperty propertyOne;

private UserProperty userProperty;

@Schema(description = "Order schema property, references UserProperty", example = "example value for userProperty")
public UserProperty getUserProperty() {
return userProperty;
}
}

// Renamed properties to make them clearer about their role in the schema
@Schema(description = "Represents a user-specific property", example = "User-specific example value")
static class UserProperty {
// public String value;
}

@Schema(description = "Represents an order-specific property", example = "Order-specific example value")
static class OrderProperty {
public BasicProperty basicProperty;
}

static class BasicSchema {

@Schema(description = "First basic schema property")
public BasicProperty propertyOne;

private BasicProperty propertyTwo;

@Schema(description = "Second basic schema property", example = "example value for propertyTwo")
public BasicProperty getPropertyTwo() {
return propertyTwo;
}
}

// Renamed to represent a basic property common in various schemas
@Schema(description = "Represents a basic schema property")
static class BasicProperty {
// public String value;
}
}
Loading

0 comments on commit 5fc5d0e

Please sign in to comment.