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 99faac7f3d..6636f6533c 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 @@ -11,7 +11,7 @@ import zio.schema.{DeriveSchema, Schema} import zio.http.Method.{GET, POST} import zio.http._ import zio.http.codec.PathCodec.string -import zio.http.codec.{ContentCodec, Doc, HttpCodec, HttpContentCodec, QueryCodec} +import zio.http.codec.{ContentCodec, Doc, HttpCodec} import zio.http.endpoint._ object OpenAPIGenSpec extends ZIOSpecDefault { @@ -212,7 +212,11 @@ object OpenAPIGenSpec extends ZIOSpecDefault { override def spec: Spec[TestEnvironment with Scope, Any] = suite("OpenAPIGenSpec")( test("simple endpoint to OpenAPI") { - val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", simpleEndpoint.tag("simple", "endpoint")) + val generated = OpenAPIGen.fromEndpoints( + "Simple Endpoint", + "1.0", + simpleEndpoint.tag("simple", "endpoint") ?? Doc.p("some extra doc"), + ) val json = toJsonAst(generated) val expectedJson = """{ | "openapi" : "3.1.0", @@ -222,7 +226,7 @@ object OpenAPIGenSpec extends ZIOSpecDefault { | }, | "paths" : { | "/static/{id}/{uuid}/{name}" : { - | "description" : "- simple\n- endpoint\n", + | "description" : "some extra doc\n\n- simple\n- endpoint\n", | "get" : { | "tags" : [ | "simple", diff --git a/zio-http/shared/src/main/scala/zio/http/codec/Doc.scala b/zio-http/shared/src/main/scala/zio/http/codec/Doc.scala index f6e7f3d221..e10dbc90ae 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/Doc.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/Doc.scala @@ -31,9 +31,10 @@ sealed trait Doc { self => def +(that: Doc): Doc = (self, that) match { - case (self, that) if self.isEmpty => that - case (self, that) if that.isEmpty => self - case _ => Doc.Sequence(self, that) + case (self, that) if self.isEmpty => that + case (self, that) if that.isEmpty => self + case _ if tags.isEmpty && that.tags.isEmpty => Doc.Sequence(self, that) + case _ => Doc.Sequence(self, that).tag(self.tags ++ that.tags) } def isEmpty: Boolean = @@ -148,7 +149,8 @@ sealed trait Doc { self => case Doc.Raw(_, docType) => throw new IllegalArgumentException(s"Unsupported raw doc type: $docType") - case Doc.Tagged(_, _) => + case Doc.Tagged(doc, _) => + render(doc, indent) } } diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala index d9e57f5ef5..2dbf6f437e 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala @@ -97,15 +97,48 @@ final case class OpenAPI( .groupBy(_._1) .map { case (path, pathItems) => val pathItem = pathItems.map(_._2).reduce { (i, j) => + var docI = Doc.empty + var docJ = Doc.empty + var get = i.get + var put = i.put + var post = i.post + var delete = i.delete + var options = i.options + var head = i.head + var patch = i.patch + var trace = i.trace + + if ( + get.isDefined || put.isDefined || post.isDefined || delete.isDefined || options.isDefined || head.isDefined || patch.isDefined || trace.isDefined + ) { + docI = i.description.getOrElse(Doc.empty) + } + if ( + (get.isEmpty && j.get.isDefined) || (put.isEmpty && j.put.isDefined) || (post.isEmpty && j.post.isDefined) || (delete.isEmpty && j.delete.isDefined) || (options.isEmpty && j.options.isDefined) || (head.isEmpty && j.head.isDefined) || (patch.isEmpty && j.patch.isDefined) || (trace.isEmpty && j.trace.isDefined) + ) { + docJ = j.description.getOrElse(Doc.empty) + } + get = get.orElse(j.get) + put = put.orElse(j.put) + post = post.orElse(j.post) + delete = delete.orElse(j.delete) + options = options.orElse(j.options) + head = head.orElse(j.head) + patch = patch.orElse(j.patch) + trace = trace.orElse(j.trace) + i.copy( - get = i.get.orElse(j.get), - put = i.put.orElse(j.put), - post = i.post.orElse(j.post), - delete = i.delete.orElse(j.delete), - options = i.options.orElse(j.options), - head = i.head.orElse(j.head), - patch = i.patch.orElse(j.patch), - trace = i.trace.orElse(j.trace), + get = get, + put = put, + post = post, + delete = delete, + options = options, + head = head, + patch = patch, + trace = trace, + description = Some(docI + docJ).filter(!_.isEmpty), + servers = i.servers ++ j.servers, + parameters = i.parameters ++ j.parameters, ) } (path, pathItem) 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 8506980560..b6d848e78f 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 @@ -521,8 +521,7 @@ object OpenAPIGen { val path = buildPath(endpoint.input) val method0 = method(inAtoms.method) // Endpoint has only one doc. But open api has a summery and a description - val pathItem = OpenAPI.PathItem.empty - .copy(description = Some(endpoint.documentation + endpoint.input.doc.getOrElse(Doc.empty)).filter(!_.isEmpty)) + val pathItem = OpenAPI.PathItem.empty.copy(description = Some(endpoint.documentation).filter(!_.isEmpty)) val pathItemWithOp = method0 match { case Method.OPTIONS => pathItem.addOptions(operation(endpoint)) case Method.GET => pathItem.addGet(operation(endpoint)) @@ -564,7 +563,7 @@ object OpenAPIGen { } def operation(endpoint: Endpoint[_, _, _, _, _]): OpenAPI.Operation = { - val maybeDoc = Some(endpoint.documentation + pathDoc).filter(!_.isEmpty) + val maybeDoc = Some(pathDoc).filter(!_.isEmpty) OpenAPI.Operation( tags = endpoint.tags, summary = None,