Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

codegen: limited support for oneOfVariant in responses #3993

Merged
merged 1 commit into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -309,35 +309,55 @@ class EndpointGenerator {
// .errorOut(stringBody)
// .out(jsonBody[List[Book]])

responses
.map { resp =>
val d = s""".description("${JavaEscape.escapeString(resp.description)}")"""
val (outs, errorOuts) = responses.partition { resp =>
resp.content match {
case Nil | _ +: Nil =>
resp.code match {
case okStatus(_) => true
case "default" | errorStatus(_) => false
case x => bail(s"Statuscode mapping is incomplete! Cannot handle $x")
}
case _ => bail("We can handle only one return content!")
}
}
def bodyFmt(resp: OpenapiResponse): String = {
val d = s""".description("${JavaEscape.escapeString(resp.description)}")"""
resp.content match {
case Nil => ""
case content +: Nil =>
s"${contentTypeMapper(content.contentType, content.schema, streamingImplementation)}$d"
}
}
def mappedGroup(group: Seq[OpenapiResponse]) = group match {
case Nil => None
case resp +: Nil =>
resp.content match {
case Nil =>
val d = s""".description("${JavaEscape.escapeString(resp.description)}")"""
resp.code match {
case "200" | "default" => ""
case okStatus(s) => s".out(statusCode(sttp.model.StatusCode($s))$d)"
case errorStatus(s) => s".errorOut(statusCode(sttp.model.StatusCode($s))$d)"
}
case content +: Nil =>
resp.code match {
case "200" =>
s".out(${contentTypeMapper(content.contentType, content.schema, streamingImplementation)}$d)"
case okStatus(s) =>
s".out(${contentTypeMapper(content.contentType, content.schema, streamingImplementation)}$d.and(statusCode(sttp.model.StatusCode($s))))"
case "default" =>
s".errorOut(${contentTypeMapper(content.contentType, content.schema, streamingImplementation)}$d)"
case errorStatus(s) =>
s".errorOut(${contentTypeMapper(content.contentType, content.schema, streamingImplementation)}$d.and(statusCode(sttp.model.StatusCode($s))))"
case x =>
bail(s"Statuscode mapping is incomplete! Cannot handle $x")
case "200" | "default" => None
case okStatus(s) => Some(s"statusCode(sttp.model.StatusCode($s))$d")
case errorStatus(s) => Some(s"statusCode(sttp.model.StatusCode($s))$d")
}
case _ => bail("We can handle only one return content!")
case _ =>
Some(resp.code match {
case "200" | "default" => s"${bodyFmt(resp)}"
case okStatus(s) => s"${bodyFmt(resp)}.and(statusCode(sttp.model.StatusCode($s)))"
case errorStatus(s) => s"${bodyFmt(resp)}.and(statusCode(sttp.model.StatusCode($s)))"
})
}
}
.sorted
.filter(_.nonEmpty)
.mkString("\n")
case many =>
if (many.map(_.code).distinct.size != many.size) bail("Cannot construct schema for multiple responses with same status code")
val oneOfs = many.map { m =>
val code = if (m.code == "default") "400" else m.code
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could've been better I think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean 200?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean, default should encompass other status codes; there's an implicit contract binding we take in our choice of code, but really this is explicitly an 'open' sort of specification.... I don't know how to express it well I'm afraid.

s"oneOfVariant(sttp.model.StatusCode(${code}), ${bodyFmt(m)})"
}
Some(s"oneOf(${oneOfs.mkString(", ")})")
}
val mappedOuts = mappedGroup(outs).map(s => s".out($s)")
val mappedErrorOuts = mappedGroup(errorOuts).map(s => s".errorOut($s)")

Seq(mappedErrorOuts, mappedOuts).flatten.mkString("\n")
}

private def contentTypeMapper(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ object SchemaGenerator {
false
) -> "implicit lazy val anyTapirSchema: sttp.tapir.Schema[io.circe.Json] = sttp.tapir.Schema.any[io.circe.Json]"
)
else throw new NotImplementedError("any not implemented for json libs other than circe")
else
throw new NotImplementedError(
s"any not implemented for json libs other than circe (problematic models: ${schemasWithAny.map(_._1)})"
Copy link
Contributor Author

@hughsimpson hughsimpson Aug 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely unrelated, but this can be triggered by surprising things (e.g. an object containing a property that's an enum without a type) so this helps avoid banging heads on walls or keyboards.

)
val openApiSchemasWithTapirSchemas = doc.components
.map(_.schemas.map {
case (name, _: OpenapiSchemaEnum) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ object TestHelpers {
| type: array
| items:
| $ref: '#/components/schemas/Book'
| '401':
| description: 'unauthorized'
| content:
| text/plain:
| schema:
| type: string
| default:
| description: ''
| content:
Expand Down Expand Up @@ -165,6 +171,7 @@ object TestHelpers {
"",
Seq(OpenapiResponseContent("application/json", OpenapiSchemaArray(OpenapiSchemaRef("#/components/schemas/Book"), false)))
),
OpenapiResponse("401", "unauthorized", Seq(OpenapiResponseContent("text/plain", OpenapiSchemaString(false)))),
OpenapiResponse("default", "", Seq(OpenapiResponseContent("text/plain", OpenapiSchemaString(false))))
),
requestBody = None,
Expand Down
Loading