diff --git a/benchmarks/src/main/scala/io/finch/benchmarks.scala b/benchmarks/src/main/scala/io/finch/benchmarks.scala index d55b25c34..92a21670c 100644 --- a/benchmarks/src/main/scala/io/finch/benchmarks.scala +++ b/benchmarks/src/main/scala/io/finch/benchmarks.scala @@ -27,22 +27,22 @@ class BodyBenchmark extends FinchBenchmark { val fooAsText: Endpoint[Foo] = body[Foo, Text.Plain] @Benchmark - def fooOption: Option[Option[Foo]] = fooOptionAsText(postPayload).value + def fooOption: Option[Option[Foo]] = fooOptionAsText(postPayload).awaitValueUnsafe() @Benchmark - def foo: Option[Foo] = fooAsText(postPayload).value + def foo: Option[Foo] = fooAsText(postPayload).awaitValueUnsafe() @Benchmark - def stringOption: Option[Option[String]] = stringBodyOption(postPayload).value + def stringOption: Option[Option[String]] = stringBodyOption(postPayload).awaitValueUnsafe() @Benchmark - def string: Option[String] = stringBody(postPayload).value + def string: Option[String] = stringBody(postPayload).awaitValueUnsafe() @Benchmark - def byteArrayOption: Option[Option[Array[Byte]]] = binaryBodyOption(postPayload).value + def byteArrayOption: Option[Option[Array[Byte]]] = binaryBodyOption(postPayload).awaitValueUnsafe() @Benchmark - def byteArray: Option[Array[Byte]] = binaryBody(postPayload).value + def byteArray: Option[Array[Byte]] = binaryBody(postPayload).awaitValueUnsafe() } @State(Scope.Benchmark) @@ -51,31 +51,31 @@ class MatchPathBenchmark extends FinchBenchmark { val foo: Endpoint[HNil] = "foo" @Benchmark - def stringSome: Option[HNil] = foo(getFooBarBaz).value + def stringSome: Option[HNil] = foo(getFooBarBaz).awaitValueUnsafe() @Benchmark - def stringNone: Option[HNil] = foo(getRoot).value + def stringNone: Option[HNil] = foo(getRoot).awaitValueUnsafe() } @State(Scope.Benchmark) class ExtractPathBenchmark extends FinchBenchmark { @Benchmark - def stringSome: Option[String] = string(getFooBarBaz).value + def stringSome: Option[String] = string(getFooBarBaz).awaitValueUnsafe() @Benchmark - def stringNone: Option[String] = string(getRoot).value + def stringNone: Option[String] = string(getRoot).awaitValueUnsafe() @Benchmark - def intSome: Option[Int] = int(getTenTwenty).value + def intSome: Option[Int] = int(getTenTwenty).awaitValueUnsafe() @Benchmark - def intNone: Option[Int] = int(getFooBarBaz).value + def intNone: Option[Int] = int(getFooBarBaz).awaitValueUnsafe() @Benchmark - def booleanSome: Option[Boolean] = boolean(getTrueFalse).value + def booleanSome: Option[Boolean] = boolean(getTrueFalse).awaitValueUnsafe() @Benchmark - def booleanNone: Option[Boolean] = boolean(getTenTwenty).value + def booleanNone: Option[Boolean] = boolean(getTenTwenty).awaitValueUnsafe() } @State(Scope.Benchmark) @@ -85,13 +85,13 @@ class ProductBenchmark extends FinchBenchmark { val right: Endpoint[(Int, String)] = Endpoint.empty[Int].product(Endpoint.const("foo")) @Benchmark - def bothMatched: Option[(Int, String)] = both(getRoot).value + def bothMatched: Option[(Int, String)] = both(getRoot).awaitValueUnsafe() @Benchmark - def leftMatched: Option[(Int, String)] = left(getRoot).value + def leftMatched: Option[(Int, String)] = left(getRoot).awaitValueUnsafe() @Benchmark - def rightMatched: Option[(Int, String)] = right(getRoot).value + def rightMatched: Option[(Int, String)] = right(getRoot).awaitValueUnsafe() } @State(Scope.Benchmark) @@ -101,13 +101,13 @@ class CoproductBenchmark extends FinchBenchmark { val right: Endpoint[String] = Endpoint.empty[String] | Endpoint.const("bar") @Benchmark - def bothMatched: Option[String] = both(getRoot).value + def bothMatched: Option[String] = both(getRoot).awaitValueUnsafe() @Benchmark - def leftMatched: Option[String] = left(getRoot).value + def leftMatched: Option[String] = left(getRoot).awaitValueUnsafe() @Benchmark - def rightMatched: Option[String] = right(getRoot).value + def rightMatched: Option[String] = right(getRoot).awaitValueUnsafe() } @State(Scope.Benchmark) @@ -119,14 +119,14 @@ class MapBenchmark extends FinchBenchmark { val mapTenOutputAsync: Endpoint[Int] = ten.mapOutputAsync(a => Future.value(Ok(a + 10))) @Benchmark - def map: Option[Int] = mapTen(getRoot).value + def map: Option[Int] = mapTen(getRoot).awaitValueUnsafe() @Benchmark - def mapAsync: Option[Int] = mapTenAsync(getRoot).value + def mapAsync: Option[Int] = mapTenAsync(getRoot).awaitValueUnsafe() @Benchmark - def mapOutput: Option[Int] = mapTenOutput(getRoot).value + def mapOutput: Option[Int] = mapTenOutput(getRoot).awaitValueUnsafe() @Benchmark - def mapOutputAsync: Option[Int] = mapTenOutputAsync(getRoot).value + def mapOutputAsync: Option[Int] = mapTenOutputAsync(getRoot).awaitValueUnsafe() } diff --git a/core/src/main/scala/io/finch/Endpoint.scala b/core/src/main/scala/io/finch/Endpoint.scala index 199ceaaf7..ebe9fd69d 100644 --- a/core/src/main/scala/io/finch/Endpoint.scala +++ b/core/src/main/scala/io/finch/Endpoint.scala @@ -71,11 +71,6 @@ trait Endpoint[A] { self => */ final def apply(mapper: Mapper[A]): Endpoint[mapper.Out] = mapper(self) - // There is a reason why `apply` can't be renamed to `run` as per - // https://github.com/finagle/finch/issues/371. - // More details are here: - // http://stackoverflow.com/questions/32064375/magnet-pattern-and-overloaded-methods - /** * Runs this endpoint. */ @@ -91,13 +86,14 @@ trait Endpoint[A] { self => * Maps this endpoint to the given function `A => Future[B]`. */ final def mapAsync[B](fn: A => Future[B]): Endpoint[B] = new Endpoint[B] { - def apply(input: Input): Endpoint.Result[B] = - self(input).map { - case (remainder, output) => remainder -> output.flatMapF(oa => oa.traverse(a => fn(a))) - } + final def apply(input: Input): Endpoint.Result[B] = self(input) match { + case EndpointResult.Matched(rem, out) => + EndpointResult.Matched(rem, out.flatMapF(oa => oa.traverse(a => fn(a)))) + case _ => EndpointResult.Skipped + } override def item = self.item - override def toString = self.toString + final override def toString: String = self.toString } /** @@ -110,22 +106,25 @@ trait Endpoint[A] { self => * Maps this endpoint to the given function `A => Future[Output[B]]`. */ final def mapOutputAsync[B](fn: A => Future[Output[B]]): Endpoint[B] = new Endpoint[B] { - def apply(input: Input): Endpoint.Result[B] = - self(input).map { - case (remainder, output) => remainder -> output.flatMapF { oa => - val fob = oa.traverse(fn).map(oob => oob.flatten) + final def apply(input: Input): Endpoint.Result[B] = self(input) match { + case EndpointResult.Matched(rem, out) => + val o = out.flatMapF { oa => + val fob = oa.traverse(fn).map(oob => oob.flatten) - fob.map { ob => - val ob1 = oa.headers.foldLeft(ob)((acc, x) => acc.withHeader(x)) - val ob2 = oa.cookies.foldLeft(ob1)((acc, x) => acc.withCookie(x)) + fob.map { ob => + val ob1 = oa.headers.foldLeft(ob)((acc, x) => acc.withHeader(x)) + val ob2 = oa.cookies.foldLeft(ob1)((acc, x) => acc.withCookie(x)) - ob2 - } + ob2 } - } + } + + EndpointResult.Matched(rem, o) + case _ => EndpointResult.Skipped + } override def item = self.item - override def toString = self.toString + final override def toString: String = self.toString } /** @@ -143,15 +142,17 @@ trait Endpoint[A] { self => * e.transform(f => Stat.timeFuture(s)(f)) * }}} */ - final def transform[B](fn: Future[Output[A]] => Future[Output[B]]): Endpoint[B] = new Endpoint[B] { - override def apply(input: Input): Endpoint.Result[B] = { - self(input).map { - case (remainder, output) => remainder -> new Rerunnable[Output[B]] { - override def run: Future[Output[B]] = fn(output.run) - } + final def transform[B](fn: Future[Output[A]] => Future[Output[B]]): Endpoint[B] = + new Endpoint[B] { + final def apply(input: Input): Endpoint.Result[B] = self(input) match { + case EndpointResult.Matched(rem, out) => + EndpointResult.Matched(rem, Rerunnable.fromFuture(fn(out.run))) + case _ => EndpointResult.Skipped } - } - } + + override def item = self.item + final override def toString: String = self.toString + } /** * Returns a product of this and `other` endpoint. The resulting endpoint returns a tuple @@ -180,16 +181,17 @@ trait Endpoint[A] { self => case (_, Throw(b)) => Future.exception(b) } - def apply(input: Input): Endpoint.Result[(A, B)] = - self(input).flatMap { - case (remainder1, outputA) => other(remainder1).map { - case (remainder2, outputB) => - remainder2 -> outputA.liftToTry.product(outputB.liftToTry).flatMapF(join) - } + final def apply(input: Input): Endpoint.Result[(A, B)] = self(input) match { + case a @ EndpointResult.Matched(_, _) => other(a.rem) match { + case b @ EndpointResult.Matched(_, _) => + EndpointResult.Matched(b.rem, a.out.liftToTry.product(b.out.liftToTry).flatMapF(join)) + case _ => EndpointResult.Skipped } + case _ => EndpointResult.Skipped + } override def item = self.item - override def toString = self.toString + final override def toString: String = self.toString } /** @@ -199,13 +201,15 @@ trait Endpoint[A] { self => final def adjoin[B](other: Endpoint[B])(implicit pairAdjoin: PairAdjoin[A, B] ): Endpoint[pairAdjoin.Out] = new Endpoint[pairAdjoin.Out] { - val inner = self.product(other).map { + + private[this] final val inner = self.product(other).map { case (a, b) => pairAdjoin(a, b) } - def apply(input: Input): Endpoint.Result[pairAdjoin.Out] = inner(input) + + final def apply(input: Input): Endpoint.Result[pairAdjoin.Out] = inner(input) override def item = items.MultipleItems - override def toString = s"${self.toString}/${other.toString}" + final override def toString: String = s"${self.toString} :: ${other.toString}" } /** @@ -219,18 +223,17 @@ trait Endpoint[A] { self => * will succeed if either this or `that` endpoints are succeed. */ final def |[B >: A](other: Endpoint[B]): Endpoint[B] = new Endpoint[B] { - private[this] def aToB(o: Endpoint.Result[A]): Endpoint.Result[B] = - o.asInstanceOf[Endpoint.Result[B]] - - def apply(input: Input): Endpoint.Result[B] = - (self(input), other(input)) match { - case (aa @ Some((a, _)), bb @ Some((b, _))) => - if (a.path.length <= b.path.length) aToB(aa) else bb - case (a, b) => aToB(a).orElse(b) + final def apply(input: Input): Endpoint.Result[B] = self(input) match { + case a @ EndpointResult.Matched(_, _) => other(input) match { + case b @ EndpointResult.Matched(_, _) => + if (a.rem.path.length <= b.rem.path.length) a else b + case _ => a } + case _ => other(input) + } override def item = items.MultipleItems - override def toString = s"(${self.toString}|${other.toString})" + final override def toString: String = s"(${self.toString}|${other.toString})" } /** @@ -335,15 +338,17 @@ trait Endpoint[A] { self => * Lifts this endpoint into one that always succeeds, with an empty `Option` representing failure. */ final def lift: Endpoint[Option[A]] = new Endpoint[Option[A]] { - def apply(input: Input): Endpoint.Result[Option[A]] = - self(input).map { - case (remainder, output) => - remainder -> output.liftToTry - .map(toa => toa.toOption.fold(Output.None: Output[Option[A]])(o => o.map(Some.apply))) - } + final def apply(input: Input): Endpoint.Result[Option[A]] = self(input) match { + case EndpointResult.Matched(rem, out) => + val o = out.liftToTry.map(toa => + toa.toOption.fold(Output.None: Output[Option[A]])(o => o.map(Some.apply)) + ) + EndpointResult.Matched(rem, o) + case _ => EndpointResult.Skipped + } override def item = self.item - override def toString = self.toString + override final def toString: String = self.toString } private[this] def withOutput[B](fn: Output[A] => Output[B]): Endpoint[B] = @@ -355,20 +360,21 @@ trait Endpoint[A] { self => */ object Endpoint { - type Result[A] = Option[(Input, Rerunnable[Output[A]])] + type Result[A] = EndpointResult[A] /** * Creates an empty [[Endpoint]] (an endpoint that never matches) for a given type. */ def empty[A]: Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Result[A] = None + final def apply(input: Input): Result[A] = EndpointResult.Skipped } /** * Creates an [[Endpoint]] that always matches and returns a given value (evaluated eagerly). */ def const[A](a: A): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Result[A] = Some(input -> Rs.const(Output.payload(a))) + final def apply(input: Input): Result[A] = + EndpointResult.Matched(input, Rs.const(Output.payload(a))) } /** @@ -384,23 +390,24 @@ object Endpoint { * }}} */ def lift[A](a: => A): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Result[A] = - Some(input -> Rerunnable(Output.payload(a))) + final def apply(input: Input): Result[A] = + EndpointResult.Matched(input, Rerunnable(Output.payload(a))) } /** * Creates an [[Endpoint]] that always matches and returns a given `Future` (evaluated lazily). */ def liftFuture[A](fa: => Future[A]): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Result[A] = - Some(input -> Rerunnable.fromFuture(fa).map(a => Output.payload(a))) + final def apply(input: Input): Result[A] = + EndpointResult.Matched(input, Rerunnable.fromFuture(fa).map(a => Output.payload(a))) } /** * Creates an [[Endpoint]] that always matches and returns a given `Output` (evaluated lazily). */ def liftOutput[A](oa: => Output[A]): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Result[A] = Some(input -> Rerunnable(oa)) + final def apply(input: Input): Result[A] = + EndpointResult.Matched(input, Rerunnable(oa)) } /** @@ -408,15 +415,16 @@ object Endpoint { * (evaluated lazily). */ def liftFutureOutput[A](foa: => Future[Output[A]]): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Result[A] = Some(input -> Rerunnable.fromFuture(foa)) + final def apply(input: Input): Result[A] = + EndpointResult.Matched(input, Rerunnable.fromFuture(foa)) } private[finch] def embed[A](i: items.RequestItem)(f: Input => Result[A]): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Result[A] = f(input) + final def apply(input: Input): Result[A] = f(input) override def item: items.RequestItem = i - override def toString: String = + override final def toString: String = s"${item.kind}${item.nameOption.map(n => "(" + n + ")").getOrElse("")}" } @@ -524,18 +532,7 @@ object Endpoint { * or more of the elements in the `Seq`. It will succeed if the result is empty or type conversion * succeeds for all elements. */ - implicit class StringSeqEndpointOps(val self: Endpoint[Seq[String]]) { - - /* IMPLEMENTATION NOTE: This implicit class should extend AnyVal like all the other ones, to - * avoid instance creation for each invocation of the extension method. However, this let's us - * run into a compiler bug when we compile for Scala 2.10: - * https://issues.scala-lang.org/browse/SI-8018. The bug is caused by the combination of four - * things: 1) an implicit class, 2) extending AnyVal, 3) wrapping a class with type parameters, - * 4) a partial function in the body. 2) is the only thing we can easily remove here, otherwise - * we'd need to move the body of the method somewhere else. Once we drop support for Scala 2.10, - * this class can safely extends AnyVal. - */ - + implicit class StringSeqEndpointOps(val self: Endpoint[Seq[String]]) extends AnyVal { def as[A](implicit d: DecodeEntity[A], tag: ClassTag[A]): Endpoint[Seq[A]] = self.mapAsync { items => val decoded = items.map(d.apply) @@ -580,21 +577,17 @@ object Endpoint { def derive[A]: GenericDerivation[A] = new GenericDerivation[A] implicit val endpointInstance: Alternative[Endpoint] = new Alternative[Endpoint] { + final override def ap[A, B](ff: Endpoint[A => B])(fa: Endpoint[A]): Endpoint[B] = + ff.product(fa).map { case (f, a) => f(a)} - override def ap[A, B](ff: Endpoint[A => B])(fa: Endpoint[A]): Endpoint[B] = ff.product(fa).map { - case (f, a) => f(a) - } - - override def map[A, B](fa: Endpoint[A])(f: A => B): Endpoint[B] = fa.map(f) - - override def product[A, B](fa: Endpoint[A], fb: Endpoint[B]): Endpoint[(A, B)] = fa.product(fb) - - override def pure[A](x: A): Endpoint[A] = new Endpoint[A] { - override def apply(input: Input): Result[A] = Some(input -> Rerunnable(Output.payload(x))) - } + final override def map[A, B](fa: Endpoint[A])(f: A => B): Endpoint[B] = + fa.map(f) - override def empty[A]: Endpoint[A] = Endpoint.empty[A] + final override def product[A, B](fa: Endpoint[A], fb: Endpoint[B]): Endpoint[(A, B)] = + fa.product(fb) - override def combineK[A](x: Endpoint[A], y: Endpoint[A]): Endpoint[A] = x | y + final override def pure[A](x: A): Endpoint[A] = Endpoint.const(x) + final override def empty[A]: Endpoint[A] = Endpoint.empty[A] + final override def combineK[A](x: Endpoint[A], y: Endpoint[A]): Endpoint[A] = x | y } } diff --git a/core/src/main/scala/io/finch/EndpointResult.scala b/core/src/main/scala/io/finch/EndpointResult.scala new file mode 100644 index 000000000..a1060c09b --- /dev/null +++ b/core/src/main/scala/io/finch/EndpointResult.scala @@ -0,0 +1,144 @@ +package io.finch + +import com.twitter.util.{Await, Duration, Try} +import io.catbird.util.Rerunnable + +/** + * A result returned from an [[Endpoint]]. This models `Option[(Input, Future[Output])]` and + * represents two cases: + * + * - Endpoint is matched so both `remainder` and `output` is returned. + * - Endpoint is skipped so `None` is returned. + * + * API methods exposed on this type are mostly introduced for testing. + * + * This class also provides various of `awaitX` methods useful for testing and experimenting. + */ +sealed abstract class EndpointResult[+A] { + + /** + * Whether the [[Endpoint]] is matched on a given [[Input]]. + */ + def isMatched: Boolean + + /** + * Returns the remainder of the [[Input]] after an [[Endpoint]] is matched. + * + * @return `Some(remainder)` if this endpoint was matched on a given input, + * `None` otherwise. + */ + final def remainder: Option[Input] = this match { + case EndpointResult.Matched(rem, _) => Some(rem) + case _ => None + } + + /** + * Awaits for an [[Output]] wrapped with [[Try]] (indicating if the [[com.twitter.util.Future]] + * is failed). + * + * @note This method is blocking. Never use it in production. + * + * @return `Some(output)` if this endpoint was matched on a given input, `None` otherwise. + */ + final def awaitOutput(d: Duration = Duration.Top): Option[Try[Output[A]]] = this match { + case EndpointResult.Matched(_, out) => Some(Await.result(out.liftToTry.run, d)) + case _ => None + } + + /** + * Awaits an [[Output]] of the [[Endpoint]] result or throws an exception if an underlying + * [[com.twitter.util.Future]] is failed. + * + * @note This method is blocking. Never use it in production. + * + * @return `Some(output)` if this endpoint was matched on a given input, `None` otherwise. + */ + final def awaitOutputUnsafe(d: Duration = Duration.Top): Option[Output[A]] = + awaitOutput(d).map(toa => toa.get) + + /** + * Awaits a value from the [[Output]] wrapped with [[Try]] (indicating if either the + * [[com.twitter.util.Future]] is failed or [[Output]] wasn't a payload). + * + * @note This method is blocking. Never use it in production. + * + * @return `Some(value)` if this endpoint was matched on a given input, `None` otherwise. + */ + final def awaitValue(d: Duration = Duration.Top): Option[Try[A]] = + awaitOutput(d).map(toa => toa.flatMap(oa => Try(oa.value))) + + /** + * Awaits a value from the [[Output]] or throws an exception if either an underlying + * [[com.twitter.util.Future]] is failed or [[Output]] wasn't a payload. + * + * @note @note This method is blocking. Never use it in production. + * + * @return `Some(value)` if this endpoint was matched on a given input, + * `None` otherwise. + */ + final def awaitValueUnsafe(d: Duration = Duration.Top): Option[A] = + awaitOutputUnsafe(d).map(oa => oa.value) + + /** + * Queries an [[Output]] wrapped with [[Try]] (indicating if the [[com.twitter.util.Future]] is + * failed). + * + * @note This method is blocking and awaits on the underlying [[com.twitter.util.Future]] with + * the upper bound of 10 seconds. + * + * @return `Some(output)` if this endpoint was matched on a given input, + * `None` otherwise. + */ + @deprecated("Use awaitOutput(Duration) instead", "0.12") + final def tryOutput: Option[Try[Output[A]]] = awaitOutput(Duration.fromSeconds(10)) + + /** + * Queries a value from the [[Output]] wrapped with [[Try]] (indicating if either the + * [[com.twitter.util.Future]] is failed or [[Output]] wasn't a payload). + * + * @note This method is blocking and awaits on the underlying [[com.twitter.util.Future]] with + * the upper bound of 10 seconds. + * + * @return `Some(value)` if this endpoint was matched on a given input, + * `None` otherwise. + */ + @deprecated("Use awaitValue(Duration) instead", "0.12") + final def tryValue: Option[Try[A]] = awaitValue(Duration.fromSeconds(10)) + + /** + * Queries an [[Output]] of the [[Endpoint]] result or throws an exception if an underlying + * [[com.twitter.util.Future]] is failed. + * + * @note This method is blocking and awaits on the underlying [[com.twitter.util.Future]] + * with the upper bound of 10 seconds. + * + * @return `Some(output)` if this endpoint was matched on a given input, + * `None` otherwise. + */ + @deprecated("Use awaitOutputUnsafe(Duration) instead", "0.12") + final def output: Option[Output[A]] = awaitOutputUnsafe(Duration.fromSeconds(10)) + + /** + * Queries the value from the [[Output]] or throws an exception if either an underlying + * [[com.twitter.util.Future]] is failed or [[Output]] wasn't a payload. + * + * @note This method is blocking and awaits on the underlying [[com.twitter.util.Future]] with + * the upper bound of 10 seconds. + * + * @return `Some(value)` if this endpoint was matched on a given input, + * `None` otherwise. + */ + @deprecated("Use awaitValueUnsafe instead", "0.12") + final def value: Option[A] = awaitValueUnsafe(Duration.fromSeconds(10)) +} + +object EndpointResult { + + case object Skipped extends EndpointResult[Nothing] { + def isMatched: Boolean = false + } + + final case class Matched[A](rem: Input, out: Rerunnable[Output[A]]) extends EndpointResult[A] { + def isMatched: Boolean = true + } +} diff --git a/core/src/main/scala/io/finch/Endpoints.scala b/core/src/main/scala/io/finch/Endpoints.scala index 28fb6c6c5..662f25824 100644 --- a/core/src/main/scala/io/finch/Endpoints.scala +++ b/core/src/main/scala/io/finch/Endpoints.scala @@ -27,13 +27,12 @@ trait Endpoints { * An universal [[Endpoint]] that matches the given string. */ private[finch] class Matcher(s: String) extends Endpoint[HNil] { - def apply(input: Input): Endpoint.Result[HNil] = - input.headOption.flatMap { - case `s` => Some(input.drop(1) -> Rs.OutputHNil) - case _ => None - } + final def apply(input: Input): Endpoint.Result[HNil] = input.headOption match { + case Some(`s`) => EndpointResult.Matched(input.drop(1), Rs.OutputHNil) + case _ => EndpointResult.Skipped + } - override def toString: String = s + override final def toString: String = s } implicit def stringToMatcher(s: String): Endpoint0 = new Matcher(s) @@ -45,28 +44,28 @@ trait Endpoints { * from the string. */ private[finch] case class Extractor[A](name: String, f: String => Option[A]) extends Endpoint[A] { - def apply(input: Input): Endpoint.Result[A] = - for { - ss <- input.headOption - aa <- f(ss) - } yield input.drop(1) -> new Rerunnable[Output[A]] { - override def run = Future.value(Output.payload(aa)) + final def apply(input: Input): Endpoint.Result[A] = input.headOption match { + case Some(ss) => f(ss) match { + case Some(a) => EndpointResult.Matched(input.drop(1), Rs.const(Output.payload(a))) + case _ => EndpointResult.Skipped } + case _ => EndpointResult.Skipped + } def apply(n: String): Endpoint[A] = copy[A](name = n) - override def toString: String = s":$name" + override final def toString: String = s":$name" } private[finch] case class StringExtractor(name: String) extends Endpoint[String] { - def apply(input: Input): Endpoint.Result[String] = - input.headOption.map(s => input.drop(1) -> new Rerunnable[Output[String]] { - override def run = Future.value(Output.payload(s)) - }) + final def apply(input: Input): Endpoint.Result[String] = input.headOption match { + case Some(s) => EndpointResult.Matched(input.drop(1), Rs.const(Output.payload(s))) + case _ => EndpointResult.Skipped + } - def apply(n: String): Endpoint[String] = copy(name = n) + final def apply(n: String): Endpoint[String] = copy(name = n) - override def toString: String = s":$name" + final override def toString: String = s":$name" } /** @@ -75,38 +74,34 @@ trait Endpoints { private[finch] case class TailExtractor[A]( name: String, f: String => Option[A]) extends Endpoint[Seq[A]] { - def apply(input: Input): Endpoint.Result[Seq[A]] = - Some(input.copy(path = Nil) -> new Rerunnable[Output[Seq[A]]] { - override def run = Future.value(Output.payload(for { - s <- input.path - a <- f(s) - } yield a)) - }) - def apply(n: String): Endpoint[Seq[A]] = copy[A](name = n) + final def apply(input: Input): Endpoint.Result[Seq[A]] = + EndpointResult.Matched( + input.copy(path = Nil), + Rs.const(Output.payload(input.path.flatMap(f.andThen(_.toSeq)))) + ) + + final def apply(n: String): Endpoint[Seq[A]] = copy[A](name = n) - override def toString: String = s":$name*" + final override def toString: String = s":$name*" } private[this] def extractUUID(s: String): Option[UUID] = if (s.length != 36) None else try Some(UUID.fromString(s)) catch { case _: Exception => None } - private[this] def result[A](i: Input, a: A): (Input, Rerunnable[Output[A]]) = - i.drop(1) -> new Rerunnable[Output[A]] { - override def run = Future.value(Output.payload(a)) - } - /** * A matching [[Endpoint]] that reads a string value from the current path segment. * * @note This is an experimental API and might be removed without any notice. */ val path: Endpoint[String] = new Endpoint[String] { - def apply(input: Input): Endpoint.Result[String] = - input.headOption.map(s => result(input, s)) + final def apply(input: Input): Endpoint.Result[String] = input.headOption match { + case Some(s) => EndpointResult.Matched(input.drop(1), Rs.const(Output.payload(s))) + case _ => EndpointResult.Skipped + } - override def toString: String = ":path" + final override def toString: String = ":path" } /** @@ -114,12 +109,17 @@ trait Endpoints { * [[DecodePath]] instances defined for `A`) from the current path segment. */ def path[A](implicit c: DecodePath[A]): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Endpoint.Result[A] = for { - ss <- input.headOption - aa <- c(ss) - } yield result(input, aa) + final def apply(input: Input): Endpoint.Result[A] = input.headOption match { + case Some(s) => c(s) match { + case Some(a) => + EndpointResult.Matched(input.drop(1), Rs.const(Output.payload(a))) + case _ => EndpointResult.Skipped + + } + case _ => EndpointResult.Skipped + } - override def toString: String = ":path" + final override def toString: String = ":path" } /** @@ -176,28 +176,28 @@ trait Endpoints { * An [[Endpoint]] that skips all path segments. */ object * extends Endpoint[HNil] { - def apply(input: Input): Endpoint.Result[HNil] = - Some(input.copy(path = Nil) -> Rs.OutputHNil) + final def apply(input: Input): Endpoint.Result[HNil] = + EndpointResult.Matched(input.copy(path = Nil), Rs.OutputHNil) - override def toString: String = "*" + final override def toString: String = "*" } /** * An identity [[Endpoint]]. */ object / extends Endpoint[HNil] { - def apply(input: Input): Endpoint.Result[HNil] = - Some(input -> Rs.OutputHNil) + final def apply(input: Input): Endpoint.Result[HNil] = + EndpointResult.Matched(input, Rs.OutputHNil) - override def toString: String = "" + final override def toString: String = "" } private[this] def method[A](m: Method)(r: Endpoint[A]): Endpoint[A] = new Endpoint[A] { - def apply(input: Input): Endpoint.Result[A] = + final def apply(input: Input): Endpoint.Result[A] = if (input.request.method == m) r(input) - else None + else EndpointResult.Skipped - override def toString: String = s"${m.toString().toUpperCase} /${r.toString}" + final override def toString: String = s"${m.toString().toUpperCase} /${r.toString}" } /** @@ -312,24 +312,25 @@ trait Endpoints { private[this] def option[A](item: items.RequestItem)(f: Request => A): Endpoint[A] = Endpoint.embed(item)(input => - Some(input -> new Rerunnable[Output[A]] { - override def run = Future.value(Output.payload(f(input.request))) - })) + EndpointResult.Matched(input, Rerunnable(Output.payload(f(input.request)))) + ) private[this] def exists[A](item: items.RequestItem)(f: Request => Option[A]): Endpoint[A] = - Endpoint.embed(item)(input => - f(input.request).map(s => input -> new Rerunnable[Output[A]] { - override def run = Future.value(Output.payload(s)) - }) - ) + Endpoint.embed(item) { input => + f(input.request) match { + case Some(a) => EndpointResult.Matched(input, Rerunnable(Output.payload(a))) + case _ => EndpointResult.Skipped + } + } private[this] def matches[A] (item: items.RequestItem) (p: Request => Boolean) (f: Request => A): Endpoint[A] = Endpoint.embed(item)(input => - if (p(input.request)) Some(input -> new Rerunnable[Output[A]] { - override def run = Future.value(Output.payload(f(input.request))) - }) else None + if (p(input.request)) + EndpointResult.Matched(input, Rerunnable(Output.payload(f(input.request)))) + else + EndpointResult.Skipped ) /** @@ -514,10 +515,11 @@ trait Endpoints { .withHeader("WWW-Authenticate" -> s"""Basic realm="$realm"""")) } - def apply(input: Input): Endpoint.Result[A] = - e(input).map { case (input, output) => - input -> authenticated(input).flatMap(if (_) output else unauthorized) - } + final def apply(input: Input): Endpoint.Result[A] = e(input) match { + case EndpointResult.Matched(rem, out) => + EndpointResult.Matched(rem, authenticated(rem).flatMap(if (_) out else unauthorized)) + case _ => EndpointResult.Skipped + } private[this] def authenticated(input: Input): Rerunnable[Boolean] = Rerunnable.fromFuture( diff --git a/core/src/main/scala/io/finch/Output.scala b/core/src/main/scala/io/finch/Output.scala index cf3086eb5..de81dd3d7 100644 --- a/core/src/main/scala/io/finch/Output.scala +++ b/core/src/main/scala/io/finch/Output.scala @@ -1,8 +1,8 @@ package io.finch import cats.Eq -import com.twitter.finagle.http.{Cookie, Response, Status, Version} -import com.twitter.util.{Await, Duration, Future, Try} +import com.twitter.finagle.http.{Cookie, Response, Status} +import com.twitter.util.Future import io.finch.internal.ToResponse import java.nio.charset.{Charset, StandardCharsets} @@ -166,69 +166,6 @@ object Output { case (_, _) => false } - /** - * Exposes an API for testing [[Endpoint]]s. - */ - implicit class EndpointResultOps[A](val o: Endpoint.Result[A]) extends AnyVal { - - /** - * Queries an [[Output]] wrapped with [[Try]] (indicating if the [[Future]] is failed). - * - * @note This method is blocking and awaits on the underlying [[Future]] with the upper - * bound of 10 seconds. - * - * @return `Some(output)` if this endpoint was matched on a given input, - * `None` otherwise. - */ - def tryOutput: Option[Try[Output[A]]] = - o.map({ case (_, oa) => Await.result(oa.liftToTry.run, Duration.fromSeconds(10)) }) - - /** - * Queries a value from the [[Output]] wrapped with [[Try]] (indicating if either the - * [[Future]] is failed or [[Output]] wasn't a payload). - * - * @note This method is blocking and awaits on the underlying [[Future]] with the upper - * bound of 10 seconds. - * - * @return `Some(value)` if this endpoint was matched on a given input, - * `None` otherwise. - */ - def tryValue: Option[Try[A]] = - tryOutput.map(toa => toa.flatMap(oa => Try(oa.value))) - - /** - * Queries an [[Output]] of the [[Endpoint]] result or throws an exception if an underlying - * [[Future]] is failed. - * - * @note This method is blocking and awaits on the underlying [[Future]] with the upper - * bound of 10 seconds. - * - * @return `Some(output)` if this endpoint was matched on a given input, - * `None` otherwise. - */ - def output: Option[Output[A]] = tryOutput.map(tao => tao.get) - - /** - * Queries the value from the [[Output]] or throws an exception if either an underlying - * [[Future]] is failed or [[Output]] wasn't a payload. - * - * @note This method is blocking and awaits on the underlying [[Future]] with the upper - * bound of 10 seconds. - * - * @return `Some(value)` if this endpoint was matched on a given input, - * `None` otherwise. - */ - def value: Option[A] = output.map(oa => oa.value) - - /** - * Returns the remainder of the [[Input]] after an [[Endpoint]] is matched. - * - * @return `Some(remainder)` if this endpoint was matched on a given input, - * `None` otherwise. - */ - def remainder: Option[Input] = o.map(_._1) - } - implicit class OutputOps[A](val o: Output[A]) extends AnyVal { /** diff --git a/core/src/main/scala/io/finch/endpoint/Body.scala b/core/src/main/scala/io/finch/endpoint/Body.scala index 317ef61d7..d39d33e2a 100644 --- a/core/src/main/scala/io/finch/endpoint/Body.scala +++ b/core/src/main/scala/io/finch/endpoint/Body.scala @@ -20,14 +20,13 @@ private[finch] abstract class Body[A, B, CT <: String]( } final def apply(input: Input): Endpoint.Result[B] = - if (input.request.isChunked) None + if (input.request.isChunked) EndpointResult.Skipped else { - val rr = input.request.contentLength match { - case None => whenNotPresent - case _ => Rerunnable.fromFuture(decode(input)) - } + val out = + if (input.request.contentLength.isEmpty) whenNotPresent + else Rerunnable.fromFuture(decode(input)) - Some(input -> rr) + EndpointResult.Matched(input, out) } override def item: RequestItem = items.BodyItem diff --git a/core/src/main/scala/io/finch/internal/FromParams.scala b/core/src/main/scala/io/finch/internal/FromParams.scala index de14635bd..60f419897 100644 --- a/core/src/main/scala/io/finch/internal/FromParams.scala +++ b/core/src/main/scala/io/finch/internal/FromParams.scala @@ -3,7 +3,6 @@ package io.finch.internal import scala.reflect.ClassTag import cats.data.NonEmptyList -import io.catbird.util.Rerunnable import io.finch._ import shapeless._ import shapeless.labelled._ @@ -19,9 +18,7 @@ trait FromParams[L <: HList] { object FromParams { implicit val hnilFromParams: FromParams[HNil] = new FromParams[HNil] { - def endpoint: Endpoint[HNil] = Endpoint.embed(items.MultipleItems)(input => - Some(input -> Rerunnable(Output.payload(HNil))) - ) + def endpoint: Endpoint[HNil] = Endpoint.const(HNil) } implicit def hconsFromParams[HK <: Symbol, HV, T <: HList](implicit diff --git a/core/src/main/scala/io/finch/internal/Rs.scala b/core/src/main/scala/io/finch/internal/Rs.scala index f9c67b8df..b50cf686f 100644 --- a/core/src/main/scala/io/finch/internal/Rs.scala +++ b/core/src/main/scala/io/finch/internal/Rs.scala @@ -10,11 +10,14 @@ import shapeless.HNil * Predefined, Finch-specific instances of [[Rerunnable]]. */ private[finch] object Rs { - // See https://github.com/travisbrown/catbird/pull/32 - final def const[A](a: A): Rerunnable[A] = new Rerunnable[A] { - override def run: Future[A] = Future.value(a) + + final def constFuture[A](fa: Future[A]): Rerunnable[A] = new Rerunnable[A] { + override def run: Future[A] = fa } + // See https://github.com/travisbrown/catbird/pull/32 + final def const[A](a: A): Rerunnable[A] = constFuture(Future.value(a)) + final val OutputNone: Rerunnable[Output[Option[Nothing]]] = new Rerunnable[Output[Option[Nothing]]] { override val run: Future[Output[Option[Nothing]]] = diff --git a/core/src/main/scala/io/finch/internal/ToService.scala b/core/src/main/scala/io/finch/internal/ToService.scala index 0db597905..23c18b868 100644 --- a/core/src/main/scala/io/finch/internal/ToService.scala +++ b/core/src/main/scala/io/finch/internal/ToService.scala @@ -3,7 +3,7 @@ package io.finch.internal import com.twitter.finagle.Service import com.twitter.finagle.http.{Request, Response, Status} import com.twitter.util.Future -import io.finch.{Endpoint, Input, Output} +import io.finch.{Endpoint, EndpointResult, Input, Output} /** * Wraps a given [[Endpoint]] with a Finagle [[Service]]. @@ -30,8 +30,8 @@ private[finch] final class ToService[A, CT <: String]( } def apply(req: Request): Future[Response] = underlying(Input.request(req)) match { - case Some(remainderAndOutput) if remainderAndOutput._1.isEmpty => - remainderAndOutput._2.map(oa => copyVersion(req, oa.toResponse(tr, tre))).run + case EndpointResult.Matched(rem, out) if rem.isEmpty => + out.map(oa => copyVersion(req, oa.toResponse(tr, tre))).run case _ => Future.value(Response(req.version, Status.NotFound)) } } diff --git a/core/src/test/scala/io/finch/BasicAuthSpec.scala b/core/src/test/scala/io/finch/BasicAuthSpec.scala index 20947bd93..88e989299 100644 --- a/core/src/test/scala/io/finch/BasicAuthSpec.scala +++ b/core/src/test/scala/io/finch/BasicAuthSpec.scala @@ -27,9 +27,9 @@ class BasicAuthSpec extends FinchSpec { val e = BasicAuth(realm)((u, p) => Future(u == c.user && p == c.pass))(Endpoint.const("foo")) val i = Input.request(req) - e(i).output === Some(Ok("foo")) && { + e(i).awaitOutputUnsafe() === Some(Ok("foo")) && { req.authorization = "secret" - e(i).output === Some(unauthorized(realm)) + e(i).awaitOutputUnsafe() === Some(unauthorized(realm)) } } } @@ -39,11 +39,11 @@ class BasicAuthSpec extends FinchSpec { val protectedInput = Input.get("/a") e(protectedInput).remainder shouldBe Some(protectedInput.drop(1)) - e(protectedInput).output shouldBe Some(unauthorized("realm")) + e(protectedInput).awaitOutputUnsafe() shouldBe Some(unauthorized("realm")) val unprotectedInput = Input.get("/b") e(unprotectedInput).remainder shouldBe Some(unprotectedInput.drop(1)) - e(unprotectedInput).output.map(_.status) shouldBe Some(Status.Ok) + e(unprotectedInput).awaitOutputUnsafe().map(_.status) shouldBe Some(Status.Ok) val notFound = Input.get("/c") e(notFound).remainder shouldBe None // 404 diff --git a/core/src/test/scala/io/finch/BodySpec.scala b/core/src/test/scala/io/finch/BodySpec.scala index 9f1756b34..e5cc5b112 100644 --- a/core/src/test/scala/io/finch/BodySpec.scala +++ b/core/src/test/scala/io/finch/BodySpec.scala @@ -22,30 +22,31 @@ class BodySpec extends FinchSpec { checkAll("Body[UUID]", EntityEndpointLaws[UUID](stringBodyOption)(withBody).evaluating) it should "respond with NotFound when it's required" in { - body[Foo, Text.Plain].apply(Input.get("/")).tryValue === + body[Foo, Text.Plain].apply(Input.get("/")).awaitValue() === Some(Throw(Error.NotPresent(items.BodyItem))) } it should "respond with None when it's optional" in { - body[Foo, Text.Plain].apply(Input.get("/")).tryValue === Some(Return(None)) + body[Foo, Text.Plain].apply(Input.get("/")).awaitValue() === Some(Return(None)) } it should "not match on streaming requests" in { val req = Request() req.setChunked(true) - body[Foo, Text.Plain].apply(Input.request(req)).value === None + body[Foo, Text.Plain].apply(Input.request(req)).awaitValueUnsafe() === None } it should "respond with a value when present and required" in { check { f: Foo => - body[Foo, Text.Plain].apply(Input.post("/").withBody[Text.Plain](f)).value === Some(f) + val i = Input.post("/").withBody[Text.Plain](f) + body[Foo, Text.Plain].apply(i).awaitValueUnsafe() === Some(f) } } it should "respond with Some(value) when it's present and optional" in { check { f: Foo => - bodyOption[Foo, Text.Plain].apply(Input.post("/").withBody[Text.Plain](f)).value === - Some(Some(f)) + val i = Input.post("/").withBody[Text.Plain](f) + bodyOption[Foo, Text.Plain].apply(i).awaitValueUnsafe().flatten === Some(f) } } } diff --git a/core/src/test/scala/io/finch/EndpointSpec.scala b/core/src/test/scala/io/finch/EndpointSpec.scala index 7ae62a4ac..6afce68c7 100644 --- a/core/src/test/scala/io/finch/EndpointSpec.scala +++ b/core/src/test/scala/io/finch/EndpointSpec.scala @@ -3,9 +3,9 @@ package io.finch import java.util.UUID import scala.reflect.ClassTag -import cats.Applicative import cats.data.NonEmptyList import cats.laws.discipline.AlternativeTests +import com.twitter.conversions.time._ import com.twitter.finagle.http.{Cookie, Method, Request} import com.twitter.io.Buf import com.twitter.util.{Future, Throw, Try} @@ -24,7 +24,7 @@ class EndpointSpec extends FinchSpec { val o = e(i) val v = i.headOption.flatMap(s => Try(f(s)).toOption) - o.value === v && (v.isEmpty || o.remainder === Some(i.drop(1))) + o.awaitValueUnsafe() === v && (v.isEmpty || o.remainder === Some(i.drop(1))) } check(extractOne(string, identity)) @@ -49,29 +49,29 @@ class EndpointSpec extends FinchSpec { it should "support very basic map" in { check { i: Input => - string.map(_ * 2)(i).value === i.headOption.map(_ * 2) + string.map(_ * 2)(i).awaitValueUnsafe() === i.headOption.map(_ * 2) } } it should "support transform" in { check { i: Input => val fn = (fs: Future[Output[String]]) => fs.map(_.map(_ * 2)) - string.transform(fn)(i).value === i.headOption.map(_ * 2) + string.transform(fn)(i).awaitValueUnsafe() === i.headOption.map(_ * 2) } } it should "propagate the default (Ok) output" in { check { i: Input => - string(i).output === i.headOption.map(s => Ok(s)) + string(i).awaitOutputUnsafe() === i.headOption.map(s => Ok(s)) } } - it should "propagate the default (Ok) output through its map'd/mapAsync'd/ap'd version" in { + it should "propagate the default (Ok) output through its map'd/mapAsync'd version" in { check { i: Input => val expected = i.headOption.map(s => Ok(s.length)) - string.map(s => s.length)(i).output === expected && - string.mapAsync(s => Future.value(s.length))(i).output === expected && - Applicative[Endpoint].ap(/.map(_ => (_: String).length))(string)(i).output == expected + + string.map(s => s.length)(i).awaitOutputUnsafe() === expected && + string.mapAsync(s => Future.value(s.length))(i).awaitOutputUnsafe() === expected } } @@ -82,7 +82,7 @@ class EndpointSpec extends FinchSpec { .withCookie(new Cookie("C", "D")) check { i: Input => - string.mapOutputAsync(s => Future.value(expected(s.length)))(i).output === + string.mapOutputAsync(s => Future.value(expected(s.length)))(i).awaitOutputUnsafe() === i.headOption.map(s => expected(s.length)) } @@ -92,14 +92,18 @@ class EndpointSpec extends FinchSpec { .foldLeft[Endpoint0](/)((acc, ee) => acc :: ee) val v = (e :: string).mapOutputAsync(s => Future.value(expected(s.length)))(i) - v.output === i.path.lastOption.map(s => expected(s.length)) + v.awaitOutputUnsafe() === i.path.lastOption.map(s => expected(s.length)) } } it should "match one patch segment" in { def matchOne[A](f: String => A)(implicit ev: A => Endpoint0): Input => Boolean = { i: Input => - val v = i.path.headOption.flatMap(s => Try(f(s)).toOption).map(ev).flatMap(e => e(i)) - v.isEmpty || v.remainder === Some(i.drop(1)) + val v = i.path.headOption + .flatMap(s => Try(f(s)).toOption) + .map(ev) + .flatMap(e => e(i).remainder) + + v.isEmpty|| v === Some(i.drop(1)) } check(matchOne(identity)) @@ -152,8 +156,8 @@ class EndpointSpec extends FinchSpec { it should "match the input if one of the endpoints succeed" in { def matchOneOfTwo(f: String => Endpoint0): Input => Boolean = { i: Input => - val v = i.path.headOption.map(f).flatMap(e => e(i)) - v.isEmpty || v.remainder === Some(i.drop(1)) + val v = i.path.headOption.map(f).flatMap(e => e(i).remainder) + v.isEmpty || v === Some(i.drop(1)) } check(matchOneOfTwo(s => (s: Endpoint0) | (s.reverse: Endpoint0))) @@ -185,7 +189,7 @@ class EndpointSpec extends FinchSpec { check { (s: String, i: Int) => (s: Endpoint0).map(_ => i).toString === s } check { (s: String, t: String) => ((s: Endpoint0) | (t: Endpoint0)).toString === s"($s|$t)" } - check { (s: String, t: String) => ((s: Endpoint0) :: (t: Endpoint0)).toString === s"$s/$t" } + check { (s: String, t: String) => ((s: Endpoint0) :: (t: Endpoint0)).toString === s"$s :: $t" } check { s: String => (s: Endpoint0).product[String](*.map(_ => "foo")).toString === s } check { (s: String, t: String) => (s: Endpoint0).mapAsync(_ => Future.value(t)).toString === s } @@ -203,28 +207,31 @@ class EndpointSpec extends FinchSpec { uuids.toString shouldBe ":uuid*" booleans.toString shouldBe ":boolean*" - (int :: string).toString shouldBe ":int/:string" + (int :: string).toString shouldBe ":int :: :string" (boolean :+: long).toString shouldBe "(:boolean|:long)" } it should "always respond with the same output if it's a constant Endpoint" in { check { s: String => - Endpoint.const(s)(Input.get("/")).value === Some(s) && - Endpoint.lift(s)(Input.get("/")).value === Some(s) && - Endpoint.liftFuture(Future.value(s))(Input.get("/")).value === Some(s) + Endpoint.const(s)(Input.get("/")).awaitValueUnsafe() === Some(s) && + Endpoint.lift(s)(Input.get("/")).awaitValueUnsafe() === Some(s) && + Endpoint.liftFuture(Future.value(s))(Input.get("/")).awaitValueUnsafe() === Some(s) } check { o: Output[String] => - Endpoint.liftOutput(o)(Input.get("/")).output === Some(o) && - Endpoint.liftFutureOutput(Future.value(o))(Input.get("/")).output === Some(o) + Endpoint.liftOutput(o)(Input.get("/")).awaitOutputUnsafe() === Some(o) && + Endpoint.liftFutureOutput(Future.value(o))(Input.get("/")).awaitOutputUnsafe() === Some(o) } } it should "support the as[A] method" in { case class Foo(s: String, i: Int, b: Boolean) + val foo = (string :: int :: boolean).as[Foo] + check { (s: String, i: Int, b: Boolean) => - foo(Input(emptyRequest, Seq(s, i.toString, b.toString))).value === Some(Foo(s, i, b)) + foo(Input(emptyRequest, Seq(s, i.toString, b.toString))).awaitValueUnsafe() === + Some(Foo(s, i, b)) } } @@ -238,13 +245,13 @@ class EndpointSpec extends FinchSpec { val i = (s: String) => Input.post("/").withBody[Text.Plain](Buf.Utf8(s)) check { (s: String) => - foo(i(s)).tryValue === Some(Throw( + foo(i(s)).awaitValue() === Some(Throw( Error.NotParsed(BodyItem, implicitly[ClassTag[Foo]], cause) )) } check { (s: String) => - fooOption(i(s)).tryValue === Some(Throw( + fooOption(i(s)).awaitValue() === Some(Throw( Error.NotParsed(BodyItem, implicitly[ClassTag[Foo]], cause) )) } @@ -254,14 +261,14 @@ class EndpointSpec extends FinchSpec { check { (i: Input, s: String, e: Exception) => Endpoint.liftFuture[Unit](Future.exception(e)) .handle({ case _ => Created(s) })(i) - .output === Some(Created(s)) + .awaitOutputUnsafe() === Some(Created(s)) } } it should "not split comma separated param values" in { val i = Input.get("/index", "foo" -> "a,b") val e = params("foo") - e(i).value shouldBe Some(Seq("a,b")) + e(i).awaitValueUnsafe() shouldBe Some(Seq("a,b")) } it should "throw NotPresent if an item is not found" in { @@ -271,7 +278,7 @@ class EndpointSpec extends FinchSpec { param("foo"), header("foo"), cookie("foo").map(_.value), fileUpload("foo").map(_.fileName), paramsNel("foo").map(_.toList.mkString), paramsNel("foor").map(_.toList.mkString), binaryBody.map(new String(_)), stringBody - ).foreach { ii => ii(i).tryValue shouldBe Some(Throw(Error.NotPresent(ii.item))) } + ).foreach { ii => ii(i).awaitValue() shouldBe Some(Throw(Error.NotPresent(ii.item))) } } it should "maps lazily to values" in { @@ -279,8 +286,8 @@ class EndpointSpec extends FinchSpec { var c = 0 val e = * { c = c + 1; Ok(c) } - e(i).value shouldBe Some(1) - e(i).value shouldBe Some(2) + e(i).awaitValueUnsafe() shouldBe Some(1) + e(i).awaitValueUnsafe() shouldBe Some(2) } it should "not evaluate Futures until matched" in { @@ -292,7 +299,7 @@ class EndpointSpec extends FinchSpec { } val e = ("a" :: 10) | endpointWithFailedFuture - e(i).isDefined shouldBe true + e(i).isMatched shouldBe true flag shouldBe false } @@ -324,11 +331,11 @@ class EndpointSpec extends FinchSpec { a.fold[Set[Error]](e => Set(e), es => es.errors.toList.toSet) ++ b.fold[Set[Error]](e => Set(e), es => es.errors.toList.toSet) - val Some(Throw(first)) = lr(Input.get("/")).tryValue - val Some(Throw(second)) = rl(Input.get("/")).tryValue + val Some(Throw(first)) = lr(Input.get("/")).awaitValue() + val Some(Throw(second)) = rl(Input.get("/")).awaitValue() - first.asInstanceOf[Errors].errors.toList.toSet == all && - second.asInstanceOf[Errors].errors.toList.toSet == all + first.asInstanceOf[Errors].errors.toList.toSet === all && + second.asInstanceOf[Errors].errors.toList.toSet === all } } @@ -344,30 +351,35 @@ class EndpointSpec extends FinchSpec { val bbee = bb.product(ee) val eebb = ee.product(bb) - aaee(Input.get("/")).tryValue === Some(Throw(e)) && - eeaa(Input.get("/")).tryValue === Some(Throw(e)) && - bbee(Input.get("/")).tryValue === Some(Throw(e)) && - eebb(Input.get("/")).tryValue === Some(Throw(e)) + aaee(Input.get("/")).awaitValue() === Some(Throw(e)) && + eeaa(Input.get("/")).awaitValue() === Some(Throw(e)) && + bbee(Input.get("/")).awaitValue() === Some(Throw(e)) && + eebb(Input.get("/")).awaitValue() === Some(Throw(e)) } } it should "support the as[A] method on Endpoint[Seq[String]]" in { - val endpoint: Endpoint[Seq[Foo]] = params("testEndpoint").as[Foo] - endpoint(Input.get("/index", "testEndpoint" -> "a")).value shouldBe Some(Seq(Foo("a"))) + val foos = params("testEndpoint").as[Foo] + foos(Input.get("/index", "testEndpoint" -> "a")).awaitValueUnsafe() shouldBe Some(Seq(Foo("a"))) } it should "collect errors on Endpoint[Seq[String]] failure" in { val endpoint: Endpoint[Seq[UUID]] = params("testEndpoint").as[UUID] - an[Errors] shouldBe thrownBy (endpoint(Input.get("/index", "testEndpoint" -> "a")).value) + an[Errors] shouldBe thrownBy ( + endpoint(Input.get("/index", "testEndpoint" -> "a")).awaitValueUnsafe() + ) } it should "support the as[A] method on Endpoint[NonEmptyList[A]]" in { - val endpoint: Endpoint[NonEmptyList[Foo]] = paramsNel("testEndpoint").as[Foo] - endpoint(Input.get("/index", "testEndpoint" -> "a")).value shouldBe Some(NonEmptyList.of(Foo("a"))) + val foos = paramsNel("testEndpoint").as[Foo] + foos(Input.get("/index", "testEndpoint" -> "a")).awaitValueUnsafe() shouldBe + Some(NonEmptyList.of(Foo("a"))) } it should "collect errors on Endpoint[NonEmptyList[String]] failure" in { val endpoint: Endpoint[NonEmptyList[UUID]] = paramsNel("testEndpoint").as[UUID] - an[Errors] shouldBe thrownBy (endpoint(Input.get("/index", "testEndpoint" -> "a")).value) + an[Errors] shouldBe thrownBy ( + endpoint(Input.get("/index", "testEndpoint" -> "a")).awaitValueUnsafe(10.seconds) + ) } } diff --git a/core/src/test/scala/io/finch/EntityEndpointLaws.scala b/core/src/test/scala/io/finch/EntityEndpointLaws.scala index ae3adb277..b77678ca4 100644 --- a/core/src/test/scala/io/finch/EntityEndpointLaws.scala +++ b/core/src/test/scala/io/finch/EntityEndpointLaws.scala @@ -19,7 +19,7 @@ trait EntityEndpointLaws[A] extends Laws with MissingInstances with AllInstances val s = a.toString val i = serialize(s) val e = endpoint.as(decoder, classTag) - e(i).value.flatten <-> Some(a) + e(i).awaitValueUnsafe().flatten <-> Some(a) } def evaluating(implicit A: Arbitrary[A], eq: Eq[A]): RuleSet = diff --git a/core/src/test/scala/io/finch/FinchSpec.scala b/core/src/test/scala/io/finch/FinchSpec.scala index 263e35f3f..9fd4d1efe 100644 --- a/core/src/test/scala/io/finch/FinchSpec.scala +++ b/core/src/test/scala/io/finch/FinchSpec.scala @@ -8,9 +8,8 @@ import cats.data.NonEmptyList import cats.instances.AllInstances import com.twitter.finagle.http._ import com.twitter.io.Buf -import com.twitter.util.{Await, Future, Try} +import com.twitter.util.{Future, Try} import io.catbird.util.Rerunnable -import io.finch.Endpoint.Result import org.scalacheck.{Arbitrary, Cogen, Gen} import org.scalatest.{FlatSpec, Matchers} import org.scalatest.prop.Checkers @@ -177,12 +176,9 @@ trait FinchSpec extends FlatSpec with Matchers with Checkers with AllInstances Gen.oneOf( Gen.const(Endpoint.empty[A]), A.arbitrary.map(a => Endpoint.const(a)), - Arbitrary.arbitrary[Throwable].map { error => - new Endpoint[A] { - override def apply(input: Input): Result[A] = - Some(input -> Rerunnable.fromFuture(Future.exception(error))) - } - }, + Arbitrary.arbitrary[Throwable].map(e => + Endpoint.liftFutureOutput(Future.exception[Output[A]](e)) + ), /** * Note that we don't provide instances of arbitrary endpoints wrapping * `Input => Output[A]` since `Endpoint` isn't actually lawful in this @@ -190,8 +186,8 @@ trait FinchSpec extends FlatSpec with Matchers with Checkers with AllInstances */ Arbitrary.arbitrary[Input => A].map { f => new Endpoint[A] { - override def apply(input: Input): Result[A] = - Some(input -> Rerunnable.fromFuture(Future.value(Ok(f(input))))) + final def apply(input: Input): Endpoint.Result[A] = + EndpointResult.Matched(input, Rerunnable(Output.payload(f(input)))) } } ) @@ -206,10 +202,10 @@ trait FinchSpec extends FlatSpec with Matchers with Checkers with AllInstances implicit def eqEndpoint[A: Eq]: Eq[Endpoint[A]] = new Eq[Endpoint[A]] { private[this] def count: Int = 16 - private[this] def await(result: Endpoint.Result[A]): Option[(Input, Try[Output[A]])] = - result.map { - case (input, rerun) => (input, Await.result(rerun.liftToTry.run)) - } + private[this] def await(result: Endpoint.Result[A]): Option[(Input, Try[Output[A]])] = for { + r <- result.remainder + o <- result.awaitOutput() + } yield (r, o) private[this] def inputs: Stream[Input] = Stream.continually( Arbitrary.arbitrary[Input].sample diff --git a/examples/src/test/scala/io/finch/div/DivSpec.scala b/examples/src/test/scala/io/finch/div/DivSpec.scala index 5a4a67405..c58ff4505 100644 --- a/examples/src/test/scala/io/finch/div/DivSpec.scala +++ b/examples/src/test/scala/io/finch/div/DivSpec.scala @@ -9,12 +9,12 @@ class DivSpec extends FlatSpec with Matchers { import Main.div it should "work if the request is a put and the divisor is not 0" in { - div(Input.post("/20/10")).value shouldBe Some(2) + div(Input.post("/20/10")).awaitValueUnsafe() shouldBe Some(2) } it should "give back bad request if we divide by 0" in { - div(Input.post("/20/0")).output.map(_.status) shouldBe Some(Status.BadRequest) + div(Input.post("/20/0")).awaitOutputUnsafe().map(_.status) shouldBe Some(Status.BadRequest) } it should "give back nothing for other verbs" in { - div(Input.get("/20/10")).value shouldBe None + div(Input.get("/20/10")).awaitValueUnsafe() shouldBe None } } diff --git a/examples/src/test/scala/io/finch/eval/EvalSpec.scala b/examples/src/test/scala/io/finch/eval/EvalSpec.scala index 6abbd08cb..e1be38393 100644 --- a/examples/src/test/scala/io/finch/eval/EvalSpec.scala +++ b/examples/src/test/scala/io/finch/eval/EvalSpec.scala @@ -12,20 +12,26 @@ class EvalSpec extends FlatSpec with Matchers { import Main._ it should "properly evaluate a well-formed expression" in { - val result = eval(Input.post("/eval") - .withBody[Application.Json](EvalInput("10 + 10"), Some(StandardCharsets.UTF_8))).value + val result = eval( + Input.post("/eval") + .withBody[Application.Json](EvalInput("10 + 10"), Some(StandardCharsets.UTF_8)) + ).awaitValueUnsafe() result shouldBe Some(EvalOutput("20")) } it should "give back bad request if the expression isn't parseable" in { - val output = eval(Input.post("/eval") - .withBody[Application.Json](EvalInput("s = 12"), Some(StandardCharsets.UTF_8))).output + val output = eval( + Input.post("/eval") + .withBody[Application.Json](EvalInput("s = 12"), Some(StandardCharsets.UTF_8)) + ).awaitOutputUnsafe() output.map(_.status) shouldBe Some(Status.BadRequest) } it should "give back nothing for other verbs" in { - val result = eval(Input.get("/eval") - .withBody[Application.Json](EvalInput("10 + 10"), Some(StandardCharsets.UTF_8))).value + val result = eval( + Input.get("/eval") + .withBody[Application.Json](EvalInput("10 + 10"), Some(StandardCharsets.UTF_8)) + ).awaitValueUnsafe() result shouldBe None } diff --git a/examples/src/test/scala/io/finch/oauth2/OAuth2Spec.scala b/examples/src/test/scala/io/finch/oauth2/OAuth2Spec.scala index 69d7d07e1..5d6928266 100644 --- a/examples/src/test/scala/io/finch/oauth2/OAuth2Spec.scala +++ b/examples/src/test/scala/io/finch/oauth2/OAuth2Spec.scala @@ -15,62 +15,82 @@ class OAuth2Spec extends FlatSpec with Matchers { "username" -> "user_name", "password" -> "user_password", "client_id" -> "user_id") - tokens(input).value.map(_.tokenType) shouldBe Some("Bearer") + + tokens(input).awaitValueUnsafe().map(_.tokenType) shouldBe Some("Bearer") } + it should "give an access token with the client credentials grant type" in { val input = Input.post("/users/auth") .withForm("grant_type" -> "client_credentials") .withHeaders("Authorization" -> "Basic dXNlcl9pZDp1c2VyX3NlY3JldA==") - tokens(input).value.map(_.tokenType) shouldBe Some("Bearer") + + tokens(input).awaitValueUnsafe().map(_.tokenType) shouldBe Some("Bearer") } + it should "give an access token with the auth code grant type" in { val input = Input.post("/users/auth") .withForm( "grant_type" -> "authorization_code", "code" -> "user_auth_code", "client_id" -> "user_id") - tokens(input).value.map(_.tokenType) shouldBe Some("Bearer") + + tokens(input).awaitValueUnsafe().map(_.tokenType) shouldBe Some("Bearer") } + it should "give back bad request if we omit the password for the password grant type" in { val input = Input.post("/users/auth") .withForm( "grant_type" -> "password", "username" -> "user_name", "client_id" -> "user_id") - tokens(input).output.map(_.status) shouldBe Some(Status.BadRequest) + + tokens(input).awaitOutputUnsafe().map(_.status) shouldBe Some(Status.BadRequest) } + it should "give back nothing for other verbs" in { val input = Input.get("/users/auth") .withForm("grant_type" -> "authorization_code", "code" -> "code", "client_id" -> "id") - tokens(input).value shouldBe None + + tokens(input).awaitValueUnsafe() shouldBe None } behavior of "the authorized endpoint" + it should "work if the access token is a valid one" in { val input = Input.post("/users/auth") .withForm("grant_type" -> "client_credentials") .withHeaders("Authorization" -> "Basic dXNlcl9pZDp1c2VyX3NlY3JldA==") - val authdUser = tokens(input).value - .map(_.accessToken) - .flatMap(t => users(Input.get("/users/current").withForm("access_token" -> t)).value) + + val authdUser = tokens(input).awaitValueUnsafe() + .map(_.accessToken).flatMap(t => + users(Input.get("/users/current").withForm("access_token" -> t)).awaitValueUnsafe() + ) + authdUser shouldBe Some(OAuthUser("user", "John Smith")) } + it should "be unauthorized when using an invalid access token" in { val input = Input.get("/users/current") .withForm("access_token" -> "at-5b0e7e3b-943f-479f-beab-7814814d0315") - users(input).output.map(_.status) shouldBe Some(Status.Unauthorized) + + users(input).awaitOutputUnsafe().map(_.status) shouldBe Some(Status.Unauthorized) } + it should "give back nothing for other verbs" in { val input = Input.post("/users/current") .withForm("access_token" -> "at-5b0e7e3b-943f-479f-beab-7814814d0315") - users(input).value shouldBe None + + users(input).awaitValueUnsafe() shouldBe None } behavior of "the unprotected users endpoint" + it should "give back the unprotected user" in { - unprotected(Input.get("/users/unprotected")).value shouldBe Some(UnprotectedUser("unprotected")) + unprotected(Input.get("/users/unprotected")).awaitValueUnsafe() shouldBe + Some(UnprotectedUser("unprotected")) } + it should "give back nothing for other verbs" in { - unprotected(Input.post("/users/unprotected")).value shouldBe None + unprotected(Input.post("/users/unprotected")).awaitValueUnsafe() shouldBe None } } diff --git a/examples/src/test/scala/io/finch/streaming/StreamingSpec.scala b/examples/src/test/scala/io/finch/streaming/StreamingSpec.scala index 9d7eff058..3b7d19e45 100644 --- a/examples/src/test/scala/io/finch/streaming/StreamingSpec.scala +++ b/examples/src/test/scala/io/finch/streaming/StreamingSpec.scala @@ -8,27 +8,34 @@ class StreamingSpec extends FlatSpec with Matchers { import Main._ behavior of "the sumTo endpoint" + it should "give back a streaming sum" in { - sumTo(Input.post("/sumTo/3")).value.map(s => Await.result(s.toSeq())) shouldBe + sumTo(Input.post("/sumTo/3")).awaitValueUnsafe().map(s => Await.result(s.toSeq())) shouldBe Some(Seq(1L, 3L, 6L)) } + it should "give back an empty stream if param <= 0" in { - sumTo(Input.post("/sumTo/-3")).value.map(s => Await.result(s.toSeq())) shouldBe Some(Seq.empty) + sumTo(Input.post("/sumTo/-3")).awaitValueUnsafe().map(s => Await.result(s.toSeq())) shouldBe + Some(Seq.empty) } + it should "give back nothing for other verbs" in { - sumTo(Input.get("/sumTo/3")).value shouldBe None + sumTo(Input.get("/sumTo/3")).awaitValueUnsafe() shouldBe None } behavior of "the examples endpoint" + it should "give back a stream of examples" in { - examples(Input.get("/examples/3")).value.map(s => Await.result(s.toSeq())) shouldBe + examples(Input.get("/examples/3")).awaitValueUnsafe().map(s => Await.result(s.toSeq())) shouldBe Some(Seq(Example(0), Example(1), Example(2))) } + it should "give back an empty stream if param <= 0" in { - examples(Input.get("/examples/-3")).value.map(s => Await.result(s.toSeq())) shouldBe + examples(Input.get("/examples/-3")).awaitValueUnsafe().map(s => Await.result(s.toSeq())) shouldBe Some(Seq.empty) } + it should "give back nothing for other verbs" in { - examples(Input.post("/examples/3")).value shouldBe None + examples(Input.post("/examples/3")).awaitValueUnsafe() shouldBe None } } diff --git a/examples/src/test/scala/io/finch/todo/TodoSpec.scala b/examples/src/test/scala/io/finch/todo/TodoSpec.scala index 73a08b250..632b0ffd3 100644 --- a/examples/src/test/scala/io/finch/todo/TodoSpec.scala +++ b/examples/src/test/scala/io/finch/todo/TodoSpec.scala @@ -32,9 +32,9 @@ class TodoSpec extends FlatSpec with Matchers with Checkers { .withBody[Application.Json](todoWithoutId, Some(StandardCharsets.UTF_8)) val res = postTodo(input) - res.output.map(_.status) === Some(Status.Created) - res.value.isDefined === true - val Some(todo) = res.value + res.awaitOutputUnsafe().map(_.status) === Some(Status.Created) + res.awaitValueUnsafe().isDefined === true + val Some(todo) = res.awaitValueUnsafe() todo.completed === todoWithoutId.completed todo.title === todoWithoutId.title todo.order === todoWithoutId.order @@ -49,7 +49,7 @@ class TodoSpec extends FlatSpec with Matchers with Checkers { val input = Input.patch(s"/todos/${todo.id}") .withBody[Application.Json](Buf.Utf8("{\"completed\": true}"), Some(StandardCharsets.UTF_8)) - patchTodo(input).value shouldBe Some(todo.copy(completed = true)) + patchTodo(input).awaitValueUnsafe() shouldBe Some(todo.copy(completed = true)) Todo.get(todo.id) shouldBe Some(todo.copy(completed = true)) } it should "throw an exception if the uuid hasn't been found" in { @@ -59,7 +59,7 @@ class TodoSpec extends FlatSpec with Matchers with Checkers { val input = Input.patch(s"/todos/$id") .withBody[Application.Json](Buf.Utf8("{\"completed\": true}"), Some(StandardCharsets.UTF_8)) - a[TodoNotFound] shouldBe thrownBy(patchTodo(input).value) + a[TodoNotFound] shouldBe thrownBy(patchTodo(input).awaitValueUnsafe()) } it should "give back the same todo with non-related json" in { @@ -67,14 +67,14 @@ class TodoSpec extends FlatSpec with Matchers with Checkers { val input = Input.patch(s"/todos/${todo.id}") .withBody[Application.Json](Buf.Utf8("{\"bla\": true}"), Some(StandardCharsets.UTF_8)) - patchTodo(input).value shouldBe Some(todo) + patchTodo(input).awaitValueUnsafe() shouldBe Some(todo) Todo.get(todo.id) shouldBe Some(todo) } behavior of "the getTodos endpoint" it should "retrieve all available todos" in { - getTodos(Input.get("/todos")).value shouldBe Some(Todo.list()) + getTodos(Input.get("/todos")).awaitValueUnsafe() shouldBe Some(Todo.list()) } behavior of "the deleteTodo endpoint" @@ -82,21 +82,21 @@ class TodoSpec extends FlatSpec with Matchers with Checkers { it should "delete the specified todo" in { val todo = createTodo() - deleteTodo(Input.delete(s"/todos/${todo.id}")).value shouldBe Some(todo) + deleteTodo(Input.delete(s"/todos/${todo.id}")).awaitValueUnsafe() shouldBe Some(todo) Todo.get(todo.id) shouldBe None } it should "throw an exception if the uuid hasn't been found" in { val id = UUID.randomUUID() Todo.get(id) shouldBe None - a[TodoNotFound] shouldBe thrownBy(deleteTodo(Input.delete(s"/todos/$id")).value) + a[TodoNotFound] shouldBe thrownBy(deleteTodo(Input.delete(s"/todos/$id")).awaitValueUnsafe()) } behavior of "the deleteTodos endpoint" it should "delete all todos" in { val todos = Todo.list() - deleteTodos(Input.delete("/todos")).value shouldBe Some(todos) + deleteTodos(Input.delete("/todos")).awaitValueUnsafe() shouldBe Some(todos) todos.foreach(t => Todo.get(t.id) shouldBe None) } diff --git a/oauth2/src/main/scala/io/finch/oauth2/package.scala b/oauth2/src/main/scala/io/finch/oauth2/package.scala index c47523fed..1f4512f6a 100644 --- a/oauth2/src/main/scala/io/finch/oauth2/package.scala +++ b/oauth2/src/main/scala/io/finch/oauth2/package.scala @@ -3,6 +3,7 @@ package io.finch import com.twitter.finagle.OAuth2 import com.twitter.finagle.http.Status import com.twitter.finagle.oauth2.{AuthInfo, DataHandler, GrantHandlerResult, OAuthError} +import com.twitter.util.Future import io.catbird.util.Rerunnable package object oauth2 { @@ -21,20 +22,30 @@ package object oauth2 { * given `dataHandler`. */ def authorize[U](dataHandler: DataHandler[U]): Endpoint[AuthInfo[U]] = - Endpoint.embed(items.MultipleItems)(i => - Some(i -> new Rerunnable[Output[AuthInfo[U]]] { - override def run = OAuth2.authorize(i.request, dataHandler).map(Output.payload(_)) - }) - ).handle(handleOAuthError) + new Endpoint[AuthInfo[U]] { + private[this] final def aux(i: Input): Future[Output[AuthInfo[U]]] = + OAuth2 + .authorize(i.request, dataHandler) + .map(ai => Output.payload(ai)) + .handle(handleOAuthError) + + final def apply(input: Input): Endpoint.Result[AuthInfo[U]] = + EndpointResult.Matched(input, Rerunnable.fromFuture(aux(input))) + } /** * An [[Endpoint]] that takes a request (with user credentials) and issues an access token for it * with respect to a given `dataHandler`. */ def issueAccessToken[U](dataHandler: DataHandler[U]): Endpoint[GrantHandlerResult] = - Endpoint.embed(items.MultipleItems)(i => - Some(i -> new Rerunnable[Output[GrantHandlerResult]] { - override def run = OAuth2.issueAccessToken(i.request, dataHandler).map(Output.payload(_)) - }) - ).handle(handleOAuthError) + new Endpoint[GrantHandlerResult] { + private[this] final def aux(i: Input): Future[Output[GrantHandlerResult]] = + OAuth2 + .issueAccessToken(i.request, dataHandler) + .map(ghr => Output.payload(ghr)) + .handle(handleOAuthError) + + final def apply(input: Input): Endpoint.Result[GrantHandlerResult] = + EndpointResult.Matched(input, Rerunnable.fromFuture(aux(input))) + } } diff --git a/oauth2/src/test/scala/io/finch/oauth2/OAuth2Spec.scala b/oauth2/src/test/scala/io/finch/oauth2/OAuth2Spec.scala index 72e7b806d..e55040a32 100644 --- a/oauth2/src/test/scala/io/finch/oauth2/OAuth2Spec.scala +++ b/oauth2/src/test/scala/io/finch/oauth2/OAuth2Spec.scala @@ -31,8 +31,8 @@ class OAuth2Spec extends FlatSpec with Matchers with Checkers with MockitoSugar val i1 = Input.get("/user", "access_token" -> "bar") val i2 = Input.get("/user") - e(i1).output shouldBe Some(Ok(42)) - val Some(error) = e(i2).output + e(i1).awaitOutputUnsafe() shouldBe Some(Ok(42)) + val Some(error) = e(i2).awaitOutputUnsafe() error.status shouldBe Status.BadRequest error.headers should contain key "WWW-Authenticate" } @@ -58,8 +58,8 @@ class OAuth2Spec extends FlatSpec with Matchers with Checkers with MockitoSugar val i2 = Input.get("/token") - e(i1).output shouldBe Some(Ok("foobar")) - val Some(error) = e(i2).output + e(i1).awaitOutputUnsafe() shouldBe Some(Ok("foobar")) + val Some(error) = e(i2).awaitOutputUnsafe() error.status shouldBe Status.BadRequest error.headers should contain key "WWW-Authenticate" }