From aa57327ea4e579652eee218a74eb9f3bbfb6c693 Mon Sep 17 00:00:00 2001 From: Jens Halm Date: Mon, 26 Jan 2015 16:25:46 +0100 Subject: [PATCH] add applicative behaviour to RequestReader. Fixes #145 --- .../main/scala/io/finch/request/package.scala | 39 +++++++++- .../ApplicativeRequestReaderSpec.scala | 77 +++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 core/src/test/scala/io/finch/request/ApplicativeRequestReaderSpec.scala diff --git a/core/src/main/scala/io/finch/request/package.scala b/core/src/main/scala/io/finch/request/package.scala index b0644c290..962b4ccb5 100644 --- a/core/src/main/scala/io/finch/request/package.scala +++ b/core/src/main/scala/io/finch/request/package.scala @@ -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 @@ -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 @@ -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. * @@ -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) + + } diff --git a/core/src/test/scala/io/finch/request/ApplicativeRequestReaderSpec.scala b/core/src/test/scala/io/finch/request/ApplicativeRequestReaderSpec.scala new file mode 100644 index 000000000..b11fc62fa --- /dev/null +++ b/core/src/test/scala/io/finch/request/ApplicativeRequestReaderSpec.scala @@ -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)) + } + + +}