Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ad-hoc filtering of directives #258

Merged
merged 3 commits into from Sep 9, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 27 additions & 12 deletions directives/src/main/scala/directives/Directive.scala
Expand Up @@ -70,24 +70,24 @@ extends (HttpRequest[T] => Result[R, A]) {
def flatMap[TT <: T, RR >: R, B](f:A => Directive[TT, RR, B]):Directive[TT, RR, B] =
Directive(r => run(r).flatMap(a => f(a)(r)))

/** Doesn't filter. Scala requires something to be defined for pattern matching in for
expressions, and we do use that. */
def withFilter(f:A => Boolean): Directive[T, R, A] = this
/** Doesn't filter. Scala requires something to be defined for pattern matching in for
expressions, and we do use that. */
def filter(f:A => Boolean): Directive[T, R, A] = this

def orElse[TT <: T, RR >: R, B >: A](next: => Directive[TT, RR, B]): Directive[TT, RR, B] =
Directive(r => run(r).orElse(next(r)))
new FilterDirective(r => run(r).orElse(next(r)), next)

def | [TT <: T, RR >: R, B >: A](next: => Directive[TT, RR, B]): Directive[TT, RR, B] =
orElse(next)

def and[TT <: T, E, B, RF](other: => Directive[TT, JoiningResponseFunction[E,RF], B])
(implicit ev: R <:< JoiningResponseFunction[E,RF]) =
new Directive[TT, JoiningResponseFunction[E,RF], (A,B)] ( req =>
this(req) and other(req)
)
(implicit ev: R <:< JoiningResponseFunction[E,RF]) = {
val runner = (req: HttpRequest[TT]) => this(req) and other(req)
// A `filter` implementation is required for pattern matching, which we
// use to extract joined successes. This is a no-op filter; an improved
// response joining system would make `toResponseFunction` available
// to use here with an empty Seq.
new FilterDirective[TT, JoiningResponseFunction[E,RF], (A,B)](
runner,
runner
)
}

def &[TT <: T, E, B, RF](other: => Directive[TT, JoiningResponseFunction[E,RF], B])
(implicit ev: R <:< JoiningResponseFunction[E,RF]) = this and other
Expand All @@ -98,6 +98,21 @@ extends (HttpRequest[T] => Result[R, A]) {
}
}

/** Supports filtering by requiring a handler for when success is filtered away.
The onEmpty handler may produce a success or failure; typically the latter. */
class FilterDirective[-T, +R, +A](
run: HttpRequest[T] => Result[R, A],
onEmpty: HttpRequest[T] => Result[R, A]
) extends Directive[T,R,A](run) {
def withFilter(filt: A => Boolean): Directive[T, R, A] =
new FilterDirective({ req =>
run(req).flatMap { a =>
if (filt(a)) Result.Success(a)
else onEmpty(req)
}
}, onEmpty)
}

class JoiningResponseFunction[E,A]
(val elements: List[E], toResponseFunction: Seq[E] => ResponseFunction[A])
extends ResponseFunction[A] {
Expand Down
6 changes: 5 additions & 1 deletion directives/src/main/scala/directives/Directives.scala
Expand Up @@ -45,7 +45,11 @@ trait Directives {

/* HttpRequest has to be of type Any because of type-inference (SLS 8.5) */
case class when[A](f:PartialFunction[HttpRequest[Any], A]){
def orElse[R](fail:ResponseFunction[R]) = Directive[Any, ResponseFunction[R], A](r => if(f.isDefinedAt(r)) Success(f(r)) else Failure(fail))
def orElse[R](fail:ResponseFunction[R]) =
new FilterDirective[Any, ResponseFunction[R], A](r =>
if(f.isDefinedAt(r)) Success(f(r)) else Failure(fail),
_ => Failure(fail)
)
}

def request[T] = Directive[T, Nothing, HttpRequest[T]](Success(_))
Expand Down
37 changes: 35 additions & 2 deletions directives/src/test/scala/DirectivesSpec.scala
Expand Up @@ -22,9 +22,13 @@ trait DirectivesSpec extends Specification with unfiltered.specs2.Hosted {

import dispatch._, Defaults._

// it's simple to define your own directives
// create a directive for a particular content type
def contentType(tpe:String) =
when{ case RequestContentType(`tpe`) => } orElse UnsupportedMediaType
when { case RequestContentType(`tpe`) => } orElse UnsupportedMediaType

// create a directive for any content type
def someContentType =
when { case RequestContentType(t) => t } orElse UnsupportedMediaType

// enables `===` in awesome_json case
implicit val contentTypeAwesome =
Expand Down Expand Up @@ -76,6 +80,13 @@ trait DirectivesSpec extends Specification with unfiltered.specs2.Hosted {
_ <- Accepts.Json
r <- request[Any]
} yield Ok ~> JsonContent ~> ResponseBytes(Body.bytes(r))
case Seg(List("if_json", id)) =>
for {
_ <- POST
contentType <- someContentType if contentType == "application/json"
_ <- Accepts.Json
r <- request[Any]
} yield Ok ~> JsonContent ~> ResponseBytes(Body.bytes(r))
case Seg(List("awesome_json", id)) =>
for {
_ <- POST
Expand Down Expand Up @@ -184,6 +195,28 @@ trait DirectivesSpec extends Specification with unfiltered.specs2.Hosted {
resp().getStatusCode must_== 415
}
}
"Directives if filtering" should {
"respond with json if accepted" in {
val resp = Http(localhost / "if_json" / "123"
<:< Map("Accept" -> "application/json")
<:< Map("Content-Type" -> "application/json")
<< someJson OK as.String)
resp() must_== someJson
}
"respond with unsupported media if content-type wrong" in {
val resp = Http(localhost / "if_json" / "123"
<:< Map("Accept" -> "application/json")
<:< Map("Content-Type" -> "text/plain")
<< someJson)
resp().getStatusCode must_== 415
}
"respond with unsupported media if content-type missing" in {
val resp = Http(localhost / "if_json" / "123"
<:< Map("Accept" -> "application/json")
<< someJson)
resp().getStatusCode must_== 415
}
}
"Directive parameters" should {
"respond with parameter if accepted" in {
val resp = Http(localhost / "valid_parameters"
Expand Down