Skip to content

Commit

Permalink
Tags for Endpoint and OpenAPI generation (zio#2716)
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil committed Aug 2, 2024
1 parent b9a6443 commit 66ce785
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import scala.meta._
import scala.meta.parsers._
import scala.util.{Failure, Success, Try}

import zio.json.JsonDecoder
import zio.json.{JsonDecoder, JsonEncoder}
import zio.test.Assertion.{hasSameElements, isFailure, isSuccess, throws}
import zio.test.TestAspect.{blocking, flaky}
import zio.test.TestFailure.fail
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ 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)
val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", simpleEndpoint.tag("simple", "endpoint"))
val json = toJsonAst(generated)
val expectedJson = """{
| "openapi" : "3.1.0",
Expand All @@ -181,55 +181,49 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
| },
| "paths" : {
| "/static/{id}/{uuid}/{name}" : {
| "description" : "- simple\n- endpoint\n",
| "get" : {
| "tags" : [
| "simple",
| "endpoint"
| ],
| "description" : "get path\n\n",
| "parameters" : [
|
| {
| {
| "name" : "id",
| "in" : "path",
| "required" : true,
| "schema" :
| {
| "type" :
| "integer",
| "schema" : {
| "type" : "integer",
| "format" : "int32"
| },
| "style" : "simple"
| },
|
| {
| {
| "name" : "uuid",
| "in" : "path",
| "description" : "user id\n\n",
| "required" : true,
| "schema" :
| {
| "type" :
| "string",
| "schema" : {
| "type" : "string",
| "format" : "uuid"
| },
| "style" : "simple"
| },
|
| {
| {
| "name" : "name",
| "in" : "path",
| "required" : true,
| "schema" :
| {
| "type" :
| "string"
| "schema" : {
| "type" : "string"
| },
| "style" : "simple"
| }
| ],
| "requestBody" :
| {
| "requestBody" : {
| "content" : {
| "application/json" : {
| "schema" :
| {
| "schema" : {
| "$ref" : "#/components/schemas/SimpleInputBody",
| "description" : "input body\n\n"
| }
Expand All @@ -238,24 +232,20 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
| "required" : true
| },
| "responses" : {
| "200" :
| {
| "200" : {
| "content" : {
| "application/json" : {
| "schema" :
| {
| "schema" : {
| "$ref" : "#/components/schemas/SimpleOutputBody",
| "description" : "output body\n\n"
| }
| }
| }
| },
| "404" :
| {
| "404" : {
| "content" : {
| "application/json" : {
| "schema" :
| {
| "schema" : {
| "$ref" : "#/components/schemas/NotFoundError",
| "description" : "not found\n\n"
| }
Expand All @@ -268,32 +258,25 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
| },
| "components" : {
| "schemas" : {
| "NotFoundError" :
| {
| "type" :
| "object",
| "NotFoundError" : {
| "type" : "object",
| "properties" : {
| "message" : {
| "type" :
| "string"
| "type" : "string"
| }
| },
| "required" : [
| "message"
| ]
| },
| "SimpleInputBody" :
| {
| "type" :
| "object",
| "SimpleInputBody" : {
| "type" : "object",
| "properties" : {
| "name" : {
| "type" :
| "string"
| "type" : "string"
| },
| "age" : {
| "type" :
| "integer",
| "type" : "integer",
| "format" : "int32"
| }
| },
Expand All @@ -302,18 +285,14 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
| "age"
| ]
| },
| "SimpleOutputBody" :
| {
| "type" :
| "object",
| "SimpleOutputBody" : {
| "type" : "object",
| "properties" : {
| "userName" : {
| "type" :
| "string"
| "type" : "string"
| },
| "score" : {
| "type" :
| "integer",
| "type" : "integer",
| "format" : "int32"
| }
| },
Expand Down
54 changes: 46 additions & 8 deletions zio-http/shared/src/main/scala/zio/http/codec/Doc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package zio.http.codec

import zio.Chunk
import zio.stacktracer.TracingImplicits.disableAutoTrace

import zio.schema.Schema

Expand Down Expand Up @@ -54,6 +53,24 @@ sealed trait Doc { self =>
case x => Chunk(x)
}

def tag(tags: Seq[String]): Doc = self match {
case Tagged(doc, existingTags) => Tagged(doc, existingTags ++ tags)
case _ => Tagged(self, tags.toList)
}

def tag(tag: String): Doc =
self match {
case Tagged(doc, tags) => Tagged(doc, tags :+ tag)
case _ => Tagged(self, List(tag))
}

def tag(tag: String, tags: String*): Doc = self.tag(tag +: tags)

def tags: List[String] = self match {
case Tagged(_, tags) => tags
case _ => Nil
}

def toCommonMark: String = {
val writer = new StringBuilder

Expand Down Expand Up @@ -131,20 +148,27 @@ sealed trait Doc { self =>
case Doc.Raw(_, docType) =>
throw new IllegalArgumentException(s"Unsupported raw doc type: $docType")

case Doc.Tagged(_, _) =>

}
}

render(this)
val tags = self.tags
if (tags.nonEmpty) {
// Add all tags to the end of the document as an unordered list
render(Doc.unorderedListing(tags.map(Doc.p): _*))
}
writer.toString()
}

def toHtml: template.Html = {
import template._

self match {
case Doc.Empty =>
val html: Html = self match {
case Doc.Empty =>
Html.Empty
case Header(value, level) =>
case Header(value, level) =>
level match {
case 1 => h1(value)
case 2 => h2(value)
Expand All @@ -154,9 +178,9 @@ sealed trait Doc { self =>
case 6 => h6(value)
case _ => throw new IllegalArgumentException(s"Invalid header level: $level")
}
case Paragraph(value) =>
case Paragraph(value) =>
p(value.toHtml)
case DescriptionList(definitions) =>
case DescriptionList(definitions) =>
dl(
definitions.flatMap { case (span, helpDoc) =>
Seq(
Expand All @@ -165,9 +189,11 @@ sealed trait Doc { self =>
)
},
)
case Sequence(left, right) =>
case Sequence(left, right) =>
left.toHtml ++ right.toHtml
case Listing(elements, listingType) =>
case Listing(elements, _) if elements.isEmpty =>
Html.Empty
case Listing(elements, listingType) =>
val elementsHtml =
elements.map { doc =>
li(doc.toHtml)
Expand All @@ -181,7 +207,10 @@ sealed trait Doc { self =>
Html.fromString(value)
case Raw(_, docType) =>
throw new IllegalArgumentException(s"Unsupported raw doc type: $docType")
case Tagged(doc, _) =>
doc.toHtml
}
html ++ (if (tags.nonEmpty) Doc.unorderedListing(self.tags.map(Doc.p): _*).toHtml else Html.Empty)
}

def toHtmlSnippet: String =
Expand Down Expand Up @@ -275,6 +304,9 @@ sealed trait Doc { self =>

case Doc.Raw(_, docType) =>
throw new IllegalArgumentException(s"Unsupported raw doc type: $docType")

case Tagged(doc, _) =>
renderHelpDoc(doc)
}

def renderSpan(span: Span): Unit = {
Expand Down Expand Up @@ -321,6 +353,11 @@ sealed trait Doc { self =>

renderHelpDoc(this)

val tags = self.tags
if (tags.nonEmpty) {
writer.append("\n")
renderHelpDoc(Doc.unorderedListing(tags.map(Doc.p): _*))
}
writer.toString() + (if (color) Console.RESET else "")
}

Expand Down Expand Up @@ -350,6 +387,7 @@ object Doc {
final case class DescriptionList(definitions: List[(Span, Doc)]) extends Doc
final case class Sequence(left: Doc, right: Doc) extends Doc
final case class Listing(elements: List[Doc], listingType: ListingType) extends Doc
final case class Tagged(doc: Doc, tgs: List[String]) extends Doc
sealed trait ListingType
object ListingType {
case object Unordered extends ListingType
Expand Down
24 changes: 18 additions & 6 deletions zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
output,
error,
codecError,
doc,
self.doc,
mw,
)

Expand Down Expand Up @@ -381,7 +381,7 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
output,
error,
codecError,
doc,
self.doc,
mw,
)

Expand Down Expand Up @@ -611,7 +611,7 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
output = (contentCodec ++ StatusCodec.status(Status.Ok) ?? doc) | self.output,
error,
codecError,
doc,
self.doc,
mw,
)
}
Expand All @@ -635,7 +635,7 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
output = (contentCodec ++ StatusCodec.status(status) ?? doc) | self.output,
error,
codecError,
doc,
self.doc,
mw,
)
}
Expand Down Expand Up @@ -671,7 +671,7 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
output = (contentCodec ++ StatusCodec.status(status)) | self.output,
error,
codecError,
doc,
self.doc,
mw,
)
}
Expand All @@ -689,7 +689,7 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
output = ((contentCodec ++ StatusCodec.status(status)) ?? doc) | self.output,
error,
codecError,
doc,
self.doc,
mw,
)
}
Expand All @@ -702,6 +702,18 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
): Endpoint[PathInput, combiner.Out, Err, Output, Middleware] =
copy(input = self.input ++ codec)

/**
* Adds tags to the endpoint. The are used for documentation generation. For
* example to group endpoints for OpenAPI.
*/
def tag(tag: String, tags: String*): Endpoint[PathInput, Input, Err, Output, Middleware] =
copy(doc = doc.tag(tag +: tags))

/**
* A list of tags for this endpoint.
*/
def tags: List[String] = doc.tags

/**
* Transforms the input of this endpoint using the specified functions. This
* is useful to build from different http inputs a domain specific input.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ object OpenAPIGen {
def operation(endpoint: Endpoint[_, _, _, _, _]): OpenAPI.Operation = {
val maybeDoc = Some(endpoint.doc + pathDoc).filter(!_.isEmpty)
OpenAPI.Operation(
tags = Nil,
tags = endpoint.tags,
summary = None,
description = maybeDoc,
externalDocs = None,
Expand Down

0 comments on commit 66ce785

Please sign in to comment.