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 73ceede3a0..405ae062d6 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 @@ -2398,6 +2398,100 @@ object OpenAPIGenSpec extends ZIOSpecDefault { |}""".stripMargin assertTrue(json == toJsonAst(expected)) }, + test("generated example") { + val endpoint = Endpoint(GET / "static").in[NestedProduct] + val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", genExamples = true, endpoint) + val json = toJsonAst(generated) + println(json.toJsonPretty) + val expected = """{ + | "openapi" : "3.1.0", + | "info" : { + | "title" : "Simple Endpoint", + | "version" : "1.0" + | }, + | "paths" : { + | "/static" : { + | "get" : { + | "requestBody" : { + | "content" : { + | "application/json" : { + | "schema" : { + | "$ref" : "#/components/schemas/NestedProduct" + | }, + | "examples" : { + | "generated" : { + | "value" : { + | "imageMetadata" : { + | "name" : "", + | "size" : 0 + | }, + | "withOptionalField" : { + | "name" : "", + | "age" : 0 + | } + | } + | } + | } + | } + | }, + | "required" : true + | } + | } + | } + | }, + | "components" : { + | "schemas" : { + | "ImageMetadata" : { + | "type" : "object", + | "properties" : { + | "name" : { + | "type" : "string" + | }, + | "size" : { + | "type" : "integer", + | "format" : "int32" + | } + | }, + | "required" : [ + | "name", + | "size" + | ] + | }, + | "NestedProduct" : { + | "type" : "object", + | "properties" : { + | "imageMetadata" : { + | "$ref" : "#/components/schemas/ImageMetadata" + | }, + | "withOptionalField" : { + | "$ref" : "#/components/schemas/WithOptionalField" + | } + | }, + | "required" : [ + | "imageMetadata", + | "withOptionalField" + | ] + | }, + | "WithOptionalField" : { + | "type" : "object", + | "properties" : { + | "name" : { + | "type" : "string" + | }, + | "age" : { + | "type" : "integer", + | "format" : "int32" + | } + | }, + | "required" : [ + | "name" + | ] + | } + | } + | } + |}""".stripMargin + assertTrue(json == toJsonAst(expected)) + }, test("enum") { val endpoint = Endpoint(GET / "static").in[SimpleEnum] val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", endpoint) 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 64ba057807..b14415ec26 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 @@ -67,8 +67,8 @@ object OpenAPIGen { case (examples, _) => examples } - def examples(schema: Schema[_]): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = - examples.map { case (k, v) => + def examples(schema: Schema[_], generate: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = + (if (generate && examples.isEmpty) generateExamples(schema) else examples).map { case (k, v) => k -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(toJsonAst(schema, v))) } @@ -132,13 +132,13 @@ object OpenAPIGen { status ++ that.status, ) - def contentExamples: Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = + def contentExamples(genExamples: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = content.flatMap { case mc @ MetaCodec(HttpCodec.Content(codec, _, _), _) if codec.lookup(MediaType.application.json).isDefined => - mc.examples(codec.lookup(MediaType.application.json).get._2.schema) + mc.examples(codec.lookup(MediaType.application.json).get._2.schema, genExamples) case mc @ MetaCodec(HttpCodec.ContentStream(codec, _, _), _) if codec.lookup(MediaType.application.json).isDefined => - mc.examples(codec.lookup(MediaType.application.json).get._2.schema) + mc.examples(codec.lookup(MediaType.application.json).get._2.schema, genExamples) case _ => Map.empty[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] }.toMap @@ -224,6 +224,57 @@ object OpenAPIGen { } } + private def generateExamples(schema: Schema[_], name: String = "generated"): Map[String, Any] = + schema match { + case collection: Schema.Collection[_, _] => + collection match { + case Schema.Sequence(elementSchema, fromChunk, _, _, _) => + elementSchema.defaultValue.map(v => Map(name -> fromChunk(Chunk.single(v)))).getOrElse(Map.empty) + case map @ Schema.Map(_, _, _) => + val example = for { + k <- map.keySchema.defaultValue + v <- map.valueSchema.defaultValue + } yield map.fromChunk(Chunk.single(k -> v)) + example.map(v => Map(name -> v)).getOrElse(Map.empty) + case map @ Schema.NonEmptyMap(_, _, _) => + val example = for { + k <- map.keySchema.defaultValue + v <- map.valueSchema.defaultValue + } yield map.fromChunk(Chunk.single(k -> v)) + example.map(v => Map(name -> v)).getOrElse(Map.empty) + case Schema.NonEmptySequence(elementSchema, fromChunkOption, _, _, _) => + elementSchema.defaultValue.map(v => Map(name -> fromChunkOption(Chunk.single(v)))).getOrElse(Map.empty) + case set @ Schema.Set(elementSchema, _) => + elementSchema.defaultValue + .map(v => set.fromChunk(Chunk.single(v))) + .map(v => Map(name -> v)) + .getOrElse(Map.empty) + + } + case Transform(schema, _, _, _, _) => + generateExamples(schema, name) + case Schema.Optional(schema, _) => + generateExamples(schema, name).map { case (k, v) => k -> Some(v) } + case Schema.Fail(_, _) => + Map.empty + case Schema.Either(left, right, _) => + generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight") + case Schema.Fallback(left, right, _, _) => + generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight") + case Schema.Lazy(schema0) => + generateExamples(schema0()) + case Schema.Dynamic(_) => + Map.empty + case s @ Schema.Primitive(_, _) => + s.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + case enum: Schema.Enum[_] => + enum.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + case record: Record[_] => + record.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + case t: Schema.Tuple2[_, _] => + t.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + } + def method(in: Chunk[MetaCodec[SimpleCodec[Method, _]]]): Method = { if (in.size > 1) throw new Exception("Multiple methods not supported") in.collectFirst { case MetaCodec(SimpleCodec.Specified(method: Method), _) => method } @@ -483,6 +534,12 @@ object OpenAPIGen { endpoints: Endpoint[_, _, _, _, _]*, ): OpenAPI = fromEndpoints(endpoint1 +: endpoints) + def fromEndpoints( + genExamples: Boolean, + endpoint1: Endpoint[_, _, _, _, _], + endpoints: Endpoint[_, _, _, _, _]*, + ): OpenAPI = fromEndpoints(genExamples, endpoint1 +: endpoints) + def fromEndpoints( title: String, version: String, @@ -490,6 +547,14 @@ object OpenAPIGen { endpoints: Endpoint[_, _, _, _, _]*, ): OpenAPI = fromEndpoints(title, version, endpoint1 +: endpoints) + def fromEndpoints( + title: String, + version: String, + genExamples: Boolean, + endpoint1: Endpoint[_, _, _, _, _], + endpoints: Endpoint[_, _, _, _, _]*, + ): OpenAPI = fromEndpoints(title, version, genExamples, endpoint1 +: endpoints) + def fromEndpoints( title: String, version: String, @@ -498,21 +563,50 @@ object OpenAPIGen { endpoints: Endpoint[_, _, _, _, _]*, ): OpenAPI = fromEndpoints(title, version, referenceType, endpoint1 +: endpoints) + def fromEndpoints( + title: String, + version: String, + referenceType: SchemaStyle, + genExamples: Boolean, + endpoint1: Endpoint[_, _, _, _, _], + endpoints: Endpoint[_, _, _, _, _]*, + ): OpenAPI = fromEndpoints(title, version, referenceType, genExamples, endpoint1 +: endpoints) + def fromEndpoints( referenceType: SchemaStyle, endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType)).reduce(_ ++ _) + def fromEndpoints( + referenceType: SchemaStyle, + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = + if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType, genExamples)).reduce(_ ++ _) + def fromEndpoints( endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact)).reduce(_ ++ _) + def fromEndpoints( + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = + if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact, genExamples)).reduce(_ ++ _) + def fromEndpoints( title: String, version: String, endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = fromEndpoints(endpoints).title(title).version(version) + def fromEndpoints( + title: String, + version: String, + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = fromEndpoints(genExamples, endpoints).title(title).version(version) + def fromEndpoints( title: String, version: String, @@ -520,9 +614,27 @@ object OpenAPIGen { endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = fromEndpoints(referenceType, endpoints).title(title).version(version) + def fromEndpoints( + title: String, + version: String, + referenceType: SchemaStyle, + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = fromEndpoints(referenceType, genExamples, endpoints).title(title).version(version) + + def gen( + endpoint: Endpoint[_, _, _, _, _], + ): OpenAPI = gen(endpoint, SchemaStyle.Compact) + + def gen( + endpoint: Endpoint[_, _, _, _, _], + referenceType: SchemaStyle, + ): OpenAPI = gen(endpoint, referenceType, genExamples = false) + def gen( endpoint: Endpoint[_, _, _, _, _], referenceType: SchemaStyle = SchemaStyle.Compact, + genExamples: Boolean, ): OpenAPI = { val inAtoms = AtomizedMetaCodecs.flatten(endpoint.input) val outs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]] = @@ -610,7 +722,7 @@ object OpenAPIGen { val mediaTypeResponses = mediaTypes.map { case (mediaType, (schema, atomized)) => mediaType.fullType -> OpenAPI.MediaType( schema = OpenAPI.ReferenceOr.Or(schema), - examples = atomized.contentExamples, + examples = atomized.contentExamples(genExamples), encoding = Map.empty, ) } @@ -627,7 +739,7 @@ object OpenAPIGen { }) def responses: OpenAPI.Responses = - responsesForAlternatives(outs) + responsesForAlternatives(outs, genExamples) def parameters: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] = queryParams ++ pathParams ++ headerParams @@ -992,13 +1104,14 @@ object OpenAPIGen { private def responsesForAlternatives( codecs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]], + genExamples: Boolean, ): Map[OpenAPI.StatusOrDefault, OpenAPI.ReferenceOr[OpenAPI.Response]] = codecs.map { case (status, mediaTypes) => val combinedAtomizedCodecs = mediaTypes.map { case (_, (_, atomized)) => atomized }.reduce(_ ++ _) val mediaTypeResponses = mediaTypes.map { case (mediaType, (schema, atomized)) => mediaType.fullType -> OpenAPI.MediaType( schema = OpenAPI.ReferenceOr.Or(schema), - examples = atomized.contentExamples, + examples = atomized.contentExamples(genExamples), encoding = Map.empty, ) }