From 2f565f0ef25409581d0c4eeeb0d86f5e331b725c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Heredia=20Montiel?= Date: Wed, 26 Jul 2023 16:16:13 -0600 Subject: [PATCH] [dsl] Expose PathVar class to facilitate the definition of custom Path extractors --- docs/docs/dsl.md | 14 +++-------- .../main/scala/org/http4s/dsl/Http4sDsl.scala | 4 ++++ .../main/scala/org/http4s/dsl/impl/Path.scala | 19 +++++++++++---- .../main/scala/org/http4s/dsl/request.scala | 4 ++++ .../test/scala/org/http4s/dsl/PathSuite.scala | 23 +++++++++++++++++++ 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/docs/docs/dsl.md b/docs/docs/dsl.md index 95158ef0ece..10d19a9cbc6 100644 --- a/docs/docs/dsl.md +++ b/docs/docs/dsl.md @@ -423,22 +423,14 @@ val usersService = HttpRoutes.of[IO] { } ``` -If you want to extract a variable of type `T`, you can provide a custom extractor -object which implements `def unapply(str: String): Option[T]`, similar to the way -in which `IntVar` does it. +If you want to extract a variable of type `A`, you can provide a custom extractor +which implements `cast: String => Try[A]`, similar to the way in which `IntVar` does it. ```scala mdoc:silent import java.time.LocalDate import scala.util.Try -object LocalDateVar { - def unapply(str: String): Option[LocalDate] = { - if (!str.isEmpty) - Try(LocalDate.parse(str)).toOption - else - None - } -} +val LocalDateVar = PathVar(str => Try(LocalDate.parse(str))) def getTemperatureForecast(date: LocalDate): IO[Double] = IO(42.23) diff --git a/dsl/src/main/scala/org/http4s/dsl/Http4sDsl.scala b/dsl/src/main/scala/org/http4s/dsl/Http4sDsl.scala index 764fe09d608..b9310096d9b 100644 --- a/dsl/src/main/scala/org/http4s/dsl/Http4sDsl.scala +++ b/dsl/src/main/scala/org/http4s/dsl/Http4sDsl.scala @@ -21,6 +21,8 @@ import org.http4s.Method import org.http4s.Uri import org.http4s.dsl.impl._ +import scala.util.Try + trait Http4sDsl2[F[_], G[_]] extends RequestDsl with Statuses with Responses[F, G] { val Path: Uri.Path.type = Uri.Path val Root: Uri.Path.Root.type = Uri.Path.Root @@ -44,6 +46,8 @@ trait Http4sDsl2[F[_], G[_]] extends RequestDsl with Statuses with Responses[F, */ val → : impl.->.type = impl.-> + def PathVar[A](cast: String => Try[A]): impl.PathVar[A] = new impl.PathVar[A](cast) + val IntVar: impl.IntVar.type = impl.IntVar val LongVar: impl.LongVar.type = impl.LongVar val UUIDVar: impl.UUIDVar.type = impl.UUIDVar diff --git a/dsl/src/main/scala/org/http4s/dsl/impl/Path.scala b/dsl/src/main/scala/org/http4s/dsl/impl/Path.scala index 35c2d96063f..6735a6e6083 100644 --- a/dsl/src/main/scala/org/http4s/dsl/impl/Path.scala +++ b/dsl/src/main/scala/org/http4s/dsl/impl/Path.scala @@ -166,12 +166,21 @@ object /: { } } -protected class PathVar[A](cast: String => Try[A]) { +/** Abstract extractor of a path variable: + * {{{ + * enum Color: + * case Red, Green, Blue + * + * val ColorPath = new PathVar(str => Try(Color.valueOf(str))) + * + * Path("/Green") match { + * case Root / ColorPath(color) => ... + * }}} + */ +class PathVar[A](cast: String => Try[A]) { def unapply(str: String): Option[A] = - if (!str.isEmpty) - cast(str).toOption - else - None + if (str.nonEmpty) cast(str).toOption + else None } /** Integer extractor of a path variable: diff --git a/dsl/src/main/scala/org/http4s/dsl/request.scala b/dsl/src/main/scala/org/http4s/dsl/request.scala index d8bed76a52b..1d71c68e2a5 100644 --- a/dsl/src/main/scala/org/http4s/dsl/request.scala +++ b/dsl/src/main/scala/org/http4s/dsl/request.scala @@ -18,6 +18,8 @@ package org.http4s.dsl import org.http4s.Uri +import scala.util.Try + object request extends RequestDslBinCompat { val Path: Uri.Path.type = Uri.Path val Root: Uri.Path.Root.type = Uri.Path.Root @@ -28,6 +30,8 @@ object request extends RequestDslBinCompat { val /: : impl./:.type = impl./: val +& : impl.+&.type = impl.+& + def PathVar[A](cast: String => Try[A]): impl.PathVar[A] = new impl.PathVar[A](cast) + val IntVar: impl.IntVar.type = impl.IntVar val LongVar: impl.LongVar.type = impl.LongVar val UUIDVar: impl.UUIDVar.type = impl.UUIDVar diff --git a/dsl/src/test/scala/org/http4s/dsl/PathSuite.scala b/dsl/src/test/scala/org/http4s/dsl/PathSuite.scala index b7ff3401caf..5983fdec805 100644 --- a/dsl/src/test/scala/org/http4s/dsl/PathSuite.scala +++ b/dsl/src/test/scala/org/http4s/dsl/PathSuite.scala @@ -26,6 +26,8 @@ import org.http4s.syntax.AllSyntax import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen +import scala.util.Try + class PathSuite extends Http4sSuite with AllSyntax { implicit val arbitraryPath: Gen[Path] = arbitrary[List[String]] @@ -127,6 +129,27 @@ class PathSuite extends Http4sSuite with AllSyntax { }) } + test("Path should Abstract extractor") { + sealed trait Color + object Color { + case object Red extends Color + case object Green extends Color + case object Blue extends Color + + def valueOf(str: String): Color = str match { + case "Red" => Red + case "Green" => Green + case "Blue" => Blue + } + } + + val ColorVar = PathVar(str => Try(Color.valueOf(str))) + assert(path"/Green" match { + case Root / ColorVar(color) => color == Color.Green + case _ => false + }) + } + test("Path should Int extractor") { assert(path"/user/123" match { case Root / "user" / IntVar(userId) => userId == 123