Skip to content

Commit

Permalink
Add methods to Endpoint to apply any kind of in/out codec
Browse files Browse the repository at this point in the history
- add some convenience methods for creating header codecs
- make text codecs implicit for easy usage in header and query codec
  • Loading branch information
987Nabil committed May 4, 2023
1 parent 237861e commit a933181
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 5 deletions.
25 changes: 25 additions & 0 deletions zio-http/src/main/scala/zio/http/codec/HeaderCodecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package zio.http.codec

import scala.util.Try

import zio.http.Header
import zio.http.Header.HeaderType

Expand All @@ -27,6 +29,29 @@ private[codec] trait HeaderCodecs {
headerCodec(headerType.name, TextCodec.string)
.transformOrFailLeft(headerType.parse(_), headerType.render(_))

def name[A](name: String)(implicit codec: TextCodec[A]): HeaderCodec[A] =
headerCodec(name, codec)

def nameTransform[A, B](name: String, parse: B => Either[String, A], render: A => B)(implicit
codec: TextCodec[B],
): HeaderCodec[A] =
headerCodec(name, codec).transformOrFailLeft(parse, render)

def nameTransformOpt[A, B](name: String, parse: B => Option[A], render: A => B)(implicit
codec: TextCodec[B],
): HeaderCodec[A] =
headerCodec(name, codec).transformOrFailLeft(parse(_).toRight(s"Failed to parse header $name"), render)

def nameTransformOrFail[A, B](
name: String,
parse: B => A,
render: A => B,
)(implicit codec: TextCodec[B]): HeaderCodec[A] =
headerCodec(name, codec).transformOrFailLeft(
s => Try(parse(s)).toEither.left.map(e => s"Failed to parse header $name: ${e.getMessage}"),
render,
)

final val accept: HeaderCodec[Header.Accept] = header(Header.Accept)
final val acceptEncoding: HeaderCodec[Header.AcceptEncoding] = header(Header.AcceptEncoding)
final val acceptLanguage: HeaderCodec[Header.AcceptLanguage] = header(Header.AcceptLanguage)
Expand Down
1 change: 1 addition & 0 deletions zio-http/src/main/scala/zio/http/codec/QueryCodecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ private[codec] trait QueryCodecs {

def queryAs[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[A] =
HttpCodec.Query(name, codec)

}
8 changes: 4 additions & 4 deletions zio-http/src/main/scala/zio/http/codec/TextCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ sealed trait TextCodec[A] extends PartialFunction[String, A] { self =>
}

object TextCodec {
val boolean: TextCodec[Boolean] = BooleanCodec
implicit val boolean: TextCodec[Boolean] = BooleanCodec

def constant(string: String): TextCodec[Unit] = Constant(string)

val int: TextCodec[Int] = IntCodec
implicit val int: TextCodec[Int] = IntCodec

val string: TextCodec[String] = StringCodec
implicit val string: TextCodec[String] = StringCodec

val uuid: TextCodec[UUID] = UUIDCodec
implicit val uuid: TextCodec[UUID] = UUIDCodec

final case class Constant(string: String) extends TextCodec[Unit] {

Expand Down
19 changes: 18 additions & 1 deletion zio-http/src/main/scala/zio/http/endpoint/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import zio.stream.ZStream
import zio.schema._

import zio.http.Status
import zio.http.codec.HttpCodecType.ResponseType
import zio.http.codec._
import zio.http.endpoint.Endpoint.OutErrors

Expand Down Expand Up @@ -145,6 +144,15 @@ final case class Endpoint[Input, Err, Output, Middleware <: EndpointMiddleware](
): Endpoint[combiner.Out, Err, Output, Middleware] =
copy(input = input ++ HttpCodec.Content(schema))

/**
* Returns a new endpoint derived from this one, whose request must satisfy
* the specified codec.
*/
def inCodec[Input2](codec: HttpCodec[HttpCodecType.RequestType, Input2])(implicit
combiner: Combiner[Input, Input2],
): Endpoint[combiner.Out, Err, Output, Middleware] =
copy(input = input ++ codec)

/**
* Returns a new endpoint derived from this one whose middleware is composed
* from the existing middleware of this endpoint, and the specified
Expand Down Expand Up @@ -193,6 +201,15 @@ final case class Endpoint[Input, Err, Output, Middleware <: EndpointMiddleware](

def outErrors[Err2]: OutErrors[Input, Err, Output, Middleware, Err2] = OutErrors(self)

/**
* Returns a new endpoint derived from this one, whose response must satisfy
* the specified codec.
*/
def outCodec[Output2](codec: HttpCodec[HttpCodecType.ResponseType, Output2])(implicit
alt: Alternator[Output, Output2],
): Endpoint[Input, Err, alt.Out, Middleware] =
copy(output = self.output | codec)

/**
* Returns a new endpoint derived from this one, whose output type is a stream
* of the specified type for the ok status code.
Expand Down
71 changes: 71 additions & 0 deletions zio-http/src/test/scala/zio/http/endpoint/EndpointSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,77 @@ object EndpointSpec extends ZIOSpecDefault {
"path(users, 123, posts, 555, comments, 777, replies, 999)",
)

},
test("composite in codecs") {
val headerOrQuery = HeaderCodec.name[String]("X-Header") | QueryCodec.query("header")

val endpoint = Endpoint.get(literal("test")).out[String].inCodec(headerOrQuery)

val routes = endpoint.implement(header => ZIO.succeed(header))

val request = Request.get(
URL
.decode("/test?header=query-value")
.toOption
.get,
)

val requestWithHeaderAndQuery = request.addHeader("X-Header", "header-value")

val requestWithHeader = Request
.get(
URL
.decode("/test")
.toOption
.get,
)
.addHeader("X-Header", "header-value")

for {
response <- routes.toApp.runZIO(request)
onlyQuery <- response.body.asString.orDie
response <- routes.toApp.runZIO(requestWithHeader)
onlyHeader <- response.body.asString.orDie
response <- routes.toApp.runZIO(requestWithHeaderAndQuery)
headerAndQuery <- response.body.asString.orDie
} yield assertTrue(
onlyQuery == """"query-value"""",
onlyHeader == """"header-value"""",
headerAndQuery == """"header-value"""",
)
},
test("composite out codecs") {
val headerOrQuery = HeaderCodec.name[String]("X-Header") | StatusCodec.status(Status.Created)

val endpoint = Endpoint.get(literal("test")).query(QueryCodec.queryBool("Created")).outCodec(headerOrQuery)

val routes =
endpoint.implement(created => if (created) ZIO.succeed(Right(())) else ZIO.succeed(Left("not created")))

val requestCreated = Request.get(
URL
.decode("/test?Created=true")
.toOption
.get,
)

val requestNotCreated = Request.get(
URL
.decode("/test?Created=false")
.toOption
.get,
)

for {
notCreated <- routes.toApp.runZIO(requestNotCreated)
header = notCreated.rawHeader("X-Header").get
response <- routes.toApp.runZIO(requestCreated)
} yield assertTrue(
header == "not created",
notCreated.status == Status.Ok,
response.status == Status.Created,
)

},
suite("request bodies")(
test("simple input") {
Expand Down

0 comments on commit a933181

Please sign in to comment.