Skip to content

Commit 891b222

Browse files
authored
Multiple bug fixes with proper attribute inheritance (#661)
* test and fix for #659
1 parent 3bf53bb commit 891b222

3 files changed

Lines changed: 154 additions & 7 deletions

File tree

openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ protected void processSchemaProperty(VisitorContext context, Element element, Cl
799799
final boolean required = hasElementSchemaRequired(element).orElseGet(() -> isElementNotNullable(element, classElement));
800800
propertySchema = bindSchemaForElement(context, element, elementType, propertySchema);
801801
String propertyName = resolvePropertyName(element, classElement, propertySchema);
802+
propertySchema.setRequired(null);
802803
addProperty(parentSchema, propertyName, propertySchema, required);
803804
}
804805
}
@@ -885,6 +886,12 @@ protected Schema bindSchemaForElement(VisitorContext context, Element element, C
885886
Schema originalSchema = schemaToBind;
886887
if (originalSchema.get$ref() != null) {
887888
schemaToBind = new Schema();
889+
if (schemaAnn != null) {
890+
Optional<String> schemaDescription = schemaAnn.get("description", String.class);
891+
if (schemaDescription.isPresent()) {
892+
schemaToBind.setDescription(schemaDescription.get());
893+
}
894+
}
888895
}
889896
if (originalSchema.get$ref() == null && schemaAnn != null) {
890897
// Apply @Schema annotation only if not $ref since for $ref schemas
@@ -960,7 +967,7 @@ protected Schema bindSchemaForElement(VisitorContext context, Element element, C
960967
}
961968
// @Schema annotation takes priority over nullability annotations
962969
Boolean isSchemaNullable = element.booleanValue(io.swagger.v3.oas.annotations.media.Schema.class, "nullable").orElse(null);
963-
if ((isSchemaNullable == null && element.isNullable()) || (Boolean.TRUE.equals(isSchemaNullable) && element.isNullable())) {
970+
if ((isSchemaNullable == null && element.isNullable()) || Boolean.TRUE.equals(isSchemaNullable)) {
964971
schemaToBind.setNullable(true);
965972
}
966973
final String defaultJacksonValue = element.stringValue(JsonProperty.class, "defaultValue").orElse(null);
@@ -1244,6 +1251,17 @@ private Schema getSchemaDefinition(
12441251
inProgressSchemas.add(schemaName);
12451252
try {
12461253
schema = readSchema(schemaValue, openAPI, context, type, mediaTypes);
1254+
AnnotationValue<io.swagger.v3.oas.annotations.media.Schema> typeSchema = type.getDeclaredAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
1255+
if (typeSchema != null) {
1256+
Schema originalTypeSchema = readSchema(typeSchema, openAPI, context, type, mediaTypes);
1257+
1258+
if (originalTypeSchema.getDescription() != null && !originalTypeSchema.getDescription().isEmpty()) {
1259+
schema.setDescription(originalTypeSchema.getDescription());
1260+
}
1261+
schema.setNullable(originalTypeSchema.getNullable());
1262+
schema.setRequired(originalTypeSchema.getRequired());
1263+
}
1264+
12471265
if (schema != null) {
12481266
schema.setName(schemaName);
12491267
schemas.put(schemaName, schema);

openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiRecursionSpec.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,10 @@ class MyBean {}
175175
expect:
176176
Schema testImpl1 = schemas.get("TestImpl1")
177177
Schema woopsieRef = testImpl1.getProperties()["woopsie"]
178-
woopsieRef
179-
woopsieRef.$ref == "#/components/schemas/woopsie-id"
178+
179+
woopsieRef instanceof ComposedSchema
180+
((ComposedSchema) woopsieRef).allOf[0].$ref == "#/components/schemas/woopsie-id"
181+
((ComposedSchema) woopsieRef).allOf[1].description == "woopsie doopsie"
180182
Schema woopsie = schemas.get("woopsie-id")
181183
woopsie.description == "woopsie doopsie"
182184
}

openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaInheritanceSpec.groovy

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import io.swagger.v3.oas.models.Operation
66
import io.swagger.v3.oas.models.media.ComposedSchema
77
import io.swagger.v3.oas.models.media.Schema
88
import io.swagger.v3.oas.models.parameters.RequestBody
9+
import spock.lang.IgnoreIf
10+
import spock.lang.Issue
911

1012
class OpenApiSchemaInheritanceSpec extends AbstractOpenApiTypeElementSpec {
1113

@@ -264,8 +266,9 @@ class MyBean {}
264266
expect:
265267
Schema owner = schemas["Owner"]
266268
Schema vehicleRef = owner.getProperties()["vehicle"]
267-
!(vehicleRef instanceof ComposedSchema)
268-
vehicleRef.$ref == "#/components/schemas/Vehicle"
269+
vehicleRef instanceof ComposedSchema
270+
((ComposedSchema) vehicleRef).allOf[0].$ref == "#/components/schemas/Vehicle"
271+
((ComposedSchema) vehicleRef).allOf[1].description == "Vehicle of the owner. Here a car or bike with a name"
269272
Schema vehicle = schemas["Vehicle"]
270273
vehicle instanceof ComposedSchema
271274
((ComposedSchema) vehicle).oneOf[0].$ref == '#/components/schemas/Car'
@@ -341,8 +344,9 @@ class MyBean {}
341344
expect:
342345
Schema owner = schemas["Owner"]
343346
Schema vehicleRef = owner.getProperties()["vehicle"]
344-
!(vehicleRef instanceof ComposedSchema)
345-
vehicleRef.$ref == "#/components/schemas/Owner.Vehicle"
347+
vehicleRef instanceof ComposedSchema
348+
((ComposedSchema) vehicleRef).allOf[0].$ref == "#/components/schemas/Owner.Vehicle"
349+
((ComposedSchema) vehicleRef).allOf[1].description == "Vehicle of the owner. Here a car or bike with a name"
346350
Schema ownerVehicle = schemas["Owner.Vehicle"]
347351
((ComposedSchema) ownerVehicle).oneOf[0].$ref == '#/components/schemas/Car'
348352
((ComposedSchema) ownerVehicle).oneOf[1].$ref == '#/components/schemas/Bike'
@@ -432,4 +436,127 @@ class MyBean {}
432436
((ComposedSchema) vehicle).oneOf[1].$ref == '#/components/schemas/Bike'
433437
}
434438

439+
@IgnoreIf({ !jvm.isJava16Compatible() })
440+
@Issue("https://github.com/micronaut-projects/micronaut-openapi/issues/659")
441+
void "test OpenAPI proper inheritance of nullable, description and required attributes"() {
442+
given:
443+
buildBeanDefinition('test.MyBean', '''
444+
445+
package test;
446+
447+
import io.swagger.v3.oas.annotations.*;
448+
import io.swagger.v3.oas.annotations.parameters.*;
449+
import io.swagger.v3.oas.annotations.responses.*;
450+
import io.swagger.v3.oas.annotations.security.*;
451+
import io.swagger.v3.oas.annotations.media.*;
452+
import io.swagger.v3.oas.annotations.enums.*;
453+
import io.swagger.v3.oas.annotations.links.*;
454+
import io.micronaut.http.annotation.*;
455+
import io.micronaut.core.annotation.*;
456+
import io.swagger.v3.oas.annotations.tags.Tag;
457+
import java.util.List;
458+
459+
@Introspected
460+
@Schema(description = "Schema that represents the possible email protocols for sending emails")
461+
enum EmailSendProtocolDto {
462+
SMTP,
463+
SMTP_SSL,
464+
SMTP_STARTTLS
465+
}
466+
467+
@Introspected
468+
@Schema(description = "Schema that represents the current email settings")
469+
record ReadEmailSettingsDto(
470+
471+
@Schema(description = "Flag that indicates whether the email sending is active or not. If set to false, all other values are null", required = true)
472+
Boolean active,
473+
474+
@Schema(description = "Hostname or IP of the email server or null if email sending is disabled", required = true, nullable = true)
475+
String hostname,
476+
477+
@Schema(description = "Port of the email server or null if email sending is disabled", required = true, nullable = true)
478+
Integer port,
479+
480+
@Schema(description = "Protocol used for the connection or null if email sending is disabled", required = true, nullable = true)
481+
EmailSendProtocolDto protocol,
482+
483+
@Schema(description = "Email username to login or null if email sending is disabled", required = true, nullable = true)
484+
String username,
485+
486+
@Schema(description = "Plaintext password for the email user to login in or null if email sending is disabled", required = true, nullable = true)
487+
String plaintextPassword,
488+
489+
@Schema(description = "Sender email address that is used to send emails or null if email sending is disabled", required = true, nullable = true)
490+
String senderEmail
491+
) {
492+
}
493+
494+
@Introspected
495+
@Schema(description = "Schema that represents an existing email output location")
496+
record ReadEmailOutputLocationDto(
497+
498+
// Snipped a lot of attributes. Class doesn't make any sense for outstanders
499+
500+
@Schema(description = "Protocol used for the connection", required = true)
501+
EmailSendProtocolDto protocol
502+
) {
503+
}
504+
505+
@Controller("/api")
506+
class EmailController {
507+
508+
/**
509+
* Get the email settings.
510+
*
511+
* @return Email settings
512+
* @throws Exception Exception in case of invalid data or an issue with reading the settings
513+
*/
514+
@Get("/email/settings")
515+
public ReadEmailSettingsDto getEmailSettings(){
516+
return null;
517+
}
518+
519+
/**
520+
* Get the email output location.
521+
*
522+
* @return Email output location
523+
* @throws Exception Exception in case of invalid data or an issue with reading the settings
524+
*/
525+
@Get("/email/output")
526+
public ReadEmailOutputLocationDto getEmailOutputLocation() {
527+
return null;
528+
}
529+
}
530+
531+
@jakarta.inject.Singleton
532+
class MyBean {}
533+
''')
534+
535+
OpenAPI openAPI = AbstractOpenApiVisitor.testReference
536+
Map<String, Schema> schemas = openAPI.getComponents().getSchemas()
537+
538+
expect:
539+
Schema EmailSendProtocolDtoSchema = schemas["EmailSendProtocolDto"]
540+
541+
EmailSendProtocolDtoSchema.description == "Schema that represents the possible email protocols for sending emails"
542+
!EmailSendProtocolDtoSchema.nullable
543+
!EmailSendProtocolDtoSchema.required
544+
545+
schemas["ReadEmailOutputLocationDto"].required.containsAll(["protocol"])
546+
Schema emailSendProtocolDtoSchemaFromReadEmailOutputLocationDto = schemas["ReadEmailOutputLocationDto"].getProperties()["protocol"]
547+
emailSendProtocolDtoSchemaFromReadEmailOutputLocationDto instanceof ComposedSchema
548+
((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailOutputLocationDto).allOf[0].$ref == "#/components/schemas/EmailSendProtocolDto"
549+
((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailOutputLocationDto).allOf[1].description == "Protocol used for the connection"
550+
!((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailOutputLocationDto).allOf[1].nullable
551+
!((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailOutputLocationDto).allOf[1].required
552+
553+
schemas["ReadEmailSettingsDto"].required.containsAll(["protocol", "active", "hostname", "port", "senderEmail", "username", "plaintextPassword"])
554+
Schema emailSendProtocolDtoSchemaFromReadEmailSettingsDto = schemas["ReadEmailSettingsDto"].getProperties()["protocol"]
555+
emailSendProtocolDtoSchemaFromReadEmailSettingsDto instanceof ComposedSchema
556+
((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailSettingsDto).allOf[0].$ref == "#/components/schemas/EmailSendProtocolDto"
557+
((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailSettingsDto).allOf[1].description == "Protocol used for the connection or null if email sending is disabled"
558+
((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailSettingsDto).allOf[1].nullable
559+
!((ComposedSchema) emailSendProtocolDtoSchemaFromReadEmailSettingsDto).allOf[1].required
560+
}
561+
435562
}

0 commit comments

Comments
 (0)