Skip to content

Commit

Permalink
Generalize the concept of metadata in HttpCodec (#2363)
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil authored Aug 7, 2023
1 parent cdafab7 commit 60c31e2
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 59 deletions.
27 changes: 14 additions & 13 deletions zio-http-cli/src/main/scala/zio/http/endpoint/cli/CliEndpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import zio.json.ast._
import zio.schema._

import zio.http._
import zio.http.codec.HttpCodec.Metadata
import zio.http.codec._
import zio.http.endpoint._

Expand Down Expand Up @@ -84,12 +85,12 @@ private[cli] object CliEndpoint {
r <- fromInput(right)
} yield l ++ r

case HttpCodec.Content(schema, _, _, _) => fromSchema(schema)
case HttpCodec.ContentStream(schema, _, _, _) => fromSchema(schema)
case HttpCodec.Empty => Set.empty
case HttpCodec.Fallback(left, right) => fromInput(left) ++ fromInput(right)
case HttpCodec.Halt => Set.empty
case HttpCodec.Query(name, queryCodec, _) =>
case HttpCodec.Content(schema, _, _, _) => fromSchema(schema)
case HttpCodec.ContentStream(schema, _, _, _) => fromSchema(schema)
case HttpCodec.Empty => Set.empty
case HttpCodec.Fallback(left, right) => fromInput(left) ++ fromInput(right)
case HttpCodec.Halt => Set.empty
case HttpCodec.Query(name, queryCodec, _) =>
queryCodec.asInstanceOf[TextCodec[_]] match {
case TextCodec.UUIDCodec =>
Set(
Expand Down Expand Up @@ -160,7 +161,7 @@ private[cli] object CliEndpoint {
),
)
}
case HttpCodec.Header(name, textCodec, _) =>
case HttpCodec.Header(name, textCodec, _) =>
textCodec.asInstanceOf[TextCodec[_]] match {
case TextCodec.UUIDCodec =>
Set(
Expand Down Expand Up @@ -226,7 +227,7 @@ private[cli] object CliEndpoint {
),
)
}
case HttpCodec.Method(codec, _) =>
case HttpCodec.Method(codec, _) =>
codec.asInstanceOf[SimpleCodec[_, _]] match {
case SimpleCodec.Specified(method) =>
Set(
Expand All @@ -240,7 +241,7 @@ private[cli] object CliEndpoint {
case SimpleCodec.Unspecified() =>
Set.empty
}
case HttpCodec.Path(pathCodec, _) =>
case HttpCodec.Path(pathCodec, _) =>
pathCodec.segments.toSet.map { (segment: SegmentCodec[_]) =>
segment match {
case _: SegmentCodec.Empty =>
Expand Down Expand Up @@ -311,10 +312,10 @@ private[cli] object CliEndpoint {
???
}
}
case HttpCodec.Status(_, _) => Set.empty
case HttpCodec.TransformOrFail(api, _, _) => fromInput(api)
case HttpCodec.WithDoc(in, doc) => fromInput(in).map(_ describeOptions doc.toPlaintext())
case HttpCodec.WithExamples(in, _) => fromInput(in)
case HttpCodec.Status(_, _) => Set.empty
case HttpCodec.TransformOrFail(api, _, _) => fromInput(api)
case HttpCodec.Annotated(in, Metadata.Documented(doc)) => fromInput(in).map(_ describeOptions doc.toPlaintext())
case HttpCodec.Annotated(in, _) => fromInput(in)
}

private def fromSchema(schema: zio.schema.Schema[_]): Set[CliEndpoint[_]] = {
Expand Down
72 changes: 56 additions & 16 deletions zio-http/src/main/scala/zio/http/codec/HttpCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import zio.stream.ZStream
import zio.schema.Schema

import zio.http._
import zio.http.codec.HttpCodec.{Annotated, Metadata}

/**
* A [[zio.http.codec.HttpCodec]] represents a codec for a part of an HTTP
Expand All @@ -42,13 +43,15 @@ import zio.http._
*/
sealed trait HttpCodec[-AtomTypes, Value] {
self =>

private lazy val encoderDecoder = zio.http.codec.internal.EncoderDecoder(self)

/**
* Returns a new codec that is the same as this one, but has attached docs,
* which will render whenever docs are generated from the codec.
*/
final def ??(doc: Doc): HttpCodec[AtomTypes, Value] = HttpCodec.WithDoc(self, doc)
final def ??(doc: Doc): HttpCodec[AtomTypes, Value] =
HttpCodec.Annotated(self, Metadata.Documented(doc))

final def |[AtomTypes1 <: AtomTypes, Value2](
that: HttpCodec[AtomTypes1, Value2],
Expand Down Expand Up @@ -108,6 +111,13 @@ sealed trait HttpCodec[-AtomTypes, Value] {
*/
final def alternatives: Chunk[HttpCodec[AtomTypes, Value]] = HttpCodec.flattenFallbacks(self)

/**
* Returns a new codec that is the same as this one, but has attached
* metadata, such as docs.
*/
def annotate(metadata: Metadata[Value]): HttpCodec[AtomTypes, Value] =
HttpCodec.Annotated(self, metadata)

/**
* Reinterprets this codec as a query codec assuming evidence that this
* interpretation is sound.
Expand Down Expand Up @@ -193,13 +203,13 @@ sealed trait HttpCodec[-AtomTypes, Value] {
): Z =
encoderDecoder.encodeWith(value)(f)

def examples(examples: Iterable[Value]): HttpCodec[AtomTypes, Value] =
HttpCodec.WithExamples(self, Chunk.fromIterable(examples))
def examples(examples: Iterable[(String, Value)]): HttpCodec[AtomTypes, Value] =
HttpCodec.Annotated(self, Metadata.Examples(Chunk.fromIterable(examples).toMap))

def examples(example1: Value, examples: Value*): HttpCodec[AtomTypes, Value] =
HttpCodec.WithExamples(self, example1 +: Chunk.fromIterable(examples))
def examples(example1: (String, Value), examples: (String, Value)*): HttpCodec[AtomTypes, Value] =
HttpCodec.Annotated(self, Metadata.Examples((example1 +: Chunk.fromIterable(examples)).toMap))

def examples: Chunk[Value] = Chunk.empty
def examples: Map[String, Value] = Map.empty

/**
* Returns a new codec that will expect the value to be equal to the specified
Expand All @@ -213,13 +223,26 @@ sealed trait HttpCodec[-AtomTypes, Value] {
_ => expected,
)

def named(name: String): HttpCodec[AtomTypes, Value] =
HttpCodec.Annotated(self, Metadata.Named(name))

/**
* Returns a new codec that is the same as this one, but has attached a name.
* This name is used for documentation generation.
*/
def named(named: Metadata.Named[Value]): HttpCodec[AtomTypes, Value] =
HttpCodec.Annotated(self, Metadata.Named(named.name))

/**
* Returns a new codec, where the value produced by this one is optional.
*/
final def optional: HttpCodec[AtomTypes, Option[Value]] =
self
.orElseEither(HttpCodec.empty)
.transform[Option[Value]](_.swap.toOption, _.fold[Either[Unit, Value]](Left(()))(Right(_)).swap)
Annotated(
self
.orElseEither(HttpCodec.empty)
.transform[Option[Value]](_.swap.toOption, _.fold[Either[Unit, Value]](Left(()))(Right(_)).swap),
Metadata.Optional(),
)

final def orElseEither[AtomTypes1 <: AtomTypes, Value2](
that: HttpCodec[AtomTypes1, Value2],
Expand Down Expand Up @@ -558,11 +581,30 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
def index(index: Int): Header[A] = copy(index = index)
}

private[http] final case class WithDoc[AtomType, A](in: HttpCodec[AtomType, A], doc: Doc)
extends HttpCodec[AtomType, A]
private[http] final case class Annotated[AtomTypes, Value](
codec: HttpCodec[AtomTypes, Value],
metadata: Metadata[Value],
) extends HttpCodec[AtomTypes, Value] {
override def examples: Map[String, Value] =
metadata match {
case value: Metadata.Examples[Value] =>
value.examples ++ codec.examples
case _ =>
codec.examples
}
}

sealed trait Metadata[Value]

object Metadata {
final case class Named[A](name: String) extends Metadata[A]

private[http] final case class WithExamples[AtomType, A](in: HttpCodec[AtomType, A], override val examples: Chunk[A])
extends HttpCodec[AtomType, A]
final case class Optional[A]() extends Metadata[Option[A]]

final case class Examples[A](examples: Map[String, A]) extends Metadata[A]

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

private[http] final case class TransformOrFail[AtomType, X, A](
api: HttpCodec[AtomType, X],
Expand Down Expand Up @@ -614,9 +656,7 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
r <- rewrite[T, combine.Right](right)
} yield HttpCodec.Combine(l, r, combiner)

case HttpCodec.WithDoc(in, doc) => rewrite[T, B](in).map(_ ?? doc)

case HttpCodec.WithExamples(in, examples) => rewrite[T, B](in).map(_.examples(examples))
case HttpCodec.Annotated(in, metadata) => rewrite[T, B](in).map(_.annotate(metadata))

case HttpCodec.Empty => Chunk.single(HttpCodec.Empty)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ private[http] object AtomizedCodecs {
case Combine(left, right, _) => flattenedAtoms(left) ++ flattenedAtoms(right)
case atom: Atom[_, _] => Chunk(atom)
case map: TransformOrFail[_, _, _] => flattenedAtoms(map.api)
case WithDoc(api, _) => flattenedAtoms(api)
case WithExamples(api, _) => flattenedAtoms(api)
case Annotated(api, _) => flattenedAtoms(api)
case Empty => Chunk.empty
case Halt => Chunk.empty
case Fallback(_, _) => throw new UnsupportedOperationException("Cannot handle fallback at this level")
Expand Down
17 changes: 6 additions & 11 deletions zio-http/src/main/scala/zio/http/codec/internal/Mechanic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,10 @@ private[http] object Mechanic {
val (api2, resultIndices) = indexedImpl(api, indices)
(TransformOrFail(api2, f, g).asInstanceOf[HttpCodec[R, A]], resultIndices)

case WithDoc(api, _) => indexedImpl(api.asInstanceOf[HttpCodec[R, A]], indices)
case WithExamples(api, _) => indexedImpl(api.asInstanceOf[HttpCodec[R, A]], indices)
case Empty => (Empty.asInstanceOf[HttpCodec[R, A]], indices)
case Halt => (Halt.asInstanceOf[HttpCodec[R, A]], indices)
case Fallback(_, _) => throw new UnsupportedOperationException("Cannot handle fallback at this level")
case Annotated(api, _) => indexedImpl(api.asInstanceOf[HttpCodec[R, A]], indices)
case Empty => (Empty.asInstanceOf[HttpCodec[R, A]], indices)
case Halt => (Halt.asInstanceOf[HttpCodec[R, A]], indices)
case Fallback(_, _) => throw new UnsupportedOperationException("Cannot handle fallback at this level")
}

def makeConstructor[R, A](
Expand Down Expand Up @@ -95,9 +94,7 @@ private[http] object Mechanic {
case Right(value) => value
}

case WithDoc(api, _) => makeConstructorLoop(api)

case WithExamples(api, _) => makeConstructorLoop(api)
case Annotated(api, _) => makeConstructorLoop(api)

case Empty =>
_ => coerce(())
Expand Down Expand Up @@ -140,9 +137,7 @@ private[http] object Mechanic {
inputsBuilder,
)

case WithDoc(api, _) => makeDeconstructorLoop(api)

case WithExamples(api, _) => makeDeconstructorLoop(api)
case Annotated(api, _) => makeDeconstructorLoop(api)

case Empty => (_, _) => ()

Expand Down
8 changes: 4 additions & 4 deletions zio-http/src/main/scala/zio/http/endpoint/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,15 @@ final case class Endpoint[PathInput, Input, Err, Output, Middleware <: EndpointM
): Invocation[PathInput, Input, Err, Output, Middleware] =
Invocation(self, ev((a, b, c, d, e, f, g, h, i, j, k, l)))

def examplesIn(examples: Input*): Endpoint[PathInput, Input, Err, Output, Middleware] =
def examplesIn(examples: (String, Input)*): Endpoint[PathInput, Input, Err, Output, Middleware] =
copy(input = self.input.examples(examples))

def examplesIn: Chunk[Input] = self.input.examples
def examplesIn: Map[String, Input] = self.input.examples

def examplesOut(examples: Output*): Endpoint[PathInput, Input, Err, Output, Middleware] =
def examplesOut(examples: (String, Output)*): Endpoint[PathInput, Input, Err, Output, Middleware] =
copy(output = self.output.examples(examples))

def examplesOut: Chunk[Output] = self.output.examples
def examplesOut: Map[String, Output] = self.output.examples

/**
* Returns a new endpoint that requires the specified headers to be present.
Expand Down
10 changes: 5 additions & 5 deletions zio-http/src/test/scala/zio/http/codec/HttpCodecSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import zio._
import zio.test._

import zio.http._
import zio.http.codec._

object HttpCodecSpec extends ZIOSpecDefault {
val googleUrl = URL.decode("http://google.com").toOption.get
Expand Down Expand Up @@ -144,18 +143,19 @@ object HttpCodecSpec extends ZIOSpecDefault {
) +
suite("Codec with examples") {
test("with examples") {
val userCodec = HttpCodec.empty.const("foo").examples("John", "Jane")
val userCodec = HttpCodec.empty.const("foo").examples("user" -> "John", "user2" -> "Jane")
val uuid1 = UUID.randomUUID
val uuid2 = UUID.randomUUID
val uuidCodec = HttpCodec.empty.const(UUID.randomUUID()).examples(uuid1, uuid2)
val uuidCodec = HttpCodec.empty.const(UUID.randomUUID()).examples("userId" -> uuid1, "userId2" -> uuid2)

val userExamples = userCodec.examples
val uuidExamples = uuidCodec.examples
assertTrue(
userExamples == Chunk("John", "Jane"),
uuidExamples == Chunk(uuid1, uuid2),
userExamples == Map("user" -> "John", "user2" -> "Jane"),
uuidExamples == Map("userId" -> uuid1, "userId2" -> uuid2),
)
}
},
)

}
16 changes: 8 additions & 8 deletions zio-http/src/test/scala/zio/http/endpoint/EndpointSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -845,22 +845,22 @@ object EndpointSpec extends ZIOSpecDefault {
test("add examples to endpoint") {
val endpoint = Endpoint(GET / "repos" / string("org"))
.out[String]
.examplesIn("zio")
.examplesOut("all, zio, repos")
.examplesIn("repo" -> "zio")
.examplesOut("foundRepos" -> "all, zio, repos")
val endpoint2 =
Endpoint(GET / "repos" / string("org") / string("repo"))
.out[String]
.examplesIn(("zio", "http"), ("zio", "zio"))
.examplesOut("zio, http")
.examplesIn("repo and org" -> ("zio", "http"), "other repo and org" -> ("zio", "zio"))
.examplesOut("repos" -> "zio, http")
val inExamples1 = endpoint.examplesIn
val outExamples1 = endpoint.examplesOut
val inExamples2 = endpoint2.examplesIn
val outExamples2 = endpoint2.examplesOut
assertTrue(
inExamples1 == Chunk("zio"),
outExamples1 == Chunk("all, zio, repos"),
inExamples2 == Chunk(("zio", "http"), ("zio", "zio")),
outExamples2 == Chunk("zio, http"),
inExamples1 == Map("repo" -> "zio"),
outExamples1 == Map("foundRepos" -> "all, zio, repos"),
inExamples2 == Map("repo and org" -> ("zio", "http"), "other repo and org" -> ("zio", "zio")),
outExamples2 == Map("repos" -> "zio, http"),
)
}
},
Expand Down

0 comments on commit 60c31e2

Please sign in to comment.