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

Topic/val response #56

Merged
merged 3 commits into from
Feb 18, 2015
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions core/src/main/scala/org/http4s/rho/ExecutableCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rho

import bits.HeaderAST._
import bits.QueryAST._
import org.http4s.rho.bits.ResponseGeneratorInstances.BadRequest

import org.http4s.rho.bits._

Expand All @@ -29,17 +30,17 @@ trait ExecutableCompiler {

case HeaderCapture(key) => req.headers.get(key) match {
case Some(h) => ParserSuccess(h::stack)
case None => ValidationFailure(s"Missing header: ${key.name}")
case None => ValidationFailure(BadRequest(s"Missing header: ${key.name}"))
}

case HeaderRequire(key, f) => req.headers.get(key) match {
case Some(h) => if (f(h)) ParserSuccess(stack) else ValidationFailure(s"Invalid header: $h")
case None => ValidationFailure(s"Missing header: ${key.name}")
case Some(h) => f(h).fold[ParserResult[HList]](ParserSuccess(stack))(r =>ValidationFailure(r))
case None => ValidationFailure(BadRequest(s"Missing header: ${key.name}"))
}

case HeaderMapper(key, f) => req.headers.get(key) match {
case Some(h) => ParserSuccess(f(h)::stack)
case None => ValidationFailure(s"Missing header: ${key.name}")
case None => ValidationFailure(BadRequest(s"Missing header: ${key.name}"))
}

case MetaCons(r, _) => runValidation(req, r, stack)
Expand Down
10 changes: 8 additions & 2 deletions core/src/main/scala/org/http4s/rho/Result.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ import Result._

trait ResultSyntaxInstances {

implicit class ResultSyntax[T >: Result.TopResult <: BaseResult](r: T) extends MessageOps {
implicit class ResultSyntax[T >: Result.TopResult <: BaseResult](r: T) extends ResponseOps {
override type Self = T

def withStatus[S <% Status](status: S): Self = r.copy(resp = r.resp.copy(status = status))

override def attemptAs[T](implicit decoder: EntityDecoder[T]): DecodeResult[T] = {
val t: Task[ParseFailure\/T] = r.resp.attemptAs(decoder).run
EitherT[Task, ParseFailure, T](t)
Expand All @@ -103,9 +105,13 @@ trait ResultSyntaxInstances {
}
}

implicit class TaskResultSyntax[T >: Result.TopResult <: BaseResult](r: Task[T]) extends MessageOps {
implicit class TaskResultSyntax[T >: Result.TopResult <: BaseResult](r: Task[T]) extends ResponseOps {
override type Self = Task[T]

def withStatus[S <% Status](status: S): Self = r.map{ result =>
result.copy(resp = result.resp.copy(status = status))
}

override def attemptAs[T](implicit decoder: EntityDecoder[T]): DecodeResult[T] = {
val t: Task[ParseFailure\/T] = r.flatMap { t =>
t.resp.attemptAs(decoder).run
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/org/http4s/rho/RhoService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ trait RhoService extends bits.MethodAliases
case NoMatch => Task.now(None)
case ParserSuccess(t) => attempt(t).map(Some(_))
case ParserFailure(s) => onBadRequest(s).map(Some(_))
case ValidationFailure(s) => onBadRequest(s).map(Some(_))
case ValidationFailure(r) => r.map( r => Some(r.resp))
}
}

Expand All @@ -55,7 +55,7 @@ trait RhoService extends bits.MethodAliases
val w = EntityEncoder.stringEncoder
w.toEntity(reason).map{ entity =>
val hs = entity.length match {
case Some(l) => w.headers.put(Header.`Content-Length`(l))
case Some(l) => w.headers.put(headers.`Content-Length`(l))
case None => w.headers
}
Response(status, body = entity.body, headers = hs)
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/scala/org/http4s/rho/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import bits.PathAST._
import bits.HeaderAST._
import bits.QueryAST.QueryRule
import org.http4s.rho.bits.{HeaderAppendable, HListToFunc}
import headers.`Content-Type`

import shapeless.{::, HList}
import shapeless.ops.hlist.Prepend
Expand Down Expand Up @@ -70,7 +71,7 @@ case class CodecRouter[T <: HList, R](router: Router[T], decoder: EntityDecoder[

override val headers: HeaderRule = {
if (!decoder.consumes.isEmpty) {
val mt = requireThat(Header.`Content-Type`) { h: Header.`Content-Type`.HeaderT =>
val mt = requireThat(`Content-Type`) { h: `Content-Type`.HeaderT =>
decoder.matchesMediaType(h.mediaType)
}

Expand Down
5 changes: 4 additions & 1 deletion core/src/main/scala/org/http4s/rho/bits/HeaderAST.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.http4s
package rho.bits

import org.http4s.rho.Result.BaseResult
import shapeless.ops.hlist.Prepend
import shapeless.{::, HList}

import scalaz.concurrent.Task

/** AST representing the Header operations of the DSL */
object HeaderAST {

Expand All @@ -23,7 +26,7 @@ object HeaderAST {

sealed trait HeaderRule

case class HeaderRequire[T <: HeaderKey.Extractable](key: T, f: T#HeaderT => Boolean) extends HeaderRule
case class HeaderRequire[T <: HeaderKey.Extractable](key: T, f: T#HeaderT => Option[Task[BaseResult]]) extends HeaderRule

case class HeaderMapper[T <: HeaderKey.Extractable, R](key: T, f: T#HeaderT => R) extends HeaderRule

Expand Down
7 changes: 6 additions & 1 deletion core/src/main/scala/org/http4s/rho/bits/ParserResult.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.http4s.rho.bits

import org.http4s.Response
import org.http4s.rho.Result.BaseResult

import scalaz.concurrent.Task

sealed trait RouteResult[+T]

case object NoMatch extends RouteResult[Nothing]
Expand All @@ -26,5 +31,5 @@ sealed trait ParserResult[+T] extends RouteResult[T] {
case class ParserSuccess[+T](result: T) extends ParserResult[T]
case class ParserFailure(reason: String) extends ParserResult[Nothing]
// TODO: I think the reason for failure could be made easier to use with specific failure types
case class ValidationFailure(reason: String) extends ParserResult[Nothing]
case class ValidationFailure(response: Task[BaseResult]) extends ParserResult[Nothing]

19 changes: 11 additions & 8 deletions core/src/main/scala/org/http4s/rho/bits/QueryParser.scala
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
package org.http4s
package rho.bits

import org.http4s.rho.Result.BaseResult
import org.http4s.rho.bits.QueryParser.Params
import org.http4s.rho.bits.ResponseGeneratorInstances.BadRequest

import scala.language.higherKinds

import scalaz.{-\/, \/-}
import scala.annotation.tailrec
import scala.collection.generic.CanBuildFrom
import scalaz.concurrent.Task

trait QueryParser[A] {
import QueryParser.Params
def collect(name: String, params: Params, default: Option[A]): ParserResult[A]
}

final class ValidatingParser[A](parent: QueryParser[A], validate: A => Boolean) extends QueryParser[A] {
final class ValidatingParser[A](parent: QueryParser[A], validate: A => Option[Task[BaseResult]]) extends QueryParser[A] {
override def collect(name: String, params: Params, default: Option[A]): ParserResult[A] = {
val result = parent.collect(name, params, default)
result.flatMap{ r =>
if (validate(r)) result
else ValidationFailure("Invalid parameter: \"" + r + '"')
result.flatMap{ r => validate(r) match {
case None => result
case Some(resp) => ValidationFailure(resp)
}
}
}
}
Expand Down Expand Up @@ -73,14 +76,14 @@ object QueryParser {

case Some(Seq()) => default match {
case Some(defaultValue) => ParserSuccess(defaultValue)
case None => ValidationFailure(s"Value of query parameter '$name' missing")
case None => ValidationFailure(BadRequest(s"Value of query parameter '$name' missing"))
}
case None => default match {
case Some(defaultValue) => ParserSuccess(defaultValue)
case None => ValidationFailure(s"Missing query param: $name")
case None => ValidationFailure(BadRequest(s"Missing query param: $name"))
}
}
}
}

}

50 changes: 42 additions & 8 deletions core/src/main/scala/org/http4s/rho/package.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.http4s

import org.http4s.rho.Result.BaseResult
import org.http4s.rho.bits.ResponseGeneratorInstances.BadRequest

import scala.language.implicitConversions

import rho.bits.PathAST._
Expand All @@ -10,6 +13,7 @@ import shapeless.{HNil, ::}
import org.http4s.rho.bits._

import scala.reflect.runtime.universe.TypeTag
import scalaz.concurrent.Task

package object rho extends Http4s with ResultSyntaxInstances {

Expand All @@ -31,13 +35,36 @@ package object rho extends Http4s with ResultSyntaxInstances {
def param[T](name: String)(implicit parser: QueryParser[T], m: TypeTag[T]): TypedQuery[T :: HNil] =
TypedQuery(QueryCapture(name, parser, default = None, m))

/**
* Defines a parameter in query string that should be bound to a route definition.
* @param name name of the parameter in query
* @param default value that should be used if no or an invalid parameter is available
* @param validate predicate to determine if a parameter is valid
*/
def param[T](name: String, default: T, validate: T => Boolean = (_: T) => true)(implicit parser: QueryParser[T], m: TypeTag[T]): TypedQuery[T :: HNil] =
/** Define a query parameter with a default value */
def param[T](name: String, default: T)(implicit parser: QueryParser[T], m: TypeTag[T]): TypedQuery[T :: HNil] =
TypedQuery(QueryCapture(name, parser, default = Some(default), m))

/** Define a query parameter that will be validated with the predicate
*
* Failure of the predicate results in a '403: BadRequest' response. */
def param[T](name: String, validate: T => Boolean)
(implicit parser: QueryParser[T], m: TypeTag[T]): TypedQuery[T :: HNil] =
paramR(name, {t =>
if (validate(t)) None
else Some(BadRequest("Invalid query parameter: \"" + t + "\""))
})

/** Define a query parameter that will be validated with the predicate
*
* Failure of the predicate results in a '403: BadRequest' response. */
def param[T](name: String, default: T, validate: T => Boolean)
(implicit parser: QueryParser[T], m: TypeTag[T]): TypedQuery[T :: HNil] =
paramR(name, default, {t =>
if (validate(t)) None
else Some(BadRequest("Invalid query parameter: \"" + t + "\""))
})

/** Defines a parameter in query string that should be bound to a route definition. */
def paramR[T](name: String, validate: T => Option[Task[BaseResult]])(implicit parser: QueryParser[T], m: TypeTag[T]): TypedQuery[T :: HNil] =
TypedQuery(QueryCapture(name, new ValidatingParser(parser, validate), default = None, m))

/** Defines a parameter in query string that should be bound to a route definition. */
def paramR[T](name: String, default: T, validate: T => Option[Task[BaseResult]])(implicit parser: QueryParser[T], m: TypeTag[T]): TypedQuery[T :: HNil] =
TypedQuery(QueryCapture(name, new ValidatingParser(parser, validate), default = Some(default), m))

/**
Expand Down Expand Up @@ -65,10 +92,17 @@ package object rho extends Http4s with ResultSyntaxInstances {
/////////////////////////////// Header helpers //////////////////////////////////////

/* Checks that the header exists */
def require(header: HeaderKey.Extractable): TypedHeader[HNil] = requireThat(header)(_ => true)
def require(header: HeaderKey.Extractable): TypedHeader[HNil] = requireThatR(header)(_ => None)

/* Check that the header exists and satisfies the condition */
def requireThat[H <: HeaderKey.Extractable](header: H)(f: H#HeaderT => Boolean): TypedHeader[HNil] =
requireThatR(header){ h =>
if (f(h)) None
else Some(BadRequest("Invalid header: " + h.value))
}

/* Check that the header exists and satisfies the condition */
def requireThatR[H <: HeaderKey.Extractable](header: H)(f: H#HeaderT => Option[Task[BaseResult]]): TypedHeader[HNil] =
TypedHeader(HeaderRequire(header, f))

/** requires the header and will pull this header from the pile and put it into the function args stack */
Expand Down
Loading