Skip to content

Commit

Permalink
Initial work on Cats effect 3
Browse files Browse the repository at this point in the history
  • Loading branch information
zarthross committed Feb 21, 2021
1 parent d1dfacb commit 5a244c7
Show file tree
Hide file tree
Showing 33 changed files with 290 additions and 266 deletions.
2 changes: 1 addition & 1 deletion core/src/main/scala/org/http4s/rho/AuthedContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import cats.Monad
import cats.data.{Kleisli, OptionT}
import shapeless.{::, HNil}
import org.http4s.rho.bits.{FailureResponseOps, SuccessResponse, TypedHeader}
import _root_.io.chrisdavenport.vault._
import cats.effect._
import org.typelevel.vault.Key

/** The [[AuthedContext]] provides a convenient way to define a RhoRoutes
* which works with http4s authentication middleware.
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/org/http4s/rho/Result.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package rho

import cats._
import org.http4s.headers.`Content-Type`
import _root_.io.chrisdavenport.vault._
import org.typelevel.vault._

/** A helper for capturing the result types and status codes from routes */
sealed case class Result[
Expand Down
10 changes: 3 additions & 7 deletions core/src/main/scala/org/http4s/rho/UriConvertible.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,8 @@ object UriConvertible {
private[rho] def addPathInfo[F[_]](request: Request[F], tpl: UriTemplate): UriTemplate = {
val caret = request.attributes.lookup(Request.Keys.PathInfoCaret).getOrElse(0)
if (caret == 0) tpl
else if (caret == 1 && request.scriptName == "/") tpl
else tpl.copy(path = UriTemplate.PathElm(removeSlash(request.scriptName)) :: tpl.path)
else if (caret == 1 && request.scriptName.absolute) tpl
else
tpl.copy(path = UriTemplate.PathElm(request.scriptName.toRelative.renderString) :: tpl.path)
}

private[rho] def removeSlash(path: String): String =
if (path.startsWith("/")) path.substring(1)
else path

}
6 changes: 5 additions & 1 deletion core/src/main/scala/org/http4s/rho/bits/PathAST.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ object PathAST {

case class PathOr(p1: PathRule, p2: PathRule) extends PathRoute

case class PathMatch(s: String) extends PathOperation
case class PathMatch(s: Uri.Path.Segment) extends PathOperation
object PathMatch {
def apply(s: String): PathMatch = PathMatch(Uri.Path.Segment(s))
val empty: PathMatch = PathMatch(Uri.Path.Segment.empty)
}

case class PathCapture[F[_]](
name: String,
Expand Down
67 changes: 27 additions & 40 deletions core/src/main/scala/org/http4s/rho/bits/PathTree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,22 @@ import org.log4s.getLogger
import shapeless.{HList, HNil}

import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
import scala.util.control.NonFatal

object PathTree {

def apply[F[_]](): PathTreeOps[F]#PathTree = {
val ops = new PathTreeOps[F] {}
new ops.PathTree(ops.MatchNode(""))
new ops.PathTree(ops.MatchNode(Uri.Path.Segment.empty))
}

def splitPath(path: String): List[String] = {
val buff = new ListBuffer[String]
val len = path.length

@tailrec
def go(i: Int, begin: Int): Unit =
if (i < len) {
if (path.charAt(i) == '/') {
if (i > begin) buff += org.http4s.Uri.decode(path.substring(begin, i))
go(i + 1, i + 1)
} else go(i + 1, begin)
} else {
buff += org.http4s.Uri.decode(path.substring(begin, i))
}

val i = if (path.nonEmpty && path.charAt(0) == '/') 1 else 0
go(i, i)

buff.result()
def splitPath(path: Uri.Path): List[Uri.Path.Segment] = {
val fixEmpty =
if (path.isEmpty) List(Uri.Path.Segment.empty)
else path.segments.filterNot(_.isEmpty).toList
if (path.endsWithSlash) fixEmpty :+ Uri.Path.Segment.empty
else fixEmpty
}

}

private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {
Expand Down Expand Up @@ -80,9 +65,6 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {

type Action = ResultResponse[F, F[Response[F]]]

/** Generates a list of tokens that represent the path */
private def keyToPath(key: Request[F]): List[String] = PathTree.splitPath(key.pathInfo)

def makeLeaf[T <: HList](route: RhoRoute[F, T])(implicit F: Monad[F]): Leaf =
route.router match {
case Router(_, _, rules) =>
Expand Down Expand Up @@ -154,7 +136,7 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {

sealed trait Node[Self <: Node[Self]] extends ResponseGeneratorInstances[F] {

def matches: Map[String, MatchNode]
def matches: Map[Uri.Path.Segment, MatchNode]

def captures: List[CaptureNode]

Expand All @@ -163,7 +145,7 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {
def end: Map[Method, Leaf]

def clone(
matches: Map[String, MatchNode],
matches: Map[Uri.Path.Segment, MatchNode],
captures: List[CaptureNode],
variadic: Map[Method, Leaf],
end: Map[Method, Leaf]): Self
Expand All @@ -184,7 +166,7 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {

case MetaCons(r, _) => append(r :: t, method, action) // discard metadata

case PathMatch("") if !t.isEmpty =>
case PathMatch.empty if !t.isEmpty =>
append(t, method, action) // "" is a NOOP in the middle of a path

// the rest of the types need to rewrite a node
Expand Down Expand Up @@ -222,20 +204,25 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {
*/
final def walkTree(method: Method, req: Request[F])(implicit
F: Monad[F]): RouteResult[F, F[Response[F]]] = {
val path = keyToPath(req)
val path = PathTree.splitPath(req.pathInfo)
walk(method, req, path, HNil)
}

// This should scan all forward paths.
final protected def walk(method: Method, req: Request[F], path: List[String], stack: HList)(
implicit F: Monad[F]): RouteResult[F, F[Response[F]]] = {
final protected def walk(
method: Method,
req: Request[F],
path: List[Uri.Path.Segment],
stack: HList)(implicit F: Monad[F]): RouteResult[F, F[Response[F]]] = {
def tryVariadic(result: RouteResult[F, F[Response[F]]]): RouteResult[F, F[Response[F]]] =
variadic.get(method) match {
case None => result
case Some(l) => l.attempt(req, path :: stack)
}

def walkHeadTail(h: String, t: List[String]): RouteResult[F, F[Response[F]]] = {
def walkHeadTail(
h: Uri.Path.Segment,
t: List[Uri.Path.Segment]): RouteResult[F, F[Response[F]]] = {
val exact: RouteResult[F, F[Response[F]]] = matches.get(h) match {
case Some(n) => n.walk(method, req, t, stack)
case None => noMatch
Expand All @@ -247,7 +234,7 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {
error: RouteResult[F, F[Response[F]]]): RouteResult[F, F[Response[F]]] =
children match {
case (c @ CaptureNode(p, _, _, _, _)) :: ns =>
p.parse(h) match {
p.parse(h.decoded()) match { //TODO: Figure out how to inject char sets etc...
case SuccessResponse(r) =>
val n = c.walk(method, req, t, r :: stack)
if (n.isSuccess) n
Expand Down Expand Up @@ -290,15 +277,15 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {
}

case class MatchNode(
name: String,
matches: Map[String, MatchNode] = Map.empty[String, MatchNode],
name: Uri.Path.Segment,
matches: Map[Uri.Path.Segment, MatchNode] = Map.empty[Uri.Path.Segment, MatchNode],
captures: List[CaptureNode] = Nil,
variadic: Map[Method, Leaf] = Map.empty[Method, Leaf],
end: Map[Method, Leaf] = Map.empty[Method, Leaf])
extends Node[MatchNode] {

override def clone(
matches: Map[String, MatchNode],
matches: Map[Uri.Path.Segment, MatchNode],
captures: List[CaptureNode],
variadic: Map[Method, Leaf],
end: Map[Method, Leaf]): MatchNode =
Expand All @@ -321,14 +308,14 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {

case class CaptureNode(
parser: StringParser[F, _],
matches: Map[String, MatchNode] = Map.empty[String, MatchNode],
matches: Map[Uri.Path.Segment, MatchNode] = Map.empty[Uri.Path.Segment, MatchNode],
captures: List[CaptureNode] = List.empty[CaptureNode],
variadic: Map[Method, Leaf] = Map.empty[Method, Leaf],
end: Map[Method, Leaf] = Map.empty[Method, Leaf])
extends Node[CaptureNode] {

override def clone(
matches: Map[String, MatchNode],
matches: Map[Uri.Path.Segment, MatchNode],
captures: List[CaptureNode],
variadic: Map[Method, Leaf],
end: Map[Method, Leaf]): CaptureNode =
Expand Down Expand Up @@ -359,8 +346,8 @@ private[rho] trait PathTreeOps[F[_]] extends RuleExecutor[F] {
}

private def mergeMatches(
m1: Map[String, MatchNode],
m2: Map[String, MatchNode]): Map[String, MatchNode] =
m1: Map[Uri.Path.Segment, MatchNode],
m2: Map[Uri.Path.Segment, MatchNode]): Map[Uri.Path.Segment, MatchNode] =
mergeMaps(m1, m2)(_ merge _)

private def mergeLeaves(l1: Map[Method, Leaf], l2: Map[Method, Leaf]): Map[Method, Leaf] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ trait ResultMatchers[F[_]] extends ResultMatcherMidPrioInstances[F] {
override def conv(req: Request[F], r: Option[R])(implicit F: Monad[F]): F[Response[F]] =
r match {
case Some(res) => Ok.pure(res)
case None => NotFound.pure(req.uri.path)
case None => NotFound.pure(req.uri.path.renderString)
}
}

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/org/http4s/rho/bits/UriConverter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ object UriConverter {
case Nil => acc
case PathAnd(a, b) :: rs => go(a :: b :: rs, acc)
case PathOr(a, _) :: rs => go(a :: rs, acc) // we decided to take the first root
case PathMatch("") :: rs => go(rs, acc)
case PathMatch(s) :: rs => go(rs, PathElm(s) :: acc)
case PathMatch.empty :: rs => go(rs, acc)
case PathMatch(s) :: rs => go(rs, PathElm(s.encoded) :: acc)
case PathCapture(id, _, _, _) :: rs => go(rs, PathExp(id) :: acc)
case CaptureTail :: rs => go(rs, acc)
case MetaCons(p, _) :: rs => go(p :: rs, acc)
Expand Down
8 changes: 6 additions & 2 deletions core/src/test/scala/org/http4s/rho/ApiTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import org.http4s.Uri.uri
import org.specs2.matcher.MatchResult
import org.specs2.mutable._
import shapeless.{HList, HNil}
import cats.effect.unsafe.implicits.global
import scala.util.control.NoStackTrace

class ApiTest extends Specification {

Expand All @@ -38,7 +40,9 @@ class ApiTest extends Specification {
existsAnd(headers.`Content-Length`)(h => h.length != 0)

val RequireThrowException =
existsAnd(headers.`Content-Length`)(_ => throw new RuntimeException("this could happen"))
existsAnd(headers.`Content-Length`)(_ =>
throw new RuntimeException("this could happen") with NoStackTrace
)

def fetchETag(p: IO[Response[IO]]): ETag = {
val resp = p.unsafeRunSync()
Expand Down Expand Up @@ -181,7 +185,7 @@ class ApiTest extends Specification {
.resp
.status

val c3 = captureMapR(headers.`Access-Control-Allow-Credentials`, Some(r2))(_ => ???)
val c3 = captureMapR(headers.Accept, Some(r2))(_ => ???)
val v2 = ruleExecutor.runRequestRules(c3.rule, req)
v2 must beAnInstanceOf[FailureResponse[IO]]
v2.asInstanceOf[FailureResponse[IO]].toResponse.unsafeRunSync().status must_== r2
Expand Down
7 changes: 4 additions & 3 deletions core/src/test/scala/org/http4s/rho/AuthedContextSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ object MyRoutes extends RhoRoutes[IO] {
}

class AuthedContextSpec extends Specification {
import cats.effect.unsafe.implicits.global

val routes = Auth.authenticated(MyAuth.toService(MyRoutes.toRoutes()))

"AuthedContext execution" should {

"Be able to have access to authInfo" in {
val request = Request[IO](Method.GET, Uri(path = "/"))
val request = Request[IO](Method.GET, Uri(path = Uri.Path.fromString("/")))
val resp = routes.run(request).value.unsafeRunSync().getOrElse(Response.notFound)
if (resp.status == Status.Ok) {
val body =
Expand All @@ -60,7 +61,7 @@ class AuthedContextSpec extends Specification {
}

"Does not prevent route from being executed without authentication" in {
val request = Request[IO](Method.GET, Uri(path = "/public/public"))
val request = Request[IO](Method.GET, Uri(path = Uri.Path.fromString("/public/public")))
val resp = routes.run(request).value.unsafeRunSync().getOrElse(Response.notFound)
if (resp.status == Status.Ok) {
val body =
Expand All @@ -70,7 +71,7 @@ class AuthedContextSpec extends Specification {
}

"Does not prevent route from being executed without authentication, but allows to extract it" in {
val request = Request[IO](Method.GET, Uri(path = "/private/private"))
val request = Request[IO](Method.GET, Uri(path = Uri.Path.fromString("/private/private")))
val resp = routes.run(request).value.unsafeRunSync().getOrElse(Response.notFound)
if (resp.status == Status.Ok) {
val body =
Expand Down
6 changes: 4 additions & 2 deletions core/src/test/scala/org/http4s/rho/CodecRouterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import fs2.Stream
import org.specs2.mutable.Specification

import scala.collection.compat.immutable.ArraySeq
import org.http4s.Uri.Path

class CodecRouterSpec extends Specification {
import cats.effect.unsafe.implicits.global

def bodyAndStatus(resp: Response[IO]): (String, Status) = {
val rbody = new String(
Expand All @@ -27,7 +29,7 @@ class CodecRouterSpec extends Specification {

val b = Stream.emits(ArraySeq.unsafeWrapArray("hello".getBytes))
val h = Headers.of(headers.`Content-Type`(MediaType.text.plain))
val req = Request[IO](Method.POST, Uri(path = "/foo"), headers = h, body = b)
val req = Request[IO](Method.POST, Uri(path = Path.fromString("/foo")), headers = h, body = b)
val result = routes(req).value.unsafeRunSync().getOrElse(Response.notFound)
val (bb, s) = bodyAndStatus(result)

Expand All @@ -38,7 +40,7 @@ class CodecRouterSpec extends Specification {
"Fail on invalid body" in {
val b = Stream.emits(ArraySeq.unsafeWrapArray("hello =".getBytes))
val h = Headers.of(headers.`Content-Type`(MediaType.application.`x-www-form-urlencoded`))
val req = Request[IO](Method.POST, Uri(path = "/form"), headers = h, body = b)
val req = Request[IO](Method.POST, Uri(path = Path.fromString("/form")), headers = h, body = b)

routes(req).value.unsafeRunSync().map(_.status) must be some Status.BadRequest
}
Expand Down
16 changes: 8 additions & 8 deletions core/src/test/scala/org/http4s/rho/CompileRoutesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.http4s.rho.bits.MethodAliases._
import org.http4s.rho.io._
import org.http4s.{Method, Request, Uri}
import org.specs2.mutable.Specification
import org.http4s.Uri.Path

class CompileRoutesSpec extends Specification {

Expand All @@ -19,17 +20,16 @@ class CompileRoutesSpec extends Specification {
val c = RoutesBuilder[IO]()
getFoo(c)

"GetFoo" === RRunner(c.toRoutes()).checkOk(Request(uri = Uri(path = "/hello")))
"GetFoo" === RRunner(c.toRoutes()).checkOk(Request(uri=Uri(path=Path.fromString("/hello"))))
}

"Build multiple routes" in {
val c = RoutesBuilder[IO]()
getFoo(c)
putFoo(c)

"GetFoo" === RRunner(c.toRoutes()).checkOk(Request(uri = Uri(path = "/hello")))
"PutFoo" === RRunner(c.toRoutes())
.checkOk(Request(method = Method.PUT, uri = Uri(path = "/hello")))
"GetFoo" === RRunner(c.toRoutes()).checkOk(Request(uri=Uri(path=Path.fromString("/hello"))))
"PutFoo" === RRunner(c.toRoutes()).checkOk(Request(method = Method.PUT, uri=Uri(path=Path.fromString("/hello"))))
}

"Make routes from a collection of RhoRoutes" in {
Expand All @@ -39,17 +39,17 @@ class CompileRoutesSpec extends Specification {
(PUT / "hello" |>> "PutFoo") :: Nil

val srvc = CompileRoutes.foldRoutes[IO](routes)
"GetFoo" === RRunner(srvc).checkOk(Request(uri = Uri(path = "/hello")))
"PutFoo" === RRunner(srvc).checkOk(Request(method = Method.PUT, uri = Uri(path = "/hello")))
"GetFoo" === RRunner(srvc).checkOk(Request(uri=Uri(path=Path.fromString("/hello"))))
"PutFoo" === RRunner(srvc).checkOk(Request(method = Method.PUT, uri=Uri(path=Path.fromString("/hello"))))
}

"Concatenate correctly" in {
val c1 = RoutesBuilder[IO](); getFoo(c1)
val c2 = RoutesBuilder[IO](); putFoo(c2)

val srvc = c1.append(c2.routes()).toRoutes()
"GetFoo" === RRunner(srvc).checkOk(Request(uri = Uri(path = "/hello")))
"PutFoo" === RRunner(srvc).checkOk(Request(method = Method.PUT, uri = Uri(path = "/hello")))
"GetFoo" === RRunner(srvc).checkOk(Request(uri=Uri(path=Path.fromString("/hello"))))
"PutFoo" === RRunner(srvc).checkOk(Request(method = Method.PUT, uri=Uri(path=Path.fromString("/hello"))))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import scala.collection.immutable.Seq

import cats.effect.IO
import org.specs2.mutable.Specification
import cats.effect.unsafe.implicits.global

class ParamDefaultValueSpec extends Specification {

Expand Down
1 change: 1 addition & 0 deletions core/src/test/scala/org/http4s/rho/RequestRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.http4s.rho
import cats.effect.IO
import org.http4s._
import org.http4s.HttpRoutes
import cats.effect.unsafe.implicits.global

/** Helper for collecting a the body from a `RhoRoutes` */
trait RequestRunner {
Expand Down
Loading

0 comments on commit 5a244c7

Please sign in to comment.