http4s 1.0.0-M44 (latest development on cats effect 3)
To use your routes in a project using http4s 1.0.0-M44, add the following to your build.sbt
:
libraryDependencies += "bondlink" %% "routing-http4s_1.0.0-M44" % "5.0.0"
http4s handlers will be of the shape Params => org.http4s.Request[F] => F[org.http4s.Response[F]]
First let's rebuild our example routes.
import routing._
val Login = Method.GET / "login"
// Login: Route[GET, Unit] = /login
val Hello = Method.GET / "hello" / pathVar[String]("name")
// Hello: Route[GET, String] = /hello/<name: String>
val BlogPost = Method.GET / "post" / pathVar[String]("slug") :? queryParam[Int]("id")
// BlogPost: Route[GET, Tuple2[String, Int]] = /post/<slug: String>?<id: Int>
Then we can define our handlers.
import cats.effect.IO
import org.http4s.{Request, Response}
import org.http4s.dsl.io._
import routing.http4s._
val handledLogin = Login.handle(_ => (_: Request[IO]) => Ok("Login page"))
// handledLogin: Handled[Function1[Request[[A >: Nothing <: Any] => IO[A]], IO[Response[[A >: Nothing <: Any] => IO[A]]]]] {
type M >: Method <: Method
type P >: Params <: Params
type R >: Login <: Login
} = routing.Route$$anon$3@5a364df5
val handledHello = Hello.handle(name => (_: Request[IO]) => Ok(s"Hello, $name"))
// handledHello: Handled[Function1[Request[[A >: Nothing <: Any] => IO[A]], IO[Response[[A >: Nothing <: Any] => IO[A]]]]] {
type M >: Method <: Method
type P >: Params <: Params
type R >: Hello <: Hello
} = routing.Route$$anon$3@77814bc1
val handledBlogPost = BlogPost.handle { case (slug, id) => (req: Request[IO]) =>
Ok(s"Blog post with id: $id, slug: $slug found at ${req.uri}")
}
// handledBlogPost: Handled[Function1[Request[[A >: Nothing <: Any] => IO[A]], IO[Response[[A >: Nothing <: Any] => IO[A]]]]] {
type M >: Method <: Method
type P >: Params <: Params
type R >: BlogPost <: BlogPost
} = routing.Route$$anon$3@5ae0a9f5
Handled routes can be composed into an http4s service by passing them to Route.httpRoutes
:
import cats.effect.IO
import org.http4s.HttpRoutes
val service1: HttpRoutes[IO] = Route.httpRoutes(
handledLogin,
handledHello,
handledBlogPost
)
// service1: Kleisli[[_$7 >: Nothing <: Any] => OptionT[[A >: Nothing <: Any] => IO[A], _$7], Request[[A >: Nothing <: Any] => IO[A]], Response[[A >: Nothing <: Any] => IO[A]]] = Kleisli(
// run = org.http4s.Http$$$Lambda/0x0000007005e79d50@3f23b213
// )
If you prefer, you can call HttpRoutes.of
with a partial function that matches on your Route
s manually:
val service2: HttpRoutes[IO] = HttpRoutes.of {
case Login(_) => Ok("Login page")
case Hello(name) => Ok(s"Hello, $name")
case req @ BlogPost(slug, id) =>
Ok(s"Blog post with id: $id, slug: $slug found at ${req.uri}")
}
// service2: Kleisli[[_$7 >: Nothing <: Any] => OptionT[[A >: Nothing <: Any] => IO[A], _$7], Request[[A >: Nothing <: Any] => IO[A]], Response[[A >: Nothing <: Any] => IO[A]]] = Kleisli(
// run = org.http4s.HttpRoutes$$$Lambda/0x0000007005e7a378@6a1df0f7
// )
You can confirm that routes are matched correctly by passing some test requests to the service:
import cats.effect.unsafe.implicits.global
import org.http4s.Request
def testRoute(service: HttpRoutes[IO], call: Call): String =
service
.run(Request[IO](method = call.method.toHttp4s, uri = call.uri.toHttp4s))
.semiflatMap(_.as[String])
.value
.unsafeRunSync()
.get
testRoute(service1, Login())
// res0: String = "Login page"
testRoute(service1, Hello("world"))
// res1: String = "Hello, world"
testRoute(service1, BlogPost("my-slug", 1))
// res2: String = "Blog post with id: 1, slug: my-slug found at /post/my-slug?id=1"
testRoute(service2, Login())
// res3: String = "Login page"
testRoute(service2, Hello("world"))
// res4: String = "Hello, world"
testRoute(service2, BlogPost("my-slug", 1))
// res5: String = "Blog post with id: 1, slug: my-slug found at /post/my-slug?id=1"
You can also check that requests matching none of your routes are not handled by the service:
import org.http4s.{Method, Uri}
def unhandled(method: Method, path: String) =
service1.run(Request[IO](method = method, uri = Uri(path = Uri.Path.unsafeFromString(path)))).value.unsafeRunSync()
unhandled(GET, "/fake")
// res6: Option[Response[[A >: Nothing <: Any] => IO[A]]] = None
// Not handled by `Hello` because the method doesn't match
unhandled(POST, "/hello/world")
// res7: Option[Response[[A >: Nothing <: Any] => IO[A]]] = None