From 13ecf378ddebf78acb02981c0fc5f0214da99414 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:19:40 +0200 Subject: [PATCH] Fix multiple issues with docs in generating OpenAPI (#3132) (#3147) --- .../endpoint/openapi/OpenAPIGenSpec.scala | 8 ++- .../src/main/scala/zio/http/codec/Doc.scala | 10 ++-- .../zio/http/endpoint/openapi/OpenAPI.scala | 49 ++++++++++++++++--- .../http/endpoint/openapi/OpenAPIGen.scala | 5 +- 4 files changed, 55 insertions(+), 17 deletions(-) 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 7ca59548c8..73ceede3a0 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 @@ -251,7 +251,11 @@ object OpenAPIGenSpec extends ZIOSpecDefault { assertTrue(json == toJsonAst(expectedJson)) }, 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", @@ -261,7 +265,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 19727820ae..64ba057807 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 @@ -538,8 +538,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)) @@ -581,7 +580,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,