From e2d4ebc9061b12d597d66dbf7d59700d4644c2b8 Mon Sep 17 00:00:00 2001 From: adamw Date: Fri, 17 Nov 2023 11:26:14 +0100 Subject: [PATCH] Properly handle independent customisation of referenced schemas, where one is an option --- .../apispec/schema/TSchemaToASchema.scala | 12 ++--- .../expected_deprecated_array_field.yml | 44 +++++++++++++++++++ .../expected_deprecated_optional_field.yml | 42 ++++++++++++++++++ .../VerifyYamlMultiCustomiseSchemaTest.scala | 23 ++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_array_field.yml create mode 100644 docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_optional_field.yml diff --git a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TSchemaToASchema.scala b/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TSchemaToASchema.scala index 7db11a3bd6..9b51e278e4 100644 --- a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TSchemaToASchema.scala +++ b/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TSchemaToASchema.scala @@ -26,11 +26,13 @@ private[schema] class TSchemaToASchema(toSchemaReference: ToSchemaReference, mar case TSchemaType.SArray(nested @ TSchema(_, Some(name), _, _, _, _, _, _, _, _, _)) => ASchema(SchemaType.Array).copy(items = Some(toSchemaReference.map(nested, name))) case TSchemaType.SArray(el) => ASchema(SchemaType.Array).copy(items = Some(apply(el))) - case TSchemaType.SOption(nested @ TSchema(_, Some(name), _, _, _, _, _, _, _, _, _)) => { - val ref = toSchemaReference.map(nested, name) - if (!markOptionsAsNullable) ref - else ASchema.oneOf(List(ref, ASchema(SchemaType.Null)), None) - } + case opt @ TSchemaType.SOption(nested @ TSchema(_, Some(name), _, _, _, _, _, _, _, _, _)) => + // #3288: in case there are multiple different customisations of the nested schema, we need to propagate the + // metadata to properly customise the reference. These are also propagated in ToKeyedSchemas when computing + // the initial list of schemas. + val propagated = propagateMetadataForOption(schema, opt).element + val ref = toSchemaReference.map(propagated, name) + if (!markOptionsAsNullable) ref else ASchema.oneOf(List(ref, ASchema(SchemaType.Null)), None) case TSchemaType.SOption(el) => apply(el, isOptionElement = true) case TSchemaType.SBinary() => ASchema(SchemaType.String).copy(format = SchemaFormat.Binary) case TSchemaType.SDate() => ASchema(SchemaType.String).copy(format = SchemaFormat.Date) diff --git a/docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_array_field.yml b/docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_array_field.yml new file mode 100644 index 0000000000..7536efe785 --- /dev/null +++ b/docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_array_field.yml @@ -0,0 +1,44 @@ +openapi: 3.1.0 +info: + title: Entities + version: '1.0' +paths: + /: + get: + operationId: getRoot + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/HasCollectionDeprecated' + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: body' + content: + text/plain: + schema: + type: string +components: + schemas: + Data1: + required: + - x + type: object + properties: + x: + type: string + HasCollectionDeprecated: + type: object + properties: + field1: + type: array + items: + $ref: '#/components/schemas/Data1' + field2: + type: array + items: + $ref: '#/components/schemas/Data1' + deprecated: true \ No newline at end of file diff --git a/docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_optional_field.yml b/docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_optional_field.yml new file mode 100644 index 0000000000..550fbf8477 --- /dev/null +++ b/docs/openapi-docs/src/test/resources/multi_customise_schema/expected_deprecated_optional_field.yml @@ -0,0 +1,42 @@ +openapi: 3.1.0 +info: + title: Entities + version: '1.0' +paths: + /: + get: + operationId: getRoot + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/HasOptionalDeprecated' + required: true + responses: + '200': + description: '' + '400': + description: 'Invalid value for: body' + content: + text/plain: + schema: + type: string +components: + schemas: + Data1: + required: + - x + type: object + properties: + x: + type: string + HasOptionalDeprecated: + required: + - field1 + type: object + properties: + field1: + $ref: '#/components/schemas/Data1' + field2: + $ref: '#/components/schemas/Data1' + deprecated: true diff --git a/docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/VerifyYamlMultiCustomiseSchemaTest.scala b/docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/VerifyYamlMultiCustomiseSchemaTest.scala index c85fd4778f..bdc56c44e8 100644 --- a/docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/VerifyYamlMultiCustomiseSchemaTest.scala +++ b/docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/VerifyYamlMultiCustomiseSchemaTest.scala @@ -45,9 +45,32 @@ class VerifyYamlMultiCustomiseSchemaTest extends AnyFunSuite with Matchers { actualYamlNoIndent shouldBe expectedYaml } + + test("deprecated optional reference field, when there's also a non-deprecated one") { + val expectedYaml = load("multi_customise_schema/expected_deprecated_optional_field.yml") + val actualYaml = OpenAPIDocsInterpreter() + .toOpenAPI(endpoint.in(jsonBody[HasOptionalDeprecated]), Info("Entities", "1.0")) + .toYaml + + val actualYamlNoIndent = noIndentation(actualYaml) + actualYamlNoIndent shouldBe expectedYaml + } + + test("deprecated optional array field, when there's also a non-deprecated one") { + val expectedYaml = load("multi_customise_schema/expected_deprecated_array_field.yml") + val actualYaml = OpenAPIDocsInterpreter() + .toOpenAPI(endpoint.in(jsonBody[HasCollectionDeprecated]), Info("Entities", "1.0")) + .toYaml + + val actualYamlNoIndent = noIndentation(actualYaml) + actualYamlNoIndent shouldBe expectedYaml + } } object VerifyYamlMultiCustomiseSchemaTest { case class Data1(x: String) case class Data2(@deprecated @description("aaa") a: Data1, @description("bbb") b: Data1) + + case class HasOptionalDeprecated(field1: Data1, @Schema.annotations.deprecated field2: Option[Data1]) + case class HasCollectionDeprecated(field1: List[Data1], @Schema.annotations.deprecated field2: List[Data1]) }