Skip to content

Commit

Permalink
Generate anyOf schema for oneOfVariants with the same status code…
Browse files Browse the repository at this point in the history
… and content-type (#3703)
  • Loading branch information
susliko authored Apr 19, 2024
1 parent 3eb21d8 commit d8c700c
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,16 @@ private[openapi] class EndpointToOperationResponse(

val description = bodies.headOption.flatMap { case (desc, _) => desc }.getOrElse(statusCodeDescriptions.headOption.getOrElse(""))

val content = bodies.flatMap { case (_, content) => content }.toListMap
val content = bodies
.flatMap { case (_, content) => content }
.foldLeft(ListMap.empty[String, Vector[MediaType]]) { case (acc, (ct, mt)) =>
acc.get(ct) match {
case Some(mts) => acc.updated(ct, mts :+ mt)
case None => acc.updated(ct, Vector(mt))
}
}
.mapValues(mergeMediaTypesToAnyOf)
.toListMap

if (bodies.nonEmpty || headers.nonEmpty) {
Some(Response(description, headers.toListMap, content))
Expand All @@ -97,6 +106,17 @@ private[openapi] class EndpointToOperationResponse(
}
}

private def mergeMediaTypesToAnyOf(bodies: Vector[MediaType]): MediaType =
bodies.toSet.toList match {
case List(body) => body
case bodies =>
MediaType(
schema = Some(ASchema(anyOf = bodies.flatMap(_.schema))),
example = bodies.flatMap(_.example).headOption,
examples = bodies.flatMap(_.examples).toListMap
)
}

private def collectBodies(outputs: List[EndpointOutput[_]]): List[(Option[String], ListMap[String, MediaType])] = {
val forcedContentType = extractFixedContentType(outputs)
outputs.flatMap(_.traverseOutputs {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
openapi: 3.1.0
info:
title: Fruits
version: '1.0'
paths:
/:
get:
operationId: getRoot
responses:
'200':
description: not found
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/NotFound'
- $ref: '#/components/schemas/Unauthorized'
'204':
description: unknown
content:
application/json:
schema:
$ref: '#/components/schemas/Unknown'
components:
schemas:
NotFound:
title: NotFound
type: object
required:
- what
properties:
what:
type: string
Unauthorized:
title: Unauthorized
type: object
required:
- realm
properties:
realm:
type: string
Unknown:
title: Unknown
type: object
required:
- code
- msg
properties:
code:
type: integer
format: int32
msg:
type: string
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,24 @@ class VerifyYamlOneOfTest extends AnyFunSuite with Matchers {
actualYamlNoIndent shouldBe expectedYaml
}

test("should support multiple the same status codes") {
test("should support multiple the same status codes and same content types") {
val expectedYaml = load("oneOf/expected_the_same_status_codes_and_content_types.yml")

val e = endpoint.out(
sttp.tapir.oneOf(
oneOfVariant(StatusCode.Ok, jsonBody[NotFound].description("not found")),
oneOfVariant(StatusCode.Ok, jsonBody[Unauthorized]),
oneOfVariant(StatusCode.NoContent, jsonBody[Unknown].description("unknown"))
)
)

val actualYaml = OpenAPIDocsInterpreter().toOpenAPI(e, Info("Fruits", "1.0")).toYaml
val actualYamlNoIndent = noIndentation(actualYaml)

actualYamlNoIndent shouldBe expectedYaml
}

test("should support multiple the same status codes and different content types") {
val expectedYaml = load("oneOf/expected_the_same_status_codes.yml")

implicit val unauthorizedTextPlainCodec: Codec[String, Unauthorized, CodecFormat.TextPlain] =
Expand Down

0 comments on commit d8c700c

Please sign in to comment.