diff --git a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala index e56a571cb8..2152bbf749 100644 --- a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala @@ -4,16 +4,17 @@ import zio.json.ast.Json import zio.json.{EncoderOps, JsonEncoder} import zio.test._ import zio.{Scope, ZIO} - import zio.schema.annotation.{caseName, discriminatorName, noDiscriminator, optionalField, transientField} import zio.schema.codec.JsonCodec import zio.schema.{DeriveSchema, Schema} - import zio.http.Method.{GET, POST} import zio.http._ +import zio.http.codec.PathCodec.string import zio.http.codec.{Doc, HttpCodec, QueryCodec} import zio.http.endpoint._ +import scala.util.chaining.scalaUtilChainingOps + object OpenAPIGenSpec extends ZIOSpecDefault { final case class SimpleInputBody(name: String, age: Int) @@ -114,6 +115,12 @@ object OpenAPIGenSpec extends ZIOSpecDefault { case class NestedThree(name: String) extends SimpleNestedSealedTrait } + case class Payload(content: String) + + object Payload { + implicit val schema: Schema[Payload] = DeriveSchema.gen[Payload] + } + private val simpleEndpoint = Endpoint( (GET / "static" / int("id") / uuid("uuid") ?? Doc.p("user id") / string("name")) ?? Doc.p("get path"), @@ -139,7 +146,7 @@ object OpenAPIGenSpec extends ZIOSpecDefault { .outError[NotFoundError](Status.NotFound) def toJsonAst(str: String): Json = - Json.decoder.decodeJson(str).toOption.get + Json.decoder.decodeJson(str).tap(println).toOption.get def toJsonAst(api: OpenAPI): Json = toJsonAst(api.toJson) @@ -2357,6 +2364,95 @@ object OpenAPIGenSpec extends ZIOSpecDefault { )(buf => ZIO.attemptBlockingIO(buf.close()).orDie)(buf => ZIO.attemptBlockingIO(buf.mkString)) } yield assertTrue(json == toJsonAst(expectedJson)) }, + test("examples for combined input"){ + + val endpoint = + Endpoint(Method.GET / "root" / string("name")) + .in[Payload] + .out[String] + .examplesIn("hi" -> ("name_value", Payload("input"))) + + val openApi = + OpenAPIGen.fromEndpoints( + title = "Combined input examples", + version = "1.0", + endpoint, + ) + val json = toJsonAst(openApi) + val expectedJson = """"{ + | "openapi": "3.1.0", + | "info": { + | "title": "Combined input examples", + | "version": "1.0" + | }, + | "paths": { + | "/root/{name}": { + | "get": { + | "parameters": [ + | { + | "name": "name", + | "in": "path", + | "required": true, + | "deprecated": false, + | "schema": { + | "type": "string" + | }, + | "explode": false, + | "style": "simple" + | } + | ], + | "requestBody": { + | "content": { + | "application/json": { + | "schema": { + | "$ref": "#/components/schemas/Payload" + | }, + | "examples": { + | "hi": { + | "value": { + | "content": "input" + | } + | } + | } + | } + | }, + | "required": true + | }, + | "responses": { + | "200": { + | "description": "", + | "content": { + | "application/json": { + | "schema": { + | "type": "string" + | } + | } + | } + | } + | }, + | "deprecated": false + | } + | } + | }, + | "components": { + | "schemas": { + | "Payload": { + | "type": "object", + | "properties": { + | "content": { + | "type": "string" + | } + | }, + | "additionalProperties": true, + | "required": [ + | "content" + | ] + | } + | } + | } + |}""".stripMargin + assertTrue(json == toJsonAst(expectedJson)) + } ) } diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala index c2460e942d..3313d96276 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala @@ -1,23 +1,21 @@ package zio.http.endpoint.openapi -import java.util.UUID - -import scala.annotation.tailrec -import scala.collection.{immutable, mutable} - import zio.Chunk +import zio.http._ +import zio.http.codec.HttpCodec.Metadata +import zio.http.codec._ +import zio.http.endpoint._ +import zio.http.endpoint.openapi.JsonSchema.SchemaStyle +import zio.http.endpoint.openapi.OpenAPIGen.AtomizedMetaCodecs.reduceExamplesLeft import zio.json.EncoderOps import zio.json.ast.Json - import zio.schema.Schema.Record import zio.schema.codec.JsonCodec import zio.schema.{Schema, TypeId} -import zio.http._ -import zio.http.codec.HttpCodec.Metadata -import zio.http.codec._ -import zio.http.endpoint._ -import zio.http.endpoint.openapi.JsonSchema.SchemaStyle +import java.util.UUID +import scala.annotation.tailrec +import scala.collection.{immutable, mutable} object OpenAPIGen { private val PathWildcard = "pathWildcard" @@ -133,13 +131,9 @@ object OpenAPIGen { def contentExamples: Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = content.flatMap { case mc @ MetaCodec(HttpCodec.Content(schema, _, _, _), _) => - mc.examples.map { case (name, value) => - name -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(toJsonAst(schema, value))) - } + mc.examples(schema) case mc @ MetaCodec(HttpCodec.ContentStream(schema, _, _, _), _) => - mc.examples.map { case (name, value) => - name -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(toJsonAst(schema, value))) - } + mc.examples(schema) case _ => Map.empty[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] }.toMap @@ -194,10 +188,11 @@ object OpenAPIGen { annotations: Chunk[HttpCodec.Metadata[Any]] = Chunk.empty, ): Chunk[MetaCodec[_]] = in match { - case HttpCodec.Combine(left, right, _) => - flattenedAtoms(left, annotations) ++ flattenedAtoms(right, annotations) - case path: HttpCodec.Path[_] => Chunk.fromIterable(path.pathCodec.segments.map(metaCodecFromSegment)) - case atom: HttpCodec.Atom[_, _] => Chunk(MetaCodec(atom, annotations)) + case HttpCodec.Combine(left, right, combiner) => + flattenedAtoms(left, reduceExamplesLeft(annotations, combiner)) ++ + flattenedAtoms(right, reduceExamplesRight(annotations, combiner)) + case path: HttpCodec.Path[_] => Chunk.fromIterable(path.pathCodec.segments.map(metaCodecFromSegment)) + case atom: HttpCodec.Atom[_, _] => Chunk(MetaCodec(atom, annotations)) case map: HttpCodec.TransformOrFail[_, _, _] => flattenedAtoms(map.api, annotations) case HttpCodec.Empty => Chunk.empty case HttpCodec.Halt => Chunk.empty @@ -205,6 +200,32 @@ object OpenAPIGen { case HttpCodec.Annotated(api, annotation) => flattenedAtoms(api, annotations :+ annotation.asInstanceOf[HttpCodec.Metadata[Any]]) } + + def reduceExamplesLeft( + annotations: Chunk[HttpCodec.Metadata[Any]], + combiner: Combiner[_, _], + ): Chunk[HttpCodec.Metadata[Any]] = + annotations.map { + case HttpCodec.Metadata.Examples(examples) => + HttpCodec.Metadata.Examples(examples.map { case (name, value) => + name -> combiner.separate(value.asInstanceOf[combiner.Out])._1 + }) + case other => + other + } + + def reduceExamplesRight( + annotations: Chunk[HttpCodec.Metadata[Any]], + combiner: Combiner[_, _], + ): Chunk[HttpCodec.Metadata[Any]] = + annotations.map { + case HttpCodec.Metadata.Examples(examples) => + HttpCodec.Metadata.Examples(examples.map { case (name, value) => + name -> combiner.separate(value.asInstanceOf[combiner.Out])._2 + }) + case other => + other + } } private def metaCodecFromSegment(segment: SegmentCodec[_]) = {