Skip to content

Commit

Permalink
transform functions for PathCodec (zio#2376)
Browse files Browse the repository at this point in the history
  • Loading branch information
vigoo authored Aug 12, 2023
1 parent 58e5284 commit 27289ca
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 29 deletions.
6 changes: 3 additions & 3 deletions zio-http/src/main/scala/zio/http/RoutePattern.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,20 @@ final case class RoutePattern[A](method: Method, pathCodec: PathCodec[A]) { self
* Encodes a value of type `A` into the method and path that this route
* pattern would successfully match against.
*/
final def encode(value: A): (Method, Path) = (method, format(value))
def encode(value: A): Either[String, (Method, Path)] = format(value).map((method, _))

/**
* Formats a value of type `A` into a path. This is useful for embedding paths
* into HTML that is rendered by the server.
*/
final def format(value: A): Path = pathCodec.format(value)
def format(value: A): Either[String, Path] = pathCodec.format(value)

/**
* Determines if this pattern matches the specified method and path. Rather
* than use this method, you should just try to decode it directly, for higher
* performance, otherwise the same information will be decoded twice.
*/
final def matches(method: Method, path: Path): Boolean = decode(method, path).isRight
def matches(method: Method, path: Path): Boolean = decode(method, path).isRight

/**
* Renders the route pattern as a string.
Expand Down
89 changes: 72 additions & 17 deletions zio-http/src/main/scala/zio/http/codec/PathCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,15 @@ sealed trait PathCodec[A] { self =>

case Unit =>
stack.push(())

case MapOrFail(f) =>
f(stack.pop) match {
case Left(failure) =>
fail = failure
i = instructions.length
case Right(value) =>
stack.push(value)
}
}

i = i + 1
Expand All @@ -212,28 +221,34 @@ sealed trait PathCodec[A] { self =>
* Encodes a value of type `A` into the method and path that this route
* pattern would successfully match against.
*/
final def encode(value: A): Path = format(value)
final def encode(value: A): Either[String, Path] = format(value)

private[http] final def erase: PathCodec[Any] = self.asInstanceOf[PathCodec[Any]]

/**
* Formats a value of type `A` into a path. This is useful for embedding paths
* into HTML that is rendered by the server.
*/
final def format(value: A): Path = {
def loop(path: PathCodec[_], value: Any): Path = path match {
final def format(value: A): Either[String, Path] = {
def loop(path: PathCodec[_], value: Any): Either[String, Path] = path match {
case PathCodec.Concat(left, right, combiner, _) =>
val (leftValue, rightValue) = combiner.separate(value.asInstanceOf[combiner.Out])

loop(left, leftValue) ++ loop(right, rightValue)
for {
leftPath <- loop(left, leftValue)
rightPath <- loop(right, rightValue)
} yield leftPath ++ rightPath

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

val path = loop(self, value)
case PathCodec.TransformOrFail(api, _, g) =>
g.asInstanceOf[Any => Either[String, Any]](value).flatMap(loop(api, _))
}

if (path.nonEmpty) path.addLeadingSlash else path
loop(self, value).map { path =>
if (path.nonEmpty) path.addLeadingSlash else path
}
}

/**
Expand Down Expand Up @@ -263,6 +278,9 @@ sealed trait PathCodec[A] { self =>

case Concat(left, right, combiner, _) =>
loop(left) ++ loop(right) ++ Chunk(Opt.Combine(combiner))

case TransformOrFail(api, f, _) =>
loop(api) :+ Opt.MapOrFail(f.asInstanceOf[Any => Either[String, Any]])
}

if (_optimize eq null) _optimize = loop(self).toArray
Expand All @@ -279,6 +297,9 @@ sealed trait PathCodec[A] { self =>
loop(left) + loop(right)

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

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

loop(self)
Expand All @@ -293,12 +314,36 @@ sealed trait PathCodec[A] { self =>

case PathCodec.Concat(left, right, _, _) =>
loop(left) ++ loop(right)

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

loop(self)
}

override def toString(): String = render

final def transform[A2](f: A => A2, g: A2 => A): PathCodec[A2] =
PathCodec.TransformOrFail[A, A2](self, in => Right(f(in)), output => Right(g(output)))

final def transformOrFail[A2](
f: A => Either[String, A2],
g: A2 => Either[String, A],
): PathCodec[A2] =
PathCodec.TransformOrFail[A, A2](self, f, g)

final def transformOrFailLeft[A2](
f: A => Either[String, A2],
g: A2 => A,
): PathCodec[A2] =
PathCodec.TransformOrFail[A, A2](self, f, output => Right(g(output)))

final def transformOrFailRight[A2](
f: A => A2,
g: A2 => Either[String, A],
): PathCodec[A2] =
PathCodec.TransformOrFail[A, A2](self, in => Right(f(in)), g)
}
object PathCodec {

Expand Down Expand Up @@ -346,6 +391,15 @@ object PathCodec {
def ??(doc: Doc): Concat[A, B, C] = copy(doc = this.doc + doc)
}

private[http] final case class TransformOrFail[X, A](
api: PathCodec[X],
f: X => Either[String, A],
g: A => Either[String, X],
) extends PathCodec[A] {
override def ??(doc: Doc): TransformOrFail[X, A] = copy(api = api ?? doc)
override def doc: Doc = api.doc
}

private[http] val someUnit = Some(())

/**
Expand All @@ -354,15 +408,16 @@ object PathCodec {
*/
private[http] sealed trait Opt
private[http] object Opt {
final case class Match(value: String) extends Opt
final case class Combine(combiner: Combiner[_, _]) extends Opt
case object IntOpt extends Opt
case object LongOpt extends Opt
case object StringOpt extends Opt
case object UUIDOpt extends Opt
case object BoolOpt extends Opt
case object TrailingOpt extends Opt
case object Unit extends Opt
final case class Match(value: String) extends Opt
final case class Combine(combiner: Combiner[_, _]) extends Opt
case object IntOpt extends Opt
case object LongOpt extends Opt
case object StringOpt extends Opt
case object UUIDOpt extends Opt
case object BoolOpt extends Opt
case object TrailingOpt extends Opt
case object Unit extends Opt
final case class MapOrFail(f: Any => Either[String, Any]) extends Opt
}

private[http] final case class SegmentSubtree[+A](
Expand Down
21 changes: 21 additions & 0 deletions zio-http/src/main/scala/zio/http/codec/SegmentCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ sealed trait SegmentCodec[A] { self =>
}
_render
}

final def transform[A2](f: A => A2, g: A2 => A): PathCodec[A2] =
PathCodec.Segment(self).transform(f, g)

final def transformOrFail[A2](
f: A => Either[String, A2],
g: A2 => Either[String, A],
): PathCodec[A2] =
PathCodec.Segment(self).transformOrFail(f, g)

final def transformOrFailLeft[A2](
f: A => Either[String, A2],
g: A2 => A,
): PathCodec[A2] =
PathCodec.Segment(self).transformOrFailLeft(f, g)

final def transformOrFailRight[A2](
f: A => A2,
g: A2 => Either[String, A],
): PathCodec[A2] =
PathCodec.Segment(self).transformOrFailRight(f, g)
}
object SegmentCodec {
def bool(name: String): SegmentCodec[Boolean] = SegmentCodec.BoolSeg(name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,12 @@ private[codec] object EncoderDecoder {
val pathCodec = flattened.path(i).erase
val input = inputs(i)

path = path ++ pathCodec.encode(input)
val encoded = pathCodec.encode(input) match {
case Left(error) =>
throw HttpCodecError.MalformedPath(path, pathCodec, error)
case Right(value) => value
}
path = path ++ encoded

i = i + 1
}
Expand Down
4 changes: 2 additions & 2 deletions zio-http/src/test/scala/zio/http/RoutePatternSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,12 @@ object RoutePatternSpec extends ZIOHttpSpec {
def formatting =
suite("formatting")(
test("/users") {
assertTrue((Method.GET / "users").format(()) == Path("/users"))
assertTrue((Method.GET / "users").format(()) == Right(Path("/users")))
},
test("/users/{user-id}/posts/{post-id}") {
val routePattern = Method.GET / "users" / int("user-id") / "posts" / string("post-id")

assertTrue(routePattern.format((1, "abc")) == Path("/users/1/posts/abc"))
assertTrue(routePattern.format((1, "abc")) == Right(Path("/users/1/posts/abc")))
},
)

Expand Down
64 changes: 58 additions & 6 deletions zio-http/src/test/scala/zio/http/codec/PathCodecSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ package zio.http.codec

import java.util.UUID

import scala.util.Try

import zio._
import zio.test._

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

object PathCodecSpec extends ZIOHttpSpec {
final case class UserId(value: Int)
final case class PostId(value: String)

def spec =
suite("PathCodecSpec")(
suite("parsing")(
Expand All @@ -47,6 +52,20 @@ object PathCodecSpec extends ZIOHttpSpec {

assertTrue(codec.segments.length == 5)
},
test("transformed") {
val codec =
PathCodec.path("/users") /
SegmentCodec.int("user-id").transform(UserId.apply, (uid: UserId) => uid.value) /
SegmentCodec.literal("posts") /
SegmentCodec
.string("post-id")
.transformOrFailLeft(
s => Try(s.toInt).toEither.left.map(_ => "Not a number").map(n => PostId(n.toString)),
(pid: PostId) => pid.value,
)

assertTrue(codec.segments.length == 5)
},
),
suite("decoding")(
test("empty") {
Expand Down Expand Up @@ -82,6 +101,23 @@ object PathCodecSpec extends ZIOHttpSpec {

assertTrue(codec.decode(Path("/users/1/posts/abc")) == Right((1, "abc")))
},
test("transformed") {
val codec =
PathCodec.path("/users") /
SegmentCodec.int("user-id").transform(UserId.apply, (uid: UserId) => uid.value) /
SegmentCodec.literal("posts") /
SegmentCodec
.string("post-id")
.transformOrFailLeft(
s => Try(s.toInt).toEither.left.map(_ => "Not a number").map(n => PostId(n.toString)),
(pid: PostId) => pid.value,
)

assertTrue(
codec.decode(Path("/users/1/posts/456")) == Right((UserId(1), PostId("456"))),
codec.decode(Path("/users/1/posts/abc")) == Left("Not a number"),
)
},
),
suite("representation")(
test("empty") {
Expand All @@ -98,25 +134,41 @@ object PathCodecSpec extends ZIOHttpSpec {
)
},
),
suite("render") {
suite("render")(
test("empty") {
val codec = PathCodec.empty

assertTrue(codec.render == "/")
}
assertTrue(codec.render == "")
},
test("/users") {
val codec = PathCodec.empty / SegmentCodec.literal("users")

assertTrue(codec.render == "/users")
}
},
test("/users/{user-id}/posts/{post-id}") {
val codec =
PathCodec.empty / SegmentCodec.literal("users") / SegmentCodec.int("user-id") / SegmentCodec.literal(
"posts",
) / SegmentCodec.string("post-id")

assertTrue(codec.render == "/users/{user-id}/posts/{post-id}")
}
},
},
test("transformed") {
val codec =
PathCodec.path("/users") /
SegmentCodec.int("user-id").transform(UserId.apply, (uid: UserId) => uid.value) /
SegmentCodec.literal("posts") /
SegmentCodec
.string("post-id")
.transformOrFailLeft(
s => Try(s.toInt).toEither.left.map(_ => "Not a number").map(n => PostId(n.toString)),
(pid: PostId) => pid.value,
)

assertTrue(
codec.render == "/users/{user-id}/posts/{post-id}",
)
},
),
)
}

0 comments on commit 27289ca

Please sign in to comment.