From 02978cd3fc161a3445df2fa441f88fefe3d2c559 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Fri, 8 Mar 2024 20:18:56 +0100 Subject: [PATCH] Simplify tapir schema -> openapi schema conversion (#3584) --- .../tapir/docs/apispec/schema/Schemas.scala | 29 ---- .../apispec/schema/SchemasForEndpoints.scala | 9 +- .../apispec/schema/TSchemaToASchema.scala | 126 +++++++++--------- .../schema/TapirSchemaToJsonSchema.scala | 4 +- .../EndpointToAsyncAPIWebSocketChannel.scala | 8 +- .../docs/asyncapi/MessagesForEndpoints.scala | 6 +- .../tapir/docs/openapi/CodecToMediaType.scala | 6 +- .../docs/openapi/EndpointToOpenAPIDocs.scala | 5 +- .../docs/openapi/EndpointToOpenAPIPaths.scala | 23 ++-- .../openapi/EndpointToOperationResponse.scala | 6 +- 10 files changed, 100 insertions(+), 122 deletions(-) delete mode 100644 docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/Schemas.scala diff --git a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/Schemas.scala b/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/Schemas.scala deleted file mode 100644 index d424de9492..0000000000 --- a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/Schemas.scala +++ /dev/null @@ -1,29 +0,0 @@ -package sttp.tapir.docs.apispec.schema - -import sttp.apispec.{SchemaType, Schema => ASchema} -import sttp.tapir.{Codec, Schema => TSchema, SchemaType => TSchemaType} - -/** Converts a tapir schema to an OpenAPI/AsyncAPI reference (if the schema is named), or to the appropriate schema. */ -class Schemas( - tschemaToASchema: TSchemaToASchema, - toSchemaReference: ToSchemaReference, - markOptionsAsNullable: Boolean -) { - def apply[T](codec: Codec[T, _, _]): ASchema = apply(codec.schema) - - def apply(schema: TSchema[_]): ASchema = { - schema.name match { - case Some(name) => toSchemaReference.map(schema, name) - case None => - schema.schemaType match { - case TSchemaType.SArray(nested @ TSchema(_, Some(name), isOptional, _, _, _, _, _, _, _, _)) => - val s = ASchema(SchemaType.Array) - .copy(items = Some(toSchemaReference.map(nested, name))) - - if (isOptional && markOptionsAsNullable) s.copy(nullable = Some(true)) else s - case TSchemaType.SOption(ts) => apply(ts) - case _ => tschemaToASchema(schema) - } - } - } -} diff --git a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/SchemasForEndpoints.scala b/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/SchemasForEndpoints.scala index ffd8e72e2f..59a5b595bd 100644 --- a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/SchemasForEndpoints.scala +++ b/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/SchemasForEndpoints.scala @@ -18,7 +18,7 @@ class SchemasForEndpoints( * A tuple: the first element can be used to create the components section in the docs. The second can be used to resolve (possible) * top-level references from parameters / bodies. */ - def apply(): (ListMap[SchemaId, ASchema], Schemas) = { + def apply(): (ListMap[SchemaId, ASchema], TSchemaToASchema) = { val keyedCombinedSchemas: Iterable[KeyedSchema] = ToKeyedSchemas.uniqueCombined( es.flatMap(e => forInput(e.securityInput) ++ forInput(e.input) ++ forOutput(e.errorOutput) ++ forOutput(e.output) @@ -29,12 +29,11 @@ class SchemasForEndpoints( val toSchemaReference = new ToSchemaReference(keysToIds, keyedCombinedSchemas.toMap) val tschemaToASchema = new TSchemaToASchema(toSchemaReference, markOptionsAsNullable) - val keysToSchemas: ListMap[SchemaKey, ASchema] = keyedCombinedSchemas.map(td => (td._1, tschemaToASchema(td._2))).toListMap + val keysToSchemas: ListMap[SchemaKey, ASchema] = + keyedCombinedSchemas.map(td => (td._1, tschemaToASchema(td._2, allowReference = false))).toListMap val schemaIds: Map[SchemaKey, (SchemaId, ASchema)] = keysToSchemas.map { case (k, v) => k -> ((keysToIds(k), v)) } - val schemas = new Schemas(tschemaToASchema, toSchemaReference, markOptionsAsNullable) - - (schemaIds.values.toListMap, schemas) + (schemaIds.values.toListMap, tschemaToASchema) } private def forInput(input: EndpointInput[_]): List[KeyedSchema] = { 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 db9a7f2767..1a7cfc9c33 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 @@ -7,71 +7,76 @@ import sttp.tapir.docs.apispec.DocsExtensionAttribute.RichSchema import sttp.tapir.docs.apispec.schema.TSchemaToASchema.{tDefaultToADefault, tExampleToAExample} import sttp.tapir.docs.apispec.{DocsExtensions, exampleValue} import sttp.tapir.internal._ -import sttp.tapir.{Validator, Schema => TSchema, SchemaType => TSchemaType} +import sttp.tapir.{Codec, Validator, Schema => TSchema, SchemaType => TSchemaType} -/** Converts a tapir schema to an OpenAPI/AsyncAPI schema, using `toSchemaReference` to resolve nested references. */ -private[schema] class TSchemaToASchema(toSchemaReference: ToSchemaReference, markOptionsAsNullable: Boolean) { - def apply[T](schema: TSchema[T], isOptionElement: Boolean = false): ASchema = { +/** Converts a tapir schema to an OpenAPI/AsyncAPI schema, using `toSchemaReference` to resolve references. */ +private[docs] class TSchemaToASchema(toSchemaReference: ToSchemaReference, markOptionsAsNullable: Boolean) { + + def apply[T](codec: Codec[T, _, _]): ASchema = apply(codec.schema, allowReference = true) + + /** @param allowReference + * Can a reference schema be generated, if this is a named schema - should be `false` for top-level component definitions (otherwise + * the definitions are infinitely recursive) + */ + def apply[T](schema: TSchema[T], allowReference: Boolean, isOptionElement: Boolean = false): ASchema = { val nullable = markOptionsAsNullable && isOptionElement - val result = schema.schemaType match { - case TSchemaType.SInteger() => ASchema(SchemaType.Integer) - case TSchemaType.SNumber() => ASchema(SchemaType.Number) - case TSchemaType.SBoolean() => ASchema(SchemaType.Boolean) - case TSchemaType.SString() => ASchema(SchemaType.String) - case p @ TSchemaType.SProduct(fields) => - ASchema(SchemaType.Object).copy( - required = p.required.map(_.encodedName), - properties = extractProperties(fields) - ) - 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 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 ref.copy(nullable = Some(true)) - 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) - case TSchemaType.SDateTime() => ASchema(SchemaType.String).copy(format = SchemaFormat.DateTime) - case TSchemaType.SRef(fullName) => toSchemaReference.mapDirect(fullName) - case TSchemaType.SCoproduct(schemas, d) => - ASchema.oneOf( - schemas - .filterNot(_.hidden) - .map { - case nested @ TSchema(_, Some(name), _, _, _, _, _, _, _, _, _) => toSchemaReference.map(nested, name) - case t => apply(t) - } - .sortBy { - case schema if schema.$ref.isDefined => schema.$ref.get - case schema => schema.`type`.collect { case t: BasicSchemaType => t.value }.getOrElse("") + schema.toString - }, - d.map(tDiscriminatorToADiscriminator) - ) - case p @ TSchemaType.SOpenProduct(fields, valueSchema) => - ASchema(SchemaType.Object).copy( - required = p.required.map(_.encodedName), - properties = extractProperties(fields), - additionalProperties = Some(valueSchema.name match { - case Some(name) => toSchemaReference.map(valueSchema, name) - case _ => apply(valueSchema) - }).filterNot(_ => valueSchema.hidden) - ) - } - val primitiveValidators = schema.validator.asPrimitiveValidators - val schemaIsWholeNumber = schema.schemaType match { - case TSchemaType.SInteger() => true - case _ => false + val result = schema.name match { + case Some(name) if allowReference => toSchemaReference.map(schema, name) + case _ => + schema.schemaType match { + case TSchemaType.SInteger() => ASchema(SchemaType.Integer) + case TSchemaType.SNumber() => ASchema(SchemaType.Number) + case TSchemaType.SBoolean() => ASchema(SchemaType.Boolean) + case TSchemaType.SString() => ASchema(SchemaType.String) + case p @ TSchemaType.SProduct(fields) => + ASchema(SchemaType.Object).copy( + required = p.required.map(_.encodedName), + properties = extractProperties(fields) + ) + case TSchemaType.SArray(el) => ASchema(SchemaType.Array).copy(items = Some(apply(el, allowReference = true))) + 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 ref.copy(nullable = Some(true)) + case TSchemaType.SOption(el) => apply(el, allowReference = true, isOptionElement = true) + case TSchemaType.SBinary() => ASchema(SchemaType.String).copy(format = SchemaFormat.Binary) + case TSchemaType.SDate() => ASchema(SchemaType.String).copy(format = SchemaFormat.Date) + case TSchemaType.SDateTime() => ASchema(SchemaType.String).copy(format = SchemaFormat.DateTime) + case TSchemaType.SRef(fullName) => toSchemaReference.mapDirect(fullName) + case TSchemaType.SCoproduct(schemas, d) => + ASchema.oneOf( + schemas + .filterNot(_.hidden) + .map(apply(_, allowReference = true)) + .sortBy { + case schema if schema.$ref.isDefined => schema.$ref.get + case schema => schema.`type`.collect { case t: BasicSchemaType => t.value }.getOrElse("") + schema.toString + }, + d.map(tDiscriminatorToADiscriminator) + ) + case p @ TSchemaType.SOpenProduct(fields, valueSchema) => + ASchema(SchemaType.Object).copy( + required = p.required.map(_.encodedName), + properties = extractProperties(fields), + additionalProperties = Some(apply(valueSchema, allowReference = true)).filterNot(_ => valueSchema.hidden) + ) + } } if (result.$ref.isEmpty) { // only customising non-reference schemas; references might get enriched with some meta-data if there // are multiple different customisations of the referenced schema in ToSchemaReference (#1203) + + val primitiveValidators = schema.validator.asPrimitiveValidators + val schemaIsWholeNumber = schema.schemaType match { + case TSchemaType.SInteger() => true + case _ => false + } + var s = result s = if (nullable) s.copy(nullable = Some(true)) else s s = addMetadata(s, schema) @@ -84,12 +89,7 @@ private[schema] class TSchemaToASchema(toSchemaReference: ToSchemaReference, mar private def extractProperties[T](fields: List[TSchemaType.SProductField[T]]) = { fields .filterNot(_.schema.hidden) - .map { f => - f.schema.name match { - case Some(name) => f.name.encodedName -> toSchemaReference.map(f.schema, name) - case None => f.name.encodedName -> apply(f.schema) - } - } + .map(f => f.name.encodedName -> apply(f.schema, allowReference = true)) .toListMap } diff --git a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TapirSchemaToJsonSchema.scala b/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TapirSchemaToJsonSchema.scala index 1da52746e8..febcaeb5b0 100644 --- a/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TapirSchemaToJsonSchema.scala +++ b/docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TapirSchemaToJsonSchema.scala @@ -20,11 +20,11 @@ object TapirSchemaToJsonSchema { val keysToIds = calculateUniqueIds(keyedSchemas.map(_._1), (key: SchemaKey) => schemaName(key.name)) val toSchemaReference = new ToSchemaReference(keysToIds, keyedSchemas.toMap, refRoot = "#/$defs/") val tschemaToASchema = new TSchemaToASchema(toSchemaReference, markOptionsAsNullable) - val keysToSchemas = keyedSchemas.map(td => (td._1, tschemaToASchema(td._2))).toListMap + val keysToSchemas = keyedSchemas.map(td => (td._1, tschemaToASchema(td._2, allowReference = false))).toListMap val schemaIds = keysToSchemas.map { case (k, v) => k -> ((keysToIds(k), v)) } val nestedKeyedSchemas = schemaIds.values - val rootApiSpecSchemaOrRef: ASchema = tschemaToASchema(schema) + val rootApiSpecSchemaOrRef: ASchema = tschemaToASchema(schema, allowReference = false) val defsList: ListMap[SchemaId, ASchema] = nestedKeyedSchemas.collect { diff --git a/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/EndpointToAsyncAPIWebSocketChannel.scala b/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/EndpointToAsyncAPIWebSocketChannel.scala index c4bed5ebe8..15f27c3837 100644 --- a/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/EndpointToAsyncAPIWebSocketChannel.scala +++ b/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/EndpointToAsyncAPIWebSocketChannel.scala @@ -6,14 +6,14 @@ import sttp.model.Method import sttp.tapir.EndpointOutput.WebSocketBodyWrapper import sttp.tapir.docs.apispec.DocsExtensionAttribute.{RichEndpointIOInfo, RichEndpointInfo} import sttp.tapir.docs.apispec.{DocsExtensions, namedPathComponents} -import sttp.tapir.docs.apispec.schema.Schemas +import sttp.tapir.docs.apispec.schema.TSchemaToASchema import sttp.tapir.internal.{IterableToListMap, RichEndpoint} import sttp.tapir.{AnyEndpoint, Codec, CodecFormat, EndpointIO, EndpointInput} import scala.collection.immutable.ListMap private[asyncapi] class EndpointToAsyncAPIWebSocketChannel( - schemas: Schemas, + tschemaToASchema: TSchemaToASchema, codecToMessageKey: Map[Codec[_, _, _ <: CodecFormat], MessageKey], options: AsyncAPIDocsOptions ) { @@ -49,7 +49,7 @@ private[asyncapi] class EndpointToAsyncAPIWebSocketChannel( codec: Codec[_, _, _ <: CodecFormat], info: EndpointIO.Info[_] ): ((String, Codec[_, _, _ <: CodecFormat]), ASchema) = { - val schemaRef = schemas(codec) + val schemaRef = tschemaToASchema(codec) schemaRef match { case schema if schema.$ref.isEmpty => val schemaWithDescription = if (schema.description.isEmpty) schemaRef.copy(description = info.description) else schemaRef @@ -63,7 +63,7 @@ private[asyncapi] class EndpointToAsyncAPIWebSocketChannel( private def parameters(inputs: Vector[EndpointInput.Basic[_]]): ListMap[String, ReferenceOr[Parameter]] = { inputs.collect { case EndpointInput.PathCapture(Some(name), codec, info) => - name -> Right(Parameter(info.description, Some(schemas(codec)), None, DocsExtensions.fromIterable(info.docsExtensions))) + name -> Right(Parameter(info.description, Some(tschemaToASchema(codec)), None, DocsExtensions.fromIterable(info.docsExtensions))) }.toListMap } diff --git a/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/MessagesForEndpoints.scala b/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/MessagesForEndpoints.scala index 3ecb1658f2..c4ea96f1eb 100644 --- a/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/MessagesForEndpoints.scala +++ b/docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/MessagesForEndpoints.scala @@ -4,14 +4,14 @@ import sttp.apispec.asyncapi.{Message, SingleMessage} import sttp.model.MediaType import sttp.tapir.EndpointOutput.WebSocketBodyWrapper import sttp.tapir.Schema.SName -import sttp.tapir.docs.apispec.schema.{Schemas, ToKeyedSchemas, calculateUniqueIds} +import sttp.tapir.docs.apispec.schema.{TSchemaToASchema, ToKeyedSchemas, calculateUniqueIds} import sttp.tapir.internal.IterableToListMap import sttp.tapir.{Codec, CodecFormat, EndpointIO, WebSocketBodyOutput, Schema => TSchema} import sttp.ws.WebSocketFrame import scala.collection.immutable.ListMap -private[asyncapi] class MessagesForEndpoints(schemas: Schemas, schemaName: SName => String) { +private[asyncapi] class MessagesForEndpoints(tschemaToASchema: TSchemaToASchema, schemaName: SName => String) { private type CodecData = Either[(SName, MediaType), TSchema[_]] private case class CodecWithInfo[T](codec: Codec[WebSocketFrame, T, _ <: CodecFormat], info: EndpointIO.Info[T]) @@ -42,7 +42,7 @@ private[asyncapi] class MessagesForEndpoints(schemas: Schemas, schemaName: SName val convertedExamples = ExampleConverter.convertExamples(ci.codec, ci.info.examples) SingleMessage( None, - Some(Right(schemas(ci.codec))), + Some(Right(tschemaToASchema(ci.codec))), None, None, Some(ci.codec.format.mediaType.toString()), diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/CodecToMediaType.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/CodecToMediaType.scala index 8db1aa8c97..2ea7817310 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/CodecToMediaType.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/CodecToMediaType.scala @@ -1,12 +1,12 @@ package sttp.tapir.docs.openapi import sttp.apispec.openapi.{MediaType => OMediaType} -import sttp.tapir.docs.apispec.schema.Schemas +import sttp.tapir.docs.apispec.schema.TSchemaToASchema import sttp.tapir.{CodecFormat, _} import scala.collection.immutable.ListMap -private[openapi] class CodecToMediaType(schemas: Schemas) { +private[openapi] class CodecToMediaType(tschemaToASchema: TSchemaToASchema) { def apply[T, CF <: CodecFormat]( o: Codec[_, T, CF], examples: List[EndpointIO.Example[T]], @@ -19,7 +19,7 @@ private[openapi] class CodecToMediaType(schemas: Schemas) { ListMap( forcedContentType.getOrElse(o.format.mediaType.noCharset.toString) -> OMediaType( - Some(schemas(o)), + Some(tschemaToASchema(o)), allExamples.singleExample, allExamples.multipleExamples ) diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala index 0cc5740ebc..0c97cfe8d0 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala @@ -17,9 +17,10 @@ private[openapi] object EndpointToOpenAPIDocs { ): OpenAPI = { val es2 = es.filter(e => findWebSocket(e).isEmpty).map(nameAllPathCapturesInEndpoint) val additionalOutputs = es2.flatMap(e => options.defaultDecodeFailureOutput(e.input)).toSet.toList - val (idToSchema, schemas) = new SchemasForEndpoints(es2, options.schemaName, options.markOptionsAsNullable, additionalOutputs).apply() + val (idToSchema, tschemaToASchema) = + new SchemasForEndpoints(es2, options.schemaName, options.markOptionsAsNullable, additionalOutputs).apply() val securitySchemes = SecuritySchemesForEndpoints(es2, apiKeyAuthTypeName = "apiKey") - val pathCreator = new EndpointToOpenAPIPaths(schemas, securitySchemes, options) + val pathCreator = new EndpointToOpenAPIPaths(tschemaToASchema, securitySchemes, options) val componentsCreator = new EndpointToOpenAPIComponents(idToSchema, securitySchemes) val base = apiToOpenApi(api, componentsCreator, docsExtensions) diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala index 766ad18031..db27ca43d0 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala @@ -5,15 +5,19 @@ import sttp.apispec.{Schema => ASchema, SchemaType => ASchemaType} import sttp.apispec.openapi._ import sttp.tapir._ import sttp.tapir.docs.apispec.DocsExtensionAttribute.{RichEndpointIOInfo, RichEndpointInfo} -import sttp.tapir.docs.apispec.schema.Schemas +import sttp.tapir.docs.apispec.schema.TSchemaToASchema import sttp.tapir.docs.apispec.{DocsExtensions, SecurityRequirementsForEndpoints, SecuritySchemes, namedPathComponents} import sttp.tapir.internal._ import scala.collection.immutable.ListMap -private[openapi] class EndpointToOpenAPIPaths(schemas: Schemas, securitySchemes: SecuritySchemes, options: OpenAPIDocsOptions) { - private val codecToMediaType = new CodecToMediaType(schemas) - private val endpointToOperationResponse = new EndpointToOperationResponse(schemas, codecToMediaType, options) +private[openapi] class EndpointToOpenAPIPaths( + tschemaToASchema: TSchemaToASchema, + securitySchemes: SecuritySchemes, + options: OpenAPIDocsOptions +) { + private val codecToMediaType = new CodecToMediaType(tschemaToASchema) + private val endpointToOperationResponse = new EndpointToOperationResponse(tschemaToASchema, codecToMediaType, options) private val securityRequirementsForEndpoint = new SecurityRequirementsForEndpoints(securitySchemes) def pathItem(e: AnyEndpoint): (String, PathItem) = { @@ -103,16 +107,19 @@ private[openapi] class EndpointToOpenAPIPaths(schemas: Schemas, securitySchemes: } } - private def headerToParameter[T](header: EndpointIO.Header[T]) = EndpointInputToParameterConverter.from(header, schemas(header.codec)) + private def headerToParameter[T](header: EndpointIO.Header[T]) = + EndpointInputToParameterConverter.from(header, tschemaToASchema(header.codec)) private def fixedHeaderToParameter[T](header: EndpointIO.FixedHeader[_]) = EndpointInputToParameterConverter.from(header, ASchema(ASchemaType.String)) - private def cookieToParameter[T](cookie: EndpointInput.Cookie[T]) = EndpointInputToParameterConverter.from(cookie, schemas(cookie.codec)) - private def pathCaptureToParameter[T](p: EndpointInput.PathCapture[T]) = EndpointInputToParameterConverter.from(p, schemas(p.codec)) + private def cookieToParameter[T](cookie: EndpointInput.Cookie[T]) = + EndpointInputToParameterConverter.from(cookie, tschemaToASchema(cookie.codec)) + private def pathCaptureToParameter[T](p: EndpointInput.PathCapture[T]) = + EndpointInputToParameterConverter.from(p, tschemaToASchema(p.codec)) private def queryToParameter[T](query: EndpointInput.Query[T]) = query.codec.format match { // use `schema` for simple plain text scenarios and `content` for complex serializations, e.g. JSON // see https://swagger.io/docs/specification/describing-parameters/#schema-vs-content - case CodecFormat.TextPlain() => EndpointInputToParameterConverter.from(query, schemas(query.codec)) + case CodecFormat.TextPlain() => EndpointInputToParameterConverter.from(query, tschemaToASchema(query.codec)) case _ => EndpointInputToParameterConverter.from(query, codecToMediaType(query.codec, query.info.examples, None, Nil)) } diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOperationResponse.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOperationResponse.scala index c25ddcf371..f4a8db6082 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOperationResponse.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOperationResponse.scala @@ -6,14 +6,14 @@ import sttp.apispec.openapi._ import sttp.tapir._ import sttp.tapir.docs.apispec.DocsExtensionAttribute.RichEndpointIOInfo import sttp.tapir.docs.apispec.{DocsExtensions, exampleValue} -import sttp.tapir.docs.apispec.schema.Schemas +import sttp.tapir.docs.apispec.schema.TSchemaToASchema import sttp.tapir.internal._ import sttp.tapir.model.StatusCodeRange import scala.collection.immutable.ListMap private[openapi] class EndpointToOperationResponse( - schemas: Schemas, + tschemaToASchema: TSchemaToASchema, codecToMediaType: CodecToMediaType, options: OpenAPIDocsOptions ) { @@ -114,7 +114,7 @@ private[openapi] class EndpointToOperationResponse( Header( description = info.description, required = Some(!codec.schema.isOptional), - schema = Some(schemas(codec)), + schema = Some(tschemaToASchema(codec)), example = info.example.flatMap(exampleValue(codec, _)) ) )