Skip to content

Commit

Permalink
Improve OpenAPI model; Add OpenAPI generator for EndpointAPI (#1498) (#…
Browse files Browse the repository at this point in the history
…2470)

* Improve OpenAPI model; Add OpenAPI generator for EndpointAPI (#1498)

* Minimize schema for optional fields

* Integrate main changes

* Fix Scala 3 build

* Fix exhaustive matching

* More OpenAPI generation tests

* Use latest zio-schema snapshot for Scala 3 macro derivation fix

* Formatting

* OpenAPI tests now compare json ASTs, to avoid string render differences

* Refactoring

* improve docs (#2482)

* Add a test of a middleware providing a context to a `Routes` (#2487)

* Add a test of a middleware providing a context to a `Routes`

* Add a test of a middleware providing a context to a `Routes`

* scalafmt

* scalafmt

* Remove usage of deprecated method in build.sbt (#2486)

* Update sbt-github-actions to 0.18.0 (#2484)

* Update sbt-github-actions to 0.18.0

* Regenerate GitHub Actions workflow

Executed command: sbt githubWorkflowGenerate

* Update netty-codec-http, ... to 4.1.100.Final (#2485)

* Generate readme

* OpenAPI gen support for all kinds of enums with(out) discriminators

OpenAPI gen support for default values, optional and transient fields

---------

Co-authored-by: TomTriple <mail@tomhoefer.de>
Co-authored-by: Jules Ivanic <jules.ivanic@gmail.com>
Co-authored-by: Scala Steward <43047562+scala-steward@users.noreply.github.com>
  • Loading branch information
4 people committed Nov 29, 2023
1 parent b43dbc4 commit 58e0594
Show file tree
Hide file tree
Showing 16 changed files with 4,817 additions and 1,107 deletions.
29 changes: 15 additions & 14 deletions zio-http-cli/src/main/scala/zio/http/endpoint/cli/HttpOptions.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package zio.http.endpoint.cli

import java.nio.file.Path

import scala.annotation.tailrec
import scala.language.implicitConversions
import scala.util.Try

Expand Down Expand Up @@ -242,8 +241,8 @@ private[cli] object HttpOptions {
self =>

override val name = pathCodec.segments.map {
case SegmentCodec.Literal(value, _) => value
case _ => ""
case SegmentCodec.Literal(value) => value
case _ => ""
}
.filter(_ != "")
.mkString("-")
Expand Down Expand Up @@ -301,7 +300,7 @@ private[cli] object HttpOptions {
Try(java.util.UUID.fromString(str)).toEither.left.map { error =>
ValidationError(
ValidationErrorType.InvalidValue,
HelpDoc.p(HelpDoc.Span.code(error.getMessage())),
HelpDoc.p(HelpDoc.Span.code(error.getMessage)),
)
},
)
Expand All @@ -313,27 +312,29 @@ private[cli] object HttpOptions {
}

private[cli] def optionsFromSegment(segment: SegmentCodec[_]): Options[String] = {
@tailrec
def fromSegment[A](segment: SegmentCodec[A]): Options[String] =
segment match {
case SegmentCodec.UUID(name, doc) =>
case SegmentCodec.UUID(name) =>
Options
.text(name)
.mapOrFail(str =>
Try(java.util.UUID.fromString(str)).toEither.left.map { error =>
ValidationError(
ValidationErrorType.InvalidValue,
HelpDoc.p(HelpDoc.Span.code(error.getMessage())),
HelpDoc.p(HelpDoc.Span.code(error.getMessage)),
)
},
)
.map(_.toString)
case SegmentCodec.Text(name, doc) => Options.text(name)
case SegmentCodec.IntSeg(name, doc) => Options.integer(name).map(_.toInt).map(_.toString)
case SegmentCodec.LongSeg(name, doc) => Options.integer(name).map(_.toInt).map(_.toString)
case SegmentCodec.BoolSeg(name, doc) => Options.boolean(name).map(_.toString)
case SegmentCodec.Literal(value, doc) => Options.Empty.map(_ => value)
case SegmentCodec.Trailing(doc) => Options.none.map(_.toString)
case SegmentCodec.Empty(_) => Options.none.map(_.toString)
case SegmentCodec.Text(name) => Options.text(name)
case SegmentCodec.IntSeg(name) => Options.integer(name).map(_.toInt).map(_.toString)
case SegmentCodec.LongSeg(name) => Options.integer(name).map(_.toInt).map(_.toString)
case SegmentCodec.BoolSeg(name) => Options.boolean(name).map(_.toString)
case SegmentCodec.Literal(value) => Options.Empty.map(_ => value)
case SegmentCodec.Trailing => Options.none.map(_.toString)
case SegmentCodec.Empty => Options.none.map(_.toString)
case SegmentCodec.Annotated(codec, _) => fromSegment(codec)
}

fromSegment(segment)
Expand Down
23 changes: 13 additions & 10 deletions zio-http-cli/src/test/scala/zio/http/endpoint/cli/CommandGen.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zio.http.endpoint.cli

import zio.ZNothing
import scala.annotation.tailrec

import zio.cli._
import zio.test._

Expand All @@ -9,7 +10,6 @@ import zio.schema._
import zio.http._
import zio.http.codec._
import zio.http.endpoint._
import zio.http.endpoint.cli.AuxGen._
import zio.http.endpoint.cli.CliRepr.HelpRepr
import zio.http.endpoint.cli.EndpointGen._

Expand All @@ -20,17 +20,20 @@ import zio.http.endpoint.cli.EndpointGen._
object CommandGen {

def getSegment(segment: SegmentCodec[_]): (String, String) = {
@tailrec
def fromSegment[A](segment: SegmentCodec[A]): (String, String) =
segment match {
case SegmentCodec.UUID(name, doc) => (name, "text")
case SegmentCodec.Text(name, doc) => (name, "text")
case SegmentCodec.IntSeg(name, doc) => (name, "integer")
case SegmentCodec.LongSeg(name, doc) => (name, "integer")
case SegmentCodec.BoolSeg(name, doc) => (name, "boolean")
case SegmentCodec.Literal(value, doc) => ("", "")
case SegmentCodec.Trailing(doc) => ("", "")
case SegmentCodec.Empty(_) => ("", "")
case SegmentCodec.UUID(name) => (name, "text")
case SegmentCodec.Text(name) => (name, "text")
case SegmentCodec.IntSeg(name) => (name, "integer")
case SegmentCodec.LongSeg(name) => (name, "integer")
case SegmentCodec.BoolSeg(name) => (name, "boolean")
case SegmentCodec.Literal(_) => ("", "")
case SegmentCodec.Trailing => ("", "")
case SegmentCodec.Empty => ("", "")
case SegmentCodec.Annotated(codec, _) => fromSegment(codec)
}

fromSegment(segment)
}

Expand Down
2 changes: 1 addition & 1 deletion zio-http/src/main/scala/zio/http/Middleware.scala
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ object Middleware extends HandlerAspects {
if (isFishy) {
Handler.fromZIO(ZIO.logWarning(s"fishy request detected: ${request.path.encode}")) *> Handler.badRequest
} else {
val segs = pattern.pathCodec.segments.collect { case SegmentCodec.Literal(v, _) =>
val segs = pattern.pathCodec.segments.collect { case SegmentCodec.Literal(v) =>
v
}
val unnest = segs.foldLeft(Path.empty)(_ / _).addLeadingSlash
Expand Down
5 changes: 5 additions & 0 deletions zio-http/src/main/scala/zio/http/Status.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package zio.http

import scala.util.Try

import zio.Trace
import zio.stacktracer.TracingImplicits.disableAutoTrace

Expand Down Expand Up @@ -170,6 +172,9 @@ object Status {

final case class Custom(override val code: Int) extends Status

def fromString(code: String): Option[Status] =
Try(code.toInt).toOption.flatMap(fromInt)

def fromInt(code: Int): Option[Status] = {

if (code < 100 || code > 599) {
Expand Down
18 changes: 18 additions & 0 deletions zio-http/src/main/scala/zio/http/codec/Doc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

package zio.http.codec

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

import zio.schema.Schema

import zio.http.codec.Doc.Span.CodeStyle
import zio.http.template

Expand All @@ -42,6 +47,13 @@ sealed trait Doc { self =>
case _ => false
}

private[zio] def flattened: Chunk[Doc] =
self match {
case Doc.Empty => Chunk.empty
case Doc.Sequence(left, right) => left.flattened ++ right.flattened
case x => Chunk(x)
}

def toCommonMark: String = {
val writer = new StringBuilder

Expand Down Expand Up @@ -315,6 +327,12 @@ sealed trait Doc { self =>
}
object Doc {

implicit val schemaDocSchema: Schema[Doc] =
Schema[String].transform(
fromCommonMark,
_.toCommonMark,
)

def fromCommonMark(commonMark: String): Doc =
Doc.Raw(commonMark, RawDocType.CommonMark)

Expand Down
15 changes: 15 additions & 0 deletions zio-http/src/main/scala/zio/http/codec/HttpCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package zio.http.codec

import java.util.concurrent.ConcurrentHashMap

import scala.annotation.tailrec
import scala.language.implicitConversions
import scala.reflect.ClassTag

Expand Down Expand Up @@ -192,6 +193,18 @@ sealed trait HttpCodec[-AtomTypes, Value] {
): Task[Value] =
encoderDecoder(Chunk.empty).decode(url, status, method, headers, body)

def doc: Option[Doc] = {
@tailrec
def loop(codec: HttpCodec[_, _]): Option[Doc] =
codec match {
case Annotated(_, Metadata.Documented(doc)) => Some(doc)
case Annotated(codec, _) => loop(codec)
case _ => None
}

loop(self)
}

/**
* Uses this codec to encode the Scala value into a request.
*/
Expand Down Expand Up @@ -630,6 +643,8 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
final case class Examples[A](examples: Map[String, A]) extends Metadata[A]

final case class Documented[A](doc: Doc) extends Metadata[A]

final case class Deprecated[A](doc: Doc) extends Metadata[A]
}

private[http] final case class TransformOrFail[AtomType, X, A](
Expand Down
61 changes: 38 additions & 23 deletions zio-http/src/main/scala/zio/http/codec/PathCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@

package zio.http.codec

import scala.annotation.tailrec
import scala.collection.immutable.ListMap
import scala.language.implicitConversions

import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.{Chunk, NonEmptyChunk}
import zio._

import zio.http.Path

Expand Down Expand Up @@ -239,7 +237,7 @@ sealed trait PathCodec[A] { self =>
rightPath <- loop(right, rightValue)
} yield leftPath ++ rightPath

case PathCodec.Segment(segment, _) =>
case PathCodec.Segment(segment) =>
Right(segment.format(value.asInstanceOf[segment.Type]))

case PathCodec.TransformOrFail(api, _, g) =>
Expand All @@ -264,16 +262,17 @@ sealed trait PathCodec[A] { self =>
private[http] def optimize: Array[Opt] = {
def loop(pattern: PathCodec[_]): Chunk[Opt] =
pattern match {
case PathCodec.Segment(segment, _) =>
case PathCodec.Segment(segment) =>
Chunk(segment.asInstanceOf[SegmentCodec[_]] match {
case SegmentCodec.Empty(_) => Opt.Unit
case SegmentCodec.Literal(value, _) => Opt.Match(value)
case SegmentCodec.IntSeg(_, _) => Opt.IntOpt
case SegmentCodec.LongSeg(_, _) => Opt.LongOpt
case SegmentCodec.Text(_, _) => Opt.StringOpt
case SegmentCodec.UUID(_, _) => Opt.UUIDOpt
case SegmentCodec.BoolSeg(_, _) => Opt.BoolOpt
case SegmentCodec.Trailing(_) => Opt.TrailingOpt
case SegmentCodec.Empty => Opt.Unit
case SegmentCodec.Literal(value) => Opt.Match(value)
case SegmentCodec.IntSeg(_) => Opt.IntOpt
case SegmentCodec.LongSeg(_) => Opt.LongOpt
case SegmentCodec.Text(_) => Opt.StringOpt
case SegmentCodec.UUID(_) => Opt.UUIDOpt
case SegmentCodec.BoolSeg(_) => Opt.BoolOpt
case SegmentCodec.Trailing => Opt.TrailingOpt
case SegmentCodec.Annotated(codec, _) => loop(PathCodec.Segment(codec)).head
})

case Concat(left, right, combiner, _) =>
Expand All @@ -296,7 +295,7 @@ sealed trait PathCodec[A] { self =>
case PathCodec.Concat(left, right, _, _) =>
loop(left) + loop(right)

case PathCodec.Segment(segment, _) => segment.render
case PathCodec.Segment(segment) => segment.render

case PathCodec.TransformOrFail(api, _, _) =>
loop(api)
Expand All @@ -305,12 +304,27 @@ sealed trait PathCodec[A] { self =>
loop(self)
}

private[zio] def renderIgnoreTrailing: String = {
def loop(path: PathCodec[_]): String = path match {
case PathCodec.Concat(left, right, _, _) =>
loop(left) + loop(right)

case PathCodec.Segment(SegmentCodec.Trailing) => ""

case PathCodec.Segment(segment) => segment.render

case PathCodec.TransformOrFail(api, _, _) => loop(api)
}

loop(self)
}

/**
* Returns the segments of the path codec.
*/
def segments: Chunk[SegmentCodec[_]] = {
def loop(path: PathCodec[_]): Chunk[SegmentCodec[_]] = path match {
case PathCodec.Segment(segment, _) => Chunk(segment)
case PathCodec.Segment(segment) => Chunk(segment)

case PathCodec.Concat(left, right, _, _) =>
loop(left) ++ loop(right)
Expand Down Expand Up @@ -354,7 +368,7 @@ object PathCodec {
/**
* The empty / root path codec.
*/
def empty: PathCodec[Unit] = Segment[Unit](SegmentCodec.Empty())
def empty: PathCodec[Unit] = Segment[Unit](SegmentCodec.Empty)

def int(name: String): PathCodec[Int] = Segment(SegmentCodec.int(name))

Expand All @@ -366,12 +380,13 @@ object PathCodec {

def string(name: String): PathCodec[String] = Segment(SegmentCodec.string(name))

def trailing: PathCodec[Path] = Segment(SegmentCodec.Trailing())
def trailing: PathCodec[Path] = Segment(SegmentCodec.Trailing)

def uuid(name: String): PathCodec[java.util.UUID] = Segment(SegmentCodec.uuid(name))

private[http] final case class Segment[A](segment: SegmentCodec[A], doc: Doc = Doc.empty) extends PathCodec[A] {
def ??(doc: Doc): Segment[A] = copy(doc = this.doc + doc)
private[http] final case class Segment[A](segment: SegmentCodec[A]) extends PathCodec[A] {
def ??(doc: Doc): Segment[A] = copy(segment ?? doc)
def doc: Doc = segment.doc
}
private[http] final case class Concat[A, B, C](
left: PathCodec[A],
Expand Down Expand Up @@ -502,14 +517,14 @@ object PathCodec {
.foldRight[SegmentSubtree[A]](SegmentSubtree(ListMap(), ListMap(), Chunk(value))) { case (segment, subtree) =>
val literals =
segment match {
case SegmentCodec.Literal(value, _) => ListMap(value -> subtree)
case _ => ListMap.empty[String, SegmentSubtree[A]]
case SegmentCodec.Literal(value) => ListMap(value -> subtree)
case _ => ListMap.empty[String, SegmentSubtree[A]]
}

val others =
ListMap[SegmentCodec[_], SegmentSubtree[A]]((segment match {
case SegmentCodec.Literal(_, _) => Chunk.empty
case _ => Chunk((segment, subtree))
case SegmentCodec.Literal(_) => Chunk.empty
case _ => Chunk((segment, subtree))
}): _*)

SegmentSubtree(literals, others, Chunk.empty)
Expand Down

0 comments on commit 58e0594

Please sign in to comment.