Skip to content

Commit

Permalink
Merge pull request finagle#148 from Sphonic/applicative-requestreader
Browse files Browse the repository at this point in the history
add applicative behaviour to RequestReader. Fixes finagle#145
  • Loading branch information
vkostyukov committed Jan 27, 2015
2 parents 52e5ec6 + aa57327 commit 10cc13a
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 1 deletion.
39 changes: 38 additions & 1 deletion core/src/main/scala/io/finch/request/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
* Ben Whitehead
* Ryan Plessner
* Pedro Viegas
* Jens Halm
*/

package io.finch

import com.twitter.finagle.httpx.Cookie
import com.twitter.util.Future
import com.twitter.util.{Future,Return,Throw,Try}

import scala.reflect.ClassTag

Expand Down Expand Up @@ -54,6 +55,27 @@ package object request {
def map[B](fn: A => B) = new RequestReader[B] {
def apply[Req](req: Req)(implicit ev: Req => HttpRequest) = self(req) map fn
}

def ~ [B](other: RequestReader[B]): RequestReader[A ~ B] = new RequestReader[A ~ B] {

def apply[Req] (req: Req)(implicit ev: Req => HttpRequest): Future[A ~ B] =
Future.join(self(req)(ev).liftToTry, other(req)(ev).liftToTry) flatMap {
case (Return(a), Return(b)) => new ~(a, b).toFuture
case (Throw(a), Throw(b)) => collectExceptions(a,b).toFutureException
case (Throw(e), _) => e.toFutureException
case (_, Throw(e)) => e.toFutureException
}

def collectExceptions (a: Throwable, b: Throwable): RequestReaderErrors = {

def collect (e: Throwable): Seq[Throwable] = e match {
case RequestReaderErrors(errors) => errors
case other => Seq(other)
}

RequestReaderErrors(collect(a) ++ collect(b))
}
}

// A workaround for https://issues.scala-lang.org/browse/SI-1336
def withFilter(p: A => Boolean) = self
Expand All @@ -66,6 +88,14 @@ package object request {
*/
class RequestReaderError(val message: String) extends Exception(message)

/**
* An exception that collects multiple request reader errors.
*
* @param errors the errors collected from various request readers
*/
case class RequestReaderErrors(errors: Seq[Throwable])
extends RequestReaderError("One or more errors reading request: " + errors.map(_.getMessage).mkString("\n ","\n ",""))

/**
* An exception that indicates missed parameter in the request.
*
Expand Down Expand Up @@ -869,4 +899,11 @@ package object request {
def apply(req: String): Option[A] = d(req)(tag)
}
}


/** A wrapper for two result values.
*/
case class ~[+A, +B](_1:A, _2:B)


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2014, by Vladimir Kostyukov and Contributors.
*
* This file is a part of a Finch library that may be found at
*
* https://github.com/finagle/finch
*
* Licensed under the Apache License, Version 2.0 (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributor(s):
* Jens Halm
*/
package io.finch.request

import org.scalatest.{FlatSpec,Matchers}

import com.twitter.finagle.httpx.Request
import com.twitter.util.{Await,Throw}

class ApplicativeRequestReaderSpec extends FlatSpec with Matchers {


val reader: RequestReader[(Int, Double, Int)] =
(RequiredIntParam("a") ~
RequiredDoubleParam("b") ~
RequiredIntParam("c")) map {
case a ~ b ~ c => (a, b, c)
}


"The applicative reader" should "produce three errors if all three numbers cannot be parsed" in {
val request = Request.apply("a"->"foo", "b"->"foo", "c"->"foo")
Await.result(reader(request).liftToTry) should be (Throw(RequestReaderErrors(Seq(
ValidationFailed("a", "should be integer"),
ValidationFailed("b", "should be double"),
ValidationFailed("c", "should be integer")
))))
}

it should "produce two validation errors if two numbers cannot be parsed" in {
val request = Request.apply("a"->"foo", "b"->"7.7", "c"->"foo")
Await.result(reader(request).liftToTry) should be (Throw(RequestReaderErrors(Seq(
ValidationFailed("a", "should be integer"),
ValidationFailed("c", "should be integer")
))))
}

it should "produce two ParamNotFound errors if two parameters are missing" in {
val request = Request.apply("b"->"7.7")
Await.result(reader(request).liftToTry) should be (Throw(RequestReaderErrors(Seq(
ParamNotFound("a"),
ParamNotFound("c")
))))
}

it should "produce one error if the last parameter cannot be parsed to an integer" in {
val request = Request.apply("a"->"9", "b"->"7.7", "c"->"foo")
Await.result(reader(request).liftToTry) should be (Throw(ValidationFailed("c","should be integer")))
}

it should "parse all integers and doubles" in {
val request = Request.apply("a"->"9", "b"->"7.7", "c"->"5")
Await.result(reader(request)) should be ((9,7.7,5))
}


}

0 comments on commit 10cc13a

Please sign in to comment.