From e6793fbf6b0df9df401bf6491fa5b32ca5515eb7 Mon Sep 17 00:00:00 2001 From: Vladimir Kostyukov Date: Wed, 25 Feb 2015 21:32:16 -0800 Subject: [PATCH] Finch in Action --- build.sbt | 2 +- .../main/scala/io/finch/micro/package.scala | 64 +++ core/src/main/scala/io/finch/package.scala | 6 +- .../LowPriorityRequestReaderImplicits.scala | 37 ++ .../io/finch/request/RequestReader.scala | 72 ++- .../main/scala/io/finch/request/package.scala | 451 ++++++++++-------- .../route/LowPriorityRouterImplicits.scala | 12 + .../main/scala/io/finch/route/Router.scala | 13 - .../main/scala/io/finch/route/package.scala | 43 +- .../request/RequestReaderCompanionSpec.scala | 2 +- .../scala/io/finch/route/RouterSpec.scala | 2 +- .../main/scala/io/finch/playgorund/Main.scala | 83 ---- .../main/scala/io/finch/playground/Main.scala | 64 +++ 13 files changed, 494 insertions(+), 357 deletions(-) create mode 100644 core/src/main/scala/io/finch/micro/package.scala create mode 100644 core/src/main/scala/io/finch/request/LowPriorityRequestReaderImplicits.scala create mode 100644 core/src/main/scala/io/finch/route/LowPriorityRouterImplicits.scala delete mode 100644 playground/src/main/scala/io/finch/playgorund/Main.scala create mode 100644 playground/src/main/scala/io/finch/playground/Main.scala diff --git a/build.sbt b/build.sbt index 0907e9d74..6b89bcee4 100644 --- a/build.sbt +++ b/build.sbt @@ -99,7 +99,7 @@ lazy val demo = project lazy val playground = project .settings(moduleName := "finch-playground") .settings(allSettings: _*) - .dependsOn(core) + .dependsOn(core, jackson) .disablePlugins(CoverallsPlugin) lazy val jawn = project diff --git a/core/src/main/scala/io/finch/micro/package.scala b/core/src/main/scala/io/finch/micro/package.scala new file mode 100644 index 000000000..0afc96739 --- /dev/null +++ b/core/src/main/scala/io/finch/micro/package.scala @@ -0,0 +1,64 @@ +package io.finch + +import com.twitter.finagle.Service +import com.twitter.util.Future + +import io.finch.route.{Endpoint => _, _} +import io.finch.response._ +import io.finch.request._ + +/** + * An experimental package that enables `micro`-services support in Finch. + */ +package object micro { + + /** + * An alias for polymorphic [[PRequestReader]]. + */ + type PMicro[R, A] = PRequestReader[R, A] + + /** + * A [[PMicro]] with request type fixed to [[HttpRequest]]. + */ + type Micro[A] = PMicro[HttpRequest, A] + + /** + * A companion object for `Micro`. + */ + val Micro = RequestReader + + /** + * A [[Router]] that fetches a [[PMicro]] is called an endpoint. + */ + type PEndpoint[R] = Router[PMicro[R, HttpResponse]] + + /** + * A [[PEndpoint]] with request type fixed to [[HttpRequest]]. + */ + type Endpoint = PEndpoint[HttpRequest] + + implicit class MicroRouterOps[R, A](r: Router[PMicro[R, A]]) { + def |[B](that: Router[PMicro[R, B]])(implicit eA: EncodeResponse[A], eB: EncodeResponse[B]): PEndpoint[R] = + r.map(_.map(Ok(_))) orElse that.map(_.map(Ok(_))) + } + + implicit def microToHttpMicro[R, A](m: PMicro[R, A])( + implicit e: EncodeResponse[A] + ): PMicro[R, HttpResponse] = m.map(Ok(_)) + + implicit def microRouterToEndpoint[R, M](r: Router[M])( + implicit ev: M => PMicro[R, HttpResponse] + ): PEndpoint[R] = r.map(ev) + + implicit def endpointToFinagleService[M, R](r: Router[M])( + implicit evM: M => PMicro[R, HttpResponse], evR: R %> HttpRequest + ): Service[R, HttpResponse] = new Service[R, HttpResponse] { + def apply(req: R): Future[HttpResponse] = { + val httpReq = evR(req) + r.map(evM)(requestToRoute(httpReq)) match { + case Some((Nil, micro)) => micro(req) + case _ => RouteNotFound(s"${httpReq.method.toString.toUpperCase} ${httpReq.path}").toFutureException + } + } + } +} diff --git a/core/src/main/scala/io/finch/package.scala b/core/src/main/scala/io/finch/package.scala index 9086ddf4d..b5ae0e9d4 100644 --- a/core/src/main/scala/io/finch/package.scala +++ b/core/src/main/scala/io/finch/package.scala @@ -23,12 +23,12 @@ package io import com.twitter.finagle.httpx.path.Path - -import scala.language.implicitConversions +import io.finch.response.{Ok, EncodeResponse} import com.twitter.finagle.httpx import com.twitter.util.Future -import com.twitter.finagle.{Filter, Service} +import com.twitter.finagle.Service +import com.twitter.finagle.Filter /** * This is a root package of the Finch library, which provides an immutable layer of functions and types atop of Finagle diff --git a/core/src/main/scala/io/finch/request/LowPriorityRequestReaderImplicits.scala b/core/src/main/scala/io/finch/request/LowPriorityRequestReaderImplicits.scala new file mode 100644 index 000000000..e9a1f069d --- /dev/null +++ b/core/src/main/scala/io/finch/request/LowPriorityRequestReaderImplicits.scala @@ -0,0 +1,37 @@ +package io.finch.request + +import com.twitter.util.{Future, Try} + +import scala.reflect.ClassTag + +/** + * Trait with low-priority implicits to avoid conflicts that would arise from adding implicits that would work with + * any type in the same scope as implicits for concrete types. + * + * Implicits defined in super-types have lower priority than those defined in a sub-type. Therefore we define low- + * priority implicits here and mix this trait into the package object. + */ +trait LowPriorityRequestReaderImplicits { + + /** + * Creates a [[io.finch.request.DecodeMagnet DecodeMagnet]] from + * [[io.finch.request.DecodeAnyRequest DecodeAnyRequest]]. + */ + implicit def magnetFromAnyDecode[A](implicit d: DecodeAnyRequest, tag: ClassTag[A]): DecodeMagnet[A] = + new DecodeMagnet[A] { + def apply(): DecodeRequest[A] = new DecodeRequest[A] { + def apply(req: String): Try[A] = d(req)(tag) + } + } + + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of one argument. + */ + implicit class RrArrow1[R, A](rr: PRequestReader[R, A]) { + def ~~>[B](fn: A => Future[B]): PRequestReader[R, B] = + rr.embedFlatMap(fn) + + def ~>[B](fn: A => B): PRequestReader[R, B] = + rr.map(fn) + } +} diff --git a/core/src/main/scala/io/finch/request/RequestReader.scala b/core/src/main/scala/io/finch/request/RequestReader.scala index 34996bc39..907ea931b 100644 --- a/core/src/main/scala/io/finch/request/RequestReader.scala +++ b/core/src/main/scala/io/finch/request/RequestReader.scala @@ -31,9 +31,9 @@ import io.finch._ import io.finch.request.items._ /** - * A request reader (a reader monad) that reads a [[com.twitter.util.Future Future]] of `A` from the HTTP request. + * A polymorphic request reader (a reader monad) that reads a [[Future]] of `A` from the request of type `R`. */ -trait RequestReader[A] { self => +trait PRequestReader[R, A] { self => /** * A [[io.finch.request.items.RequestItem RequestItem]] read by this request reader. @@ -43,50 +43,48 @@ trait RequestReader[A] { self => /** * Reads the data from given request `req`. * - * @tparam Req the request type * @param req the request to read */ - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[A] + def apply(req: R): Future[A] /** * Flat-maps this request reader to the given function `A => RequestReader[B]`. */ - def flatMap[B](fn: A => RequestReader[B]): RequestReader[B] = new RequestReader[B] { + def flatMap[B](fn: A => PRequestReader[R, B]): PRequestReader[R, B] = new PRequestReader[R, B] { val item = MultipleItems - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[B] = self(req) flatMap { fn(_)(req) } + def apply(req: R): Future[B] = self(req) flatMap { fn(_)(req) } } /** * Maps this request reader to the given function `A => B`. */ - def map[B](fn: A => B): RequestReader[B] = new RequestReader[B] { + def map[B](fn: A => B): PRequestReader[R, B] = new PRequestReader[R, B] { val item = self.item - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[B] = self(req) map fn + def apply(req: R): Future[B] = self(req) map fn } /** * Flat-maps this request reader to the given function `A => Future[B]`. */ - def embedFlatMap[B](fn: A => Future[B]): RequestReader[B] = new RequestReader[B] { + def embedFlatMap[B](fn: A => Future[B]): PRequestReader[R, B] = new PRequestReader[R, B] { val item = self.item - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[B] = self(req) flatMap fn + def apply(req: R): Future[B] = self(req) flatMap fn } /** * Composes this request reader with the given `that` request reader. */ - def ~[B](that: RequestReader[B]): RequestReader[A ~ B] = new RequestReader[A ~ B] { + def ~[S, B](magnet: ApplicativeMagnet[R, A, S, B]): PRequestReader[S, A ~ B] = new PRequestReader[S, A ~ B] { val item = MultipleItems - def apply[Req] (req: Req)(implicit ev: Req => HttpRequest): Future[A ~ B] = - Future.join(self(req)(ev).liftToTry, that(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 apply(req: S): Future[A ~ B] = magnet(req, self).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): RequestErrors = { - def collect (e: Throwable): Seq[Throwable] = e match { + def collectExceptions(a: Throwable, b: Throwable): RequestErrors = { + def collect(e: Throwable): Seq[Throwable] = e match { case RequestErrors(errors) => errors case other => Seq(other) } @@ -98,7 +96,7 @@ trait RequestReader[A] { self => /** * Applies the given filter `p` to this request reader. */ - def withFilter(p: A => Boolean): RequestReader[A] = self.should("not fail validation")(p) + def withFilter(p: A => Boolean): PRequestReader[R, A] = self.should("not fail validation")(p) /** * Validates the result of this request reader using a `predicate`. The rule is used for error reporting. @@ -109,7 +107,7 @@ trait RequestReader[A] { self => * @return a request reader that will return the value of this reader if it is valid. * Otherwise the future fails with a [[io.finch.request.NotValid NotValid]] error. */ - def should(rule: String)(predicate: A => Boolean): RequestReader[A] = embedFlatMap { a => + def should(rule: String)(predicate: A => Boolean): PRequestReader[R, A] = embedFlatMap { a => if (predicate(a)) a.toFuture else NotValid(self.item, "should " + rule).toFutureException } @@ -123,7 +121,7 @@ trait RequestReader[A] { self => * @return a request reader that will return the value of this reader if it is valid. * Otherwise the future fails with a [[io.finch.request.NotValid NotValid]] error. */ - def shouldNot(rule: String)(predicate: A => Boolean): RequestReader[A] = should(s"not $rule.")(x => !predicate(x)) + def shouldNot(rule: String)(predicate: A => Boolean): PRequestReader[R, A] = should(s"not $rule.")(x => !predicate(x)) /** * Validates the result of this request reader using a predefined `rule`. This method allows for rules to be reused @@ -135,7 +133,7 @@ trait RequestReader[A] { self => * @return a request reader that will return the value of this reader if it is valid. * Otherwise the future fails with a [[io.finch.request.NotValid NotValid]] error. */ - def should(rule: ValidationRule[A]): RequestReader[A] = should(rule.description)(rule.apply) + def should(rule: ValidationRule[A]): PRequestReader[R, A] = should(rule.description)(rule.apply) /** * Validates the result of this request reader using a predefined `rule`. This method allows for rules to be reused @@ -147,7 +145,7 @@ trait RequestReader[A] { self => * @return a request reader that will return the value of this reader if it is valid. * Otherwise the future fails with a [[io.finch.request.NotValid NotValid]] error. */ - def shouldNot(rule: ValidationRule[A]): RequestReader[A] = shouldNot(rule.description)(rule.apply) + def shouldNot(rule: ValidationRule[A]): PRequestReader[R, A] = shouldNot(rule.description)(rule.apply) } /** @@ -159,47 +157,39 @@ object RequestReader { * Creates a new [[io.finch.request.RequestReader RequestReader]] that always succeeds, producing the specified value. * * @param value the value the new reader should produce - * @param item the request item (e.g. parameter, header) the value is associated with * @return a new reader that always succeeds, producing the specified value */ - def value[A](value: A, item: RequestItem = MultipleItems): RequestReader[A] = - const[A](value.toFuture) + def value[A](value: A): RequestReader[A] = const[A](value.toFuture) /** * Creates a new [[io.finch.request.RequestReader RequestReader]] that always fails, producing the specified * exception. * * @param exc the exception the new reader should produce - * @param item the request item (e.g. parameter, header) the value is associated with * @return a new reader that always fails, producing the specified exception */ - def exception[A](exc: Throwable, item: RequestItem = MultipleItems): RequestReader[A] = - const[A](exc.toFutureException) + def exception[A](exc: Throwable): RequestReader[A] = const[A](exc.toFutureException) /** * Creates a new [[io.finch.request.RequestReader RequestReader]] that always produces the specified value. It will * succeed if the given `Future` succeeds and fail if the `Future` fails. * * @param value the value the new reader should produce - * @param item the request item (e.g. parameter, header) the value is associated with * @return a new reader that always produces the specified value */ - def const[A](value: Future[A], item: RequestItem = MultipleItems): RequestReader[A] = - embed[A](item)(_ => value) + def const[A](value: Future[A]): RequestReader[A] = embed[HttpRequest, A](MultipleItems)(_ => value) /** * Creates a new [[io.finch.request.RequestReader RequestReader]] that reads the result from the request. * - * @param item the request item (e.g. parameter, header) the value is associated with * @param f the function to apply to the request * @return a new reader that reads the result from the request */ - def apply[A](item: RequestItem)(f: HttpRequest => A): RequestReader[A] = - embed[A](item)(f(_).toFuture) + def apply[R, A](f: R => A): PRequestReader[R, A] = embed[R, A](MultipleItems)(f(_).toFuture) - private[this] def embed[A](reqItem: RequestItem)(f: HttpRequest => Future[A]): RequestReader[A] = - new RequestReader[A] { - val item = reqItem - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[A] = f(req) + private[request] def embed[R, A](i: RequestItem)(f: R => Future[A]): PRequestReader[R, A] = + new PRequestReader[R, A] { + val item = i + def apply(req: R): Future[A] = f(req) } } diff --git a/core/src/main/scala/io/finch/request/package.scala b/core/src/main/scala/io/finch/request/package.scala index 988c94814..6a7e7a1db 100644 --- a/core/src/main/scala/io/finch/request/package.scala +++ b/core/src/main/scala/io/finch/request/package.scala @@ -32,33 +32,10 @@ import com.twitter.util.{Future, Throw, Try} import org.jboss.netty.handler.codec.http.multipart.{HttpPostRequestDecoder, Attribute} +import scala.annotation.implicitNotFound import scala.collection.JavaConverters._ import scala.reflect.ClassTag -package request { - - /** - * Trait with low-priority implicits to avoid conflicts that would arise from adding implicits that would work with - * any type in the same scope as implicits for concrete types. - * - * Implicits defined in super-types have lower priority than those defined in a sub-type. Therefore we define low- - * priority implicits here and mix this trait into the package object. - */ - trait LowPriorityImplicits { - - /** - * Creates a [[io.finch.request.DecodeMagnet DecodeMagnet]] from - * [[io.finch.request.DecodeAnyRequest DecodeAnyRequest]]. - */ - implicit def magnetFromAnyDecode[A](implicit d: DecodeAnyRequest, tag: ClassTag[A]): DecodeMagnet[A] = - new DecodeMagnet[A] { - def apply(): DecodeRequest[A] = new DecodeRequest[A] { - def apply(req: String): Try[A] = d(req)(tag) - } - } - } -} - /** * This package introduces types and functions that enable _request processing_ in Finch. The [[io.finch.request]] * primitives allow both to _read_ the various request items (''query string param'', ''header'' and ''cookie'') using @@ -93,7 +70,7 @@ package request { * } * }}} */ -package object request extends LowPriorityImplicits { +package object request extends LowPriorityRequestReaderImplicits { /** * A type alias for a [[org.jboss.netty.handler.codec.http.multipart.FileUpload]] @@ -101,6 +78,35 @@ package object request extends LowPriorityImplicits { */ type FileUpload = org.jboss.netty.handler.codec.http.multipart.FileUpload + /** + * A sane and safe approach to implicit view `A => B`. + */ + @implicitNotFound("Can not view ${A} as ${B}. You must define an implicit value of type View[${A}, ${B}].") + trait View[A, B] { + def apply(x: A): B + } + + /** + * A companion object for [[View]]. + */ + object View { + def apply[A, B](f: A => B): View[A, B] = new View[A, B] { + def apply(x: A) = f(x) + } + + implicit def identityView[A]: View[A, A] = View(x => x) + } + + /** + * A symbolic alias for [[View]]. + */ + type %>[A, B] = View[A, B] + + /** + * A [[PRequestReader]] with request type fixed to [[HttpRequest]]. + */ + type RequestReader[A] = PRequestReader[HttpRequest, A] + /** * A [[io.finch.request.DecodeRequest DecodeRequest]] instance for `Int`. */ @@ -142,8 +148,8 @@ package object request extends LowPriorityImplicits { import items._ - private[this] def notParsed[A](reader: RequestReader[_], tag: ClassTag[_]): PartialFunction[Throwable,Try[A]] = { - case exc => Throw(NotParsed(reader.item, tag, exc)) + private[this] def notParsed[A](rr: PRequestReader[_, _], tag: ClassTag[_]): PartialFunction[Throwable, Try[A]] = { + case exc => Throw(NotParsed(rr.item, tag, exc)) } /** @@ -152,9 +158,9 @@ package object request extends LowPriorityImplicits { * * The resulting reader will fail when type conversion fails. */ - implicit class StringReaderOps(val reader: RequestReader[String]) extends AnyVal { - def as[A](implicit magnet: DecodeMagnet[A], tag: ClassTag[A]): RequestReader[A] = reader embedFlatMap { value => - Future.const(magnet()(value).rescue(notParsed(reader, tag))) + implicit class StringReaderOps[R](val rr: PRequestReader[R, String]) extends AnyVal { + def as[A](implicit magnet: DecodeMagnet[A], tag: ClassTag[A]): PRequestReader[R, A] = rr.embedFlatMap { value => + Future.const(magnet()(value).rescue(notParsed(rr, tag))) } } @@ -165,13 +171,13 @@ package object request extends LowPriorityImplicits { * The resulting reader will fail when the result is non-empty and type conversion fails. It will succeed if the * result is empty or type conversion succeeds. */ - implicit class StringOptionReaderOps(val reader: RequestReader[Option[String]]) extends AnyVal { - def as[A](implicit magnet: DecodeMagnet[A], tag: ClassTag[A]): RequestReader[Option[A]] = reader embedFlatMap { - case Some(value) => Future.const(magnet()(value).rescue(notParsed(reader, tag)) map (Some(_))) + implicit class StringOptionReaderOps[R](val rr: PRequestReader[R, Option[String]]) extends AnyVal { + def as[A](implicit magnet: DecodeMagnet[A], tag: ClassTag[A]): PRequestReader[R, Option[A]] = rr.embedFlatMap { + case Some(value) => Future.const(magnet()(value).rescue(notParsed(rr, tag)) map (Some(_))) case None => Future.None } - private[request] def noneIfEmpty: RequestReader[Option[String]] = reader map { + private[request] def noneIfEmpty: PRequestReader[R, Option[String]] = rr.map { case Some(value) if value.isEmpty => None case other => other } @@ -184,7 +190,7 @@ package object request extends LowPriorityImplicits { * The resulting reader will fail when the result is non-empty and type conversion fails on one 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 StringSeqReaderOps(val reader: RequestReader[Seq[String]]) { + implicit class StringSeqReaderOps[R](val rr: PRequestReader[R, 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 @@ -194,22 +200,22 @@ package object request extends LowPriorityImplicits { * somewhere else. Once we drop support for Scala 2.10, this class can safely extends AnyVal. */ - def as[A](implicit magnet: DecodeMagnet[A], tag: ClassTag[A]): RequestReader[Seq[A]] = - reader embedFlatMap { items => + def as[A](implicit magnet: DecodeMagnet[A], tag: ClassTag[A]): PRequestReader[R, Seq[A]] = + rr.embedFlatMap { items => val converted = items map (magnet()(_)) if (converted.forall(_.isReturn)) converted.map(_.get).toFuture - else RequestErrors(converted collect { case Throw(e) => NotParsed(reader.item, tag, e) }).toFutureException + else RequestErrors(converted collect { case Throw(e) => NotParsed(rr.item, tag, e) }).toFutureException } } /** * Implicit conversion that adds convenience methods to readers for optional values. */ - implicit class OptionReaderOps[A](val reader: RequestReader[Option[A]]) extends AnyVal { + implicit class OptionReaderOps[R, A](val rr: PRequestReader[R, Option[A]]) extends AnyVal { // TODO: better name. See #187. - private[request] def failIfNone: RequestReader[A] = reader embedFlatMap { + private[request] def failIfNone: PRequestReader[R, A] = rr.embedFlatMap { case Some(value) => value.toFuture - case None => NotPresent(reader.item).toFutureException + case None => NotPresent(rr.item).toFutureException } } @@ -247,6 +253,85 @@ package object request extends LowPriorityImplicits { case None => true } + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of two arguments. + */ + implicit class RrArrow2[R, A, B](val rr: PRequestReader[R, A ~ B]) extends AnyVal { + def ~~>[C](fn: (A, B) => Future[C]): PRequestReader[R, C] = + rr.embedFlatMap { case (a ~ b) => fn(a, b) } + + def ~>[C](fn: (A, B) => C): PRequestReader[R, C] = + rr.map { case (a ~ b) => fn(a, b) } + } + + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of three arguments. + */ + implicit class RrArrow3[R, A, B, C](val rr: PRequestReader[R, A ~ B ~ C]) extends AnyVal { + def ~~>[D](fn: (A, B, C) => Future[D]): PRequestReader[R, D] = + rr.embedFlatMap { case (a ~ b ~ c) => fn(a, b, c) } + + def ~>[D](fn: (A, B, C) => D): PRequestReader[R, D] = + rr.map { case (a ~ b ~ c) => fn(a, b, c) } + } + + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of four arguments. + */ + implicit class RrArrow4[R, A, B, C, D](val rr: PRequestReader[R, A ~ B ~ C ~ D]) extends AnyVal { + def ~~>[E](fn: (A, B, C, D) => Future[E]): PRequestReader[R, E] = + rr.embedFlatMap { case (a ~ b ~ c ~ d) => fn(a, b, c, d) } + + def ~>[E](fn: (A, B, C, D) => E): PRequestReader[R, E] = + rr.map { case (a ~ b ~ c ~ d) => fn(a, b, c, d) } + } + + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of five arguments. + */ + implicit class RrArrow5[R, A, B, C, D, E](val rr: PRequestReader[R, A ~ B ~ C ~ D ~ E]) extends AnyVal { + def ~~>[F](fn: (A, B, C, D, E) => Future[F]): PRequestReader[R, F] = + rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e) => fn(a, b, c, d, e) } + + def ~>[F](fn: (A, B, C, D, E) => F): PRequestReader[R, F] = + rr.map { case (a ~ b ~ c ~ d ~ e) => fn(a, b, c, d, e) } + } + + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of six arguments. + */ + implicit class RrArrow6[R, A, B, C, D, E, F](val rr: PRequestReader[R, A ~ B ~ C ~ D ~ E ~ F]) extends AnyVal { + def ~~>[G](fn: (A, B, C, D, E, F) => Future[G]): PRequestReader[R, G] = + rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e ~ f) => fn(a, b, c, d, e, f) } + + def ~>[G](fn: (A, B, C, D, E, F) => G): PRequestReader[R, G] = + rr.map { case (a ~ b ~ c ~ d ~ e ~ f) => fn(a, b, c, d, e, f) } + } + + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of seven arguments. + */ + implicit class RrArrow7[R, A, B, C, D, E, F, G](val rr: PRequestReader[R, A ~ B ~ C ~ D ~ E ~ F ~ G]) extends AnyVal { + def ~~>[H](fn: (A, B, C, D, E, F, G) => Future[H]): PRequestReader[R, H] = + rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e ~ f ~ g) => fn(a, b, c, d, e, f, g) } + + def ~>[H](fn: (A, B, C, D, E, F, G) => H): PRequestReader[R, H] = + rr.map { case (a ~ b ~ c ~ d ~ e ~ f ~ g) => fn(a, b, c, d, e, f, g) } + } + + /** + * Adds a `~>` and `~~>` compositors to `RequestReader` to compose it with function of eight arguments. + */ + implicit class RrArrow8[R, A, B, C, D, E, F, G, H]( + val rr: PRequestReader[R, A ~ B ~ C ~ D ~ E ~ F ~ G ~ H] + ) extends AnyVal { + def ~~>[I](fn: (A, B, C, D, E, F, G, H) => Future[I]): PRequestReader[R, I] = + rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e ~ f ~ g ~ h) => fn(a, b, c, d, e, f, g, h) } + + def ~>[I](fn: (A, B, C, D, E, F, G, H) => I): PRequestReader[R, I] = + rr.map { case (a ~ b ~ c ~ d ~ e ~ f ~ g ~ h) => fn(a, b, c, d, e, f, g, h) } + } + // Helper functions. private[request] def requestParam(param: String)(req: HttpRequest): Option[String] = req.params.get(param) orElse { @@ -282,214 +367,147 @@ package object request extends LowPriorityImplicits { } } - /** - * A required string param. - */ - object RequiredParam { + // A convenient method for internal needs. + private[request] def rr[A](i: RequestItem)(f: HttpRequest => A): RequestReader[A] = + RequestReader.embed[HttpRequest, A](i)(f(_).toFuture) - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads a required string ''param'' from the - * request or raises a [[io.finch.request.NotPresent NotPresent]] exception when the param is missing or empty. - * - * @param param the param to read - * - * @return a param value - */ - def apply(param: String): RequestReader[String] = - RequestReader(ParamItem(param))(requestParam(param)).failIfNone shouldNot beEmpty - } /** - * An optional string param. + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads a required string ''param'' from the + * request or raises a [[io.finch.request.NotPresent NotPresent]] exception when the param is missing or empty. + * + * @param param the param to read + * + * @return a param value */ - object OptionalParam { - - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional string ''param'' from the - * request into an `Option`. - * - * @param param the param to read - * - * @return an `Option` that contains a param value or `None` if the param is empty - */ - def apply(param: String): RequestReader[Option[String]] = - RequestReader(ParamItem(param))(requestParam(param)).noneIfEmpty - } + def RequiredParam(param: String): RequestReader[String] = + rr(ParamItem(param))(requestParam(param)).failIfNone.shouldNot(beEmpty) /** - * A required multi-value string param. + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional string ''param'' from the + * request into an `Option`. + * + * @param param the param to read + * + * @return an `Option` that contains a param value or `None` if the param is empty */ - object RequiredParams { - - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads a required multi-value string ''param'' - * from the request into a `Seq` or raises a [[io.finch.request.NotPresent NotPresent]] exception when the param is - * missing or empty. - * - * @param param the param to read - * - * @return a `Seq` that contains all the values of multi-value param - */ - def apply(param: String): RequestReader[Seq[String]] = - (RequestReader(ParamItem(param))(requestParams(param)) embedFlatMap { - case Nil => NotPresent(ParamItem(param)).toFutureException - case unfiltered => unfiltered.filter(_.nonEmpty).toFuture - }).shouldNot("be empty")(_.isEmpty) - } + def OptionalParam(param: String): RequestReader[Option[String]] = + rr(ParamItem(param))(requestParam(param)).noneIfEmpty /** - * An optional multi-value string param. + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads a required multi-value string ''param'' + * from the request into a `Seq` or raises a [[io.finch.request.NotPresent NotPresent]] exception when the param is + * missing or empty. + * + * @param param the param to read + * + * @return a `Seq` that contains all the values of multi-value param */ - object OptionalParams { - - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional multi-value string ''param'' - * from the request into a `Seq`. - * - * @param param the param to read - * - * @return a `Seq` that contains all the values of multi-value param or an empty seq `Nil` if the param is missing - * or empty. - */ - def apply(param: String): RequestReader[Seq[String]] = - RequestReader(ParamItem(param))(requestParams(param)(_).filter(_.nonEmpty)) - } + def RequiredParams(param: String): RequestReader[Seq[String]] = + rr(ParamItem(param))(requestParams(param)).embedFlatMap({ + case Nil => NotPresent(ParamItem(param)).toFutureException + case unfiltered => unfiltered.filter(_.nonEmpty).toFuture + }).shouldNot("be empty")(_.isEmpty) /** - * A required header. + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional multi-value string ''param'' + * from the request into a `Seq`. + * + * @param param the param to read + * + * @return a `Seq` that contains all the values of multi-value param or an empty seq `Nil` if the param is missing + * or empty. */ - object RequiredHeader { - - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads a required string ''header'' from the - * request or raises a [[io.finch.request.NotPresent NotPresent]] exception when the header is missing. - * - * @param header the header to read - * - * @return a header - */ - def apply(header: String): RequestReader[String] = - RequestReader(HeaderItem(header))(requestHeader(header)).failIfNone shouldNot beEmpty - } + def OptionalParams(param: String): RequestReader[Seq[String]] = + rr(ParamItem(param))(requestParams(param)(_).filter(_.nonEmpty)) /** - * An optional header. + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads a required string ''header'' from the + * request or raises a [[io.finch.request.NotPresent NotPresent]] exception when the header is missing. + * + * @param header the header to read + * + * @return a header */ - object OptionalHeader { - - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional string ''header'' from the - * request into an `Option`. - * - * @param header the header to read - * - * @return an `Option` that contains a header value or `None` if the header is not present in the request - */ - def apply(header: String): RequestReader[Option[String]] = - RequestReader(HeaderItem(header))(requestHeader(header)).noneIfEmpty - } + def RequiredHeader(header: String): RequestReader[String] = + rr(HeaderItem(header))(requestHeader(header)).failIfNone shouldNot beEmpty /** - * A [[io.finch.request.RequestReader RequestReader]] that reads a binary request ''body'', interpreted as a - * `Array[Byte]`, or throws a [[io.finch.request.NotPresent NotPresent]] exception. + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional string ''header'' from the + * request into an `Option`. + * + * @param header the header to read + * + * @return an `Option` that contains a header value or `None` if the header is not present in the request */ - object RequiredBinaryBody extends RequestReader[Array[Byte]] { - val item = BodyItem - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[Array[Byte]] = OptionalBinaryBody.failIfNone(req) - } + def OptionalHeader(header: String): RequestReader[Option[String]] = + rr(HeaderItem(header))(requestHeader(header)).noneIfEmpty /** * A [[io.finch.request.RequestReader RequestReader]] that reads a binary request ''body'', interpreted as a * `Array[Byte]`, into an `Option`. */ - object OptionalBinaryBody extends RequestReader[Option[Array[Byte]]] { - val item = BodyItem - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[Option[Array[Byte]]] = - req.contentLength match { - case Some(length) if length > 0 => Some(requestBody(req)).toFuture - case _ => Future.None - } + val OptionalBinaryBody: RequestReader[Option[Array[Byte]]] = rr(BodyItem) { req => + req.contentLength.flatMap(length => + if (length > 0) Some(requestBody(req)) else None + ) } /** - * A [[io.finch.request.RequestReader RequestReader]] that reads the request body, interpreted as a `String`, or - * throws a [[io.finch.request.NotPresent NotPresent]] exception. + * A [[io.finch.request.RequestReader RequestReader]] that reads a binary request ''body'', interpreted as a + * `Array[Byte]`, or throws a [[io.finch.request.NotPresent NotPresent]] exception. */ - object RequiredBody extends RequestReader[String] { - val item = BodyItem - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[String] = OptionalBody.failIfNone(req) - } + val RequiredBinaryBody: RequestReader[Array[Byte]] = OptionalBinaryBody.failIfNone /** * A [[io.finch.request.RequestReader RequestReader]] that reads the request body, interpreted as a `String`, into an * `Option`. */ - object OptionalBody extends RequestReader[Option[String]] { - val item = BodyItem - def apply[Req](req: Req)(implicit ev: Req => HttpRequest): Future[Option[String]] = for { - b <- OptionalBinaryBody(req) - } yield b map (new String(_, "UTF-8")) - } + val OptionalBody: RequestReader[Option[String]] = OptionalBinaryBody.map(_.map(new String(_, "UTF-8"))) /** - * An optional cookie. + * A [[io.finch.request.RequestReader RequestReader]] that reads the request body, interpreted as a `String`, or + * throws a [[io.finch.request.NotPresent NotPresent]] exception. */ - object OptionalCookie { - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional cookie from the request. - * - * @param cookie the name of the cookie to read - * - * @return an `Option` that contains a cookie or None if the cookie does not exist on the request. - */ - def apply(cookie: String): RequestReader[Option[Cookie]] = RequestReader(CookieItem(cookie))(requestCookie(cookie)) - } + val RequiredBody: RequestReader[String] = OptionalBody.failIfNone /** - * A required cookie. + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional cookie from the request. + * + * @param cookie the name of the cookie to read + * + * @return an `Option` that contains a cookie or None if the cookie does not exist on the request. */ - object RequiredCookie { + def OptionalCookie(cookie: String): RequestReader[Option[Cookie]] = rr(CookieItem(cookie))(requestCookie(cookie)) - /** - * Creates a [[RequestReader]] that reads a required cookie from the request or raises a [[NotPresent]] exception - * when the cookie is missing. - * - * @param cookieName the name of the cookie to read - * - * @return the cookie - */ - def apply(cookieName: String): RequestReader[Cookie] = OptionalCookie(cookieName).failIfNone - } + /** + * Creates a [[RequestReader]] that reads a required cookie from the request or raises a [[NotPresent]] exception + * when the cookie is missing. + * + * @param cookie the name of the cookie to read + * + * @return the cookie + */ + def RequiredCookie(cookie: String): RequestReader[Cookie] = OptionalCookie(cookie).failIfNone /** - * An optional uploaded file that is send via a multipart/form-data request. - */ - object OptionalFileUpload { - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional file from a multipart/form-data - * request. - * - * @param upload the name of the parameter to read - * @return an `Option` that contains the file or `None` is the parameter does not exist on the request. - */ - def apply(upload: String): RequestReader[Option[FileUpload]] = - RequestReader(ParamItem(upload))(requestUpload(upload)) - } + * Creates a [[io.finch.request.RequestReader RequestReader]] that reads an optional file from a multipart/form-data + * request. + * + * @param upload the name of the parameter to read + * @return an `Option` that contains the file or `None` is the parameter does not exist on the request. + */ + def OptionalFileUpload(upload: String): RequestReader[Option[FileUpload]] = + rr(ParamItem(upload))(requestUpload(upload)) /** - * A required uploaded file that is send via a multipart/form-data - * request + * Creates a [[io.finch.request.RequestReader RequestReader]] + * that reads a required file from a multipart/form-data request. + * + * @param upload the name of the parameter to read + * @return the file */ - object RequiredFileUpload { - /** - * Creates a [[io.finch.request.RequestReader RequestReader]] - * that reads a required file from a multipart/form-data request. - * - * @param upload the name of the parameter to read - * @return the file - */ - def apply(upload: String): RequestReader[FileUpload] = OptionalFileUpload(upload).failIfNone - } + def RequiredFileUpload(upload: String): RequestReader[FileUpload] = OptionalFileUpload(upload).failIfNone /** * An abstraction that is responsible for decoding the request of type `A`. @@ -529,6 +547,31 @@ package object request extends LowPriorityImplicits { def apply(): DecodeRequest[A] = d } + /** + * + */ + trait ApplicativeMagnet[R, A, S, B] { + def apply(req: S, left: PRequestReader[R, A]): Future[(Try[A], Try[B])] + } + + trait LowPriorityApplicativeMagnetImplicits { + implicit def rightAssociativeMagnet[R, A, S, B](right: PRequestReader[S, B])( + implicit ev: R %> S + ): ApplicativeMagnet[R, A, R, B] = new ApplicativeMagnet[R, A, R, B] { + def apply(req: R, left: PRequestReader[R, A]): Future[(Try[A], Try[B])] = + Future.join(left(req).liftToTry, right(ev(req)).liftToTry) + } + } + + object ApplicativeMagnet extends LowPriorityApplicativeMagnetImplicits { + implicit def leftAssociativeMagnet[R, A, S, B](right: PRequestReader[S, B])( + implicit ev: S %> R + ): ApplicativeMagnet[R, A, S, B] = new ApplicativeMagnet[R, A, S, B] { + def apply(req: S, left: PRequestReader[R, A]): Future[(Try[A], Try[B])] = + Future.join(left(ev(req)).liftToTry, right(req).liftToTry) + } + } + /** * A wrapper for two result values. */ diff --git a/core/src/main/scala/io/finch/route/LowPriorityRouterImplicits.scala b/core/src/main/scala/io/finch/route/LowPriorityRouterImplicits.scala new file mode 100644 index 000000000..b1c410791 --- /dev/null +++ b/core/src/main/scala/io/finch/route/LowPriorityRouterImplicits.scala @@ -0,0 +1,12 @@ +package io.finch.route + +trait LowPriorityRouterImplicits { + + /** + * Add `/>` compositors to `RouterN` to compose it with function of one argument. + */ + implicit class RArrow1[A](r: RouterN[A]) { + def />[B](fn: A => B): RouterN[B] = r.map(fn) + def |[B >: A](that: RouterN[B]): RouterN[B] = r orElse that + } +} diff --git a/core/src/main/scala/io/finch/route/Router.scala b/core/src/main/scala/io/finch/route/Router.scala index ddc32532e..109e4a67b 100644 --- a/core/src/main/scala/io/finch/route/Router.scala +++ b/core/src/main/scala/io/finch/route/Router.scala @@ -119,19 +119,6 @@ trait RouterN[+A] { self => def /(that: Router0): RouterN[A] = this andThen that - /** - * Maps this router to the given function `A => B`. - */ - def />[B](fn: A => B): RouterN[B] = - this map fn - - /** - * Sequentially composes this router with the given `that` router. The resulting router will succeed if either this or - * `that` routers are succeed. - */ - def |[B >: A](that: RouterN[B]): RouterN[B] = - this orElse that - // A workaround for https://issues.scala-lang.org/browse/SI-1336 def withFilter(p: A => Boolean): RouterN[A] = self } diff --git a/core/src/main/scala/io/finch/route/package.scala b/core/src/main/scala/io/finch/route/package.scala index 2f532d9d0..8a7b86063 100644 --- a/core/src/main/scala/io/finch/route/package.scala +++ b/core/src/main/scala/io/finch/route/package.scala @@ -49,7 +49,7 @@ import com.twitter.util.Future * ) * }}} */ -package object route { +package object route extends LowPriorityRouterImplicits { object tokens { // @@ -89,23 +89,46 @@ package object route { */ case class RouteNotFound(r: String) extends Exception(s"Route not found: $r") + /** + * Converts `Req` to `Route`. + */ + private[finch] def requestToRoute[Req](req: HttpRequest): Route = + (MethodToken(req.method): RouteToken) :: (req.path.split("/").toList.drop(1) map PathToken) + /** * Implicitly converts the given `Router[Service[_, _]]` into a service. */ implicit def endpointToService[Req, Rep]( r: RouterN[Service[Req, Rep]] )(implicit ev: Req => HttpRequest): Service[Req, Rep] = new Service[Req, Rep] { + def apply(req: Req): Future[Rep] = r(requestToRoute(req)) match { + case Some((Nil, service)) => service(req) + case _ => RouteNotFound(s"${req.method.toString.toUpperCase} ${req.path}").toFutureException + } + } - private def requestToRoute(req: Req)(implicit ev: Req => HttpRequest): Route = - (MethodToken(req.method): RouteToken) :: (req.path.split("/").toList.drop(1) map PathToken) + /** + * Add `/>` compositor to `RouterN` to compose it with function of two argument. + */ + implicit class RArrow2[A, B](val r: RouterN[A / B]) extends AnyVal { + def />[C](fn: (A, B) => C): RouterN[C] = + r.map { case a / b => fn(a, b) } + } - def apply(req: Req): Future[Rep] = { - val path = requestToRoute(req) - r(path) match { - case Some((Nil, service)) => service(req) - case _ => RouteNotFound(s"${req.method.toString.toUpperCase} ${req.path}").toFutureException - } - } + /** + * Add `/>` compositor to `RouterN` to compose it with function of three argument. + */ + implicit class RArrow3[A, B, C](val r: RouterN[A / B / C]) extends AnyVal { + def />[D](fn: (A, B, C) => D): RouterN[D] = + r.map { case a / b / c => fn(a, b, c) } + } + + /** + * Add `/>` compositor to `RouterN` to compose it with function of four argument. + */ + implicit class RArrow4[A, B, C, D](val r: RouterN[A / B / C / D]) extends AnyVal { + def />[E](fn: (A, B, C, D) => E): RouterN[E] = + r.map { case a / b / c / d => fn(a, b, c, d) } } implicit def intToMatcher(i: Int): Router0 = new Matcher(i.toString) diff --git a/core/src/test/scala/io/finch/request/RequestReaderCompanionSpec.scala b/core/src/test/scala/io/finch/request/RequestReaderCompanionSpec.scala index c29f21195..aa1ae2ed7 100644 --- a/core/src/test/scala/io/finch/request/RequestReaderCompanionSpec.scala +++ b/core/src/test/scala/io/finch/request/RequestReaderCompanionSpec.scala @@ -32,7 +32,7 @@ class RequestReaderCompanionSpec extends FlatSpec with Matchers { "The RequestReaderCompanion" should "support a factory method based on a function that reads from the request" in { val request: HttpRequest = Request(("foo", "5")) - val futureResult: Future[Option[String]] = RequestReader(ParamItem("foo"))(_ => Some("5"))(request) + val futureResult: Future[Option[String]] = RequestReader[HttpRequest, Option[String]](_ => Some("5"))(request) Await.result(futureResult) shouldBe Some("5") } diff --git a/core/src/test/scala/io/finch/route/RouterSpec.scala b/core/src/test/scala/io/finch/route/RouterSpec.scala index e6ae1b6db..d4f96888e 100644 --- a/core/src/test/scala/io/finch/route/RouterSpec.scala +++ b/core/src/test/scala/io/finch/route/RouterSpec.scala @@ -140,7 +140,7 @@ class RouterSpec extends FlatSpec with Matchers { it should "be composable as an endpoint" in { val r1 = Get / "a" / int /> { _ + 10 } - val r2 = Get / "b" / int / int /> { case a / b => a + b } + val r2 = Get / "b" / int / int /> { _ + _ } val r3 = r1 | r2 r3(route) shouldBe Some((route.drop(3), 11)) diff --git a/playground/src/main/scala/io/finch/playgorund/Main.scala b/playground/src/main/scala/io/finch/playgorund/Main.scala deleted file mode 100644 index 616d2591a..000000000 --- a/playground/src/main/scala/io/finch/playgorund/Main.scala +++ /dev/null @@ -1,83 +0,0 @@ -package io.finch.playgorund - -import com.twitter.finagle.{Httpx, Service, Filter} -import com.twitter.util.{Await, Future} - -import io.finch.{Endpoint => _, _} -import io.finch.request._ -import io.finch.route._ -import io.finch.response._ - -/** - * GET /user/groups -> Seq[Group] - * POST /groups?name=foo -> Group - * PUT /user/groups/:group -> User - */ -object Main extends App { - - // model - case class Group(name: String, ownerId: Int = 0) - case class User(id: Int, groups: Seq[Group]) - - // custom request - case class AuthReq(http: HttpRequest, userId: Int) - implicit val authReqEv: AuthReq => HttpRequest = _.http - - // GET /user/groups -> Seq[Group] - def getUserGroups(userId: Int): Future[Seq[Group]] = Seq(Group("foo"), Group("bar")).toFuture - - // POST /groups?name=foo -> Group - def postGroup(name: String, ownerId: Int): Future[Group] = Group(name, ownerId).toFuture - - // PUT /user/groups/:group -> User - def putUserGroup(userId: Int, group: String): Future[User] = User(userId, Seq.empty[Group]).toFuture - - implicit val encodeGroup: EncodeResponse[Group] = - EncodeResponse[Group]("application/json") { g => - s""" - |{"name":"$${g.name}","owner":$${g.ownerId}} - """.stripMargin - } - - implicit def encodeUser(implicit e: EncodeResponse[Group]): EncodeResponse[User] = - EncodeResponse[User]("application/json") { u => - s""" - |{"id":$${u.id},"groups":[$${encodeSeq(e(u.groups))}]} - """.stripMargin - } - - implicit def encodeSeq[A](implicit encode: EncodeResponse[A]): EncodeResponse[Seq[A]] = - EncodeResponse[Seq[A]]("application/json") { seq => - seq.map(encode(_)).mkString("[", ",", "]") - } - - def service[Req, Rep](f: Req => Future[Rep]): Service[Req, Rep] = Service.mk(f) - - val endpoint: Endpoint[AuthReq, HttpResponse] = ( - Get / "user" / "groups" /> service[AuthReq, HttpResponse] { req => - getUserGroups(req.userId).map(Ok(_)) - } - ) | ( - Post / "groups" /> service[AuthReq, HttpResponse] { req => - RequiredParam("group").embedFlatMap(postGroup(_, req.userId)).map(Ok(_))(req) - } - ) | ( - Put / "user" / "groups" / string /> { group => - service[AuthReq, HttpResponse] { req => - putUserGroup(req.userId, group).map(Ok(_)) - } - } - ) - - val authorize = new Filter[HttpRequest, HttpResponse, AuthReq, HttpResponse] { - def apply(req: HttpRequest, service: Service[AuthReq, HttpResponse]): Future[HttpResponse] = for { - id <- OptionalHeader("X-User-Id")(req) - rep <- service(AuthReq(req, id.getOrElse("0").toInt)) - } yield rep - } - - val api: Service[HttpRequest, HttpResponse] = - authorize andThen (endpoint: Service[AuthReq, HttpResponse]) - - Await.ready(Httpx.serve(":8081", api)) -} diff --git a/playground/src/main/scala/io/finch/playground/Main.scala b/playground/src/main/scala/io/finch/playground/Main.scala new file mode 100644 index 000000000..64d2b4ce2 --- /dev/null +++ b/playground/src/main/scala/io/finch/playground/Main.scala @@ -0,0 +1,64 @@ +package io.finch.playground + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import com.twitter.finagle.{Service, Filter, Httpx} +import com.twitter.util.{Future, Await} + +import io.finch.{Endpoint => _, _} +import io.finch.micro._ +import io.finch.request._ +import io.finch.route.{Endpoint => _, _} +import io.finch.jackson._ + +/** + * GET /user/groups -> Seq[Group] + * POST /groups?name=foo -> Group + * PUT /user/groups/:group -> User + */ +object Main extends App { + + // enable finch-jackson magic + implicit val objectMapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule) + + // model + case class Group(name: String, ownerId: Int = 0) + case class User(id: Int, groups: Seq[Group]) + + case class UnknownUser(id: Int) extends Exception(s"Unknown user with id=$id") + + case class AuthRequest(userId: Int, httpRequest: HttpRequest) + val auth: Filter[HttpRequest, HttpResponse, AuthRequest, HttpResponse] = + new Filter[HttpRequest, HttpResponse, AuthRequest, HttpResponse] { + def apply(req: HttpRequest, service: Service[AuthRequest, HttpResponse]): Future[HttpResponse] = + service(AuthRequest(10, req)) + } + + // implicit view + implicit val reqEv: AuthRequest %> HttpRequest = View(_.httpRequest) + + type MicroA[A] = PMicro[AuthRequest, A] + type EndpointA = PEndpoint[AuthRequest] + + val currentUser: MicroA[Int] = Micro(_.userId) + + // GET /user/groups -> Seq[Group] + val getUserGroups: MicroA[Seq[Group]] = + currentUser ~> { userId => Seq(Group("foo", userId), Group("bar", userId)) } + + // POST /groups?name=foo -> Group + val postGroup: MicroA[Group] = + RequiredParam("name") ~ currentUser ~> Group + + // PUT /user/groups/:group -> User + def putUserGroup(group: String): MicroA[User] = + currentUser ~> { User(_, Seq.empty[Group]) } + + // an API endpoint + val api: EndpointA = + Get / "user" / "groups" /> getUserGroups | + Post / "groups" /> postGroup | + Put / "user" / "groups" / string /> putUserGroup + + Await.ready(Httpx.serve(":8081", auth andThen api)) +}