From 3cd633327b2980d73cf9f6f0467125a4ed237c29 Mon Sep 17 00:00:00 2001 From: Vladimir Kostyukov Date: Sun, 18 Jan 2015 23:25:04 -0800 Subject: [PATCH] Fix #128 --- demo/src/main/scala/io/finch/demo/Main.scala | 82 +++++++++++++++----- docs.md | 2 +- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/demo/src/main/scala/io/finch/demo/Main.scala b/demo/src/main/scala/io/finch/demo/Main.scala index d7809e2f6..7f6fe296b 100644 --- a/demo/src/main/scala/io/finch/demo/Main.scala +++ b/demo/src/main/scala/io/finch/demo/Main.scala @@ -22,8 +22,7 @@ package io.finch.demo -import com.twitter.finagle.{SimpleFilter, Service} -import com.twitter.finagle.Httpx +import com.twitter.finagle.{Filter, SimpleFilter, Service, Httpx} import com.twitter.finagle.httpx.Method import com.twitter.finagle.httpx.path._ import com.twitter.util.Await @@ -39,11 +38,44 @@ import io.finch._ // import ''Endpoint'' and pipe ''!'' operator import io.finch.json._ // import finch-json classes such as ''Json'' import io.finch.request._ // import request readers such as ''RequiredParam'' import io.finch.response._ // import response builders such as ''BadRequest'' -import io.finch.auth._ // import ''BasicallyAuthorize'' filter + +// A custom request type that wraps an ''HttpRequest''. +// We prefer composition over inheritance. +case class AuthRequest(http: HttpRequest, secret: String) + +// A companion object for ''AuthRequest'' that is required only to +// wrap an implicit value ''authReqEv''. +object AuthRequest { + + // We define an implicit view from ''AuthRequest'' to ''HttpRequest'', + // so we can get two benefits: + // 1. We can treat an ''Endpoint'' as a ''Service'', since it will be implicitly converted. + // 2. We can treat an ''AuthRequest'' as ''HttpRequest'' and pass it to ''RequestReader''. + implicit val authReqEv = (req: AuthRequest) => req.http +} + +// Import an implicit view. +import AuthRequest._ // A simple base class for data classes. trait ToJson { def toJson: Json } +// A companion object for ''ToJson'' used to define an implicit class +// ''SeqToJson'' that converts a service that returns a sequence of ''ToJson'' +// objects into a service that returns ''ToJson'' object. +object ToJson { + def seqToJson(seq: Seq[ToJson]) = new ToJson { + def toJson = Json.arr(seq.map { _.toJson }: _*) + } + + implicit class SeqToJson[A](s: Service[A, Seq[ToJson]]) extends Service[A, ToJson] { + def apply(req: A) = s(req) map seqToJson + } +} + +// Import implicit class. +import ToJson._ + // A ticket object with two fields: ''id'' and ''label''. case class Ticket(id: Long, label: String) extends ToJson { def toJson = Json.obj( @@ -71,15 +103,20 @@ object WithGeneratedId { } // A REST service that fetches a user with ''userId'' from the database ''db''. -case class GetUser(userId: Long, db: Main.Db) extends Service[HttpRequest, User] { - def apply(req: HttpRequest) = db.get(userId) match { +case class GetUser(userId: Long, db: Main.Db) extends Service[AuthRequest, User] { + def apply(req: AuthRequest) = db.get(userId) match { case Some(user) => user.toFuture case None => UserNotFound(userId).toFutureException } } +// A REST service that fetches all users from the the database ''db''. +case class GetAllUsers(db: Main.Db) extends Service[AuthRequest, Seq[User]] { + def apply(req: AuthRequest) = db.values.toSeq.toFuture +} + // A REST service that inserts a new user with ''userId'' into the database ''db''. -case class PostUser(userId: Long, db: Main.Db) extends Service[HttpRequest, User] { +case class PostUser(userId: Long, db: Main.Db) extends Service[AuthRequest, User] { // A requests reader that reads user objects from the http request. // A user is represented by url-encoded param ''name''. val user: RequestReader[User] = for { @@ -87,7 +124,7 @@ case class PostUser(userId: Long, db: Main.Db) extends Service[HttpRequest, User _ <- ValidationRule("name", "should be greater then 5 symbols") { name.length > 5 } } yield User(userId, name, Seq.empty[Ticket]) - def apply(req: HttpRequest) = for { + def apply(req: AuthRequest) = for { u <- user(req) } yield { db += (userId -> u) // add new user into a mutable map @@ -101,14 +138,14 @@ object TurnModelIntoJson extends Service[ToJson, Json] { } // A REST service that add a ticket to a given user ''userId''. -case class PostUserTicket(userId: Long, ticketId: Long, db: Main.Db) extends Service[HttpRequest, Ticket] { +case class PostUserTicket(userId: Long, ticketId: Long, db: Main.Db) extends Service[AuthRequest, Ticket] { // A request reader that reads ticket object from the http request. // A ticket object is represented by json object serialized in request body. val ticket: RequestReader[Ticket] = for { json <- RequiredBody[Json] } yield Ticket(ticketId, json[String]("label").getOrElse("N/A")) - def apply(req: HttpRequest) = for { + def apply(req: AuthRequest) = for { t <- ticket(req) u <- GetUser(userId, db)(req) // fetch exist user updatedU = u.copy(tickets = u.tickets :+ t) // modify its tickets @@ -119,8 +156,10 @@ case class PostUserTicket(userId: Long, ticketId: Long, db: Main.Db) extends Ser } // A REST endpoint that routes user-specific requests. -object UserEndpoint extends Endpoint[HttpRequest, ToJson] { +object UserEndpoint extends Endpoint[AuthRequest, ToJson] { def route = { + case Method.Get -> Root / "users" => + GetAllUsers(Main.Db) case Method.Get -> Root / "users" / Long(id) => GetUser(id, Main.Db) case Method.Post -> Root / "users" => WithGeneratedId { id => @@ -130,7 +169,7 @@ object UserEndpoint extends Endpoint[HttpRequest, ToJson] { } // A REST endpoint that routes ticket-specific requests. -object TicketEndpoint extends Endpoint[HttpRequest, ToJson] { +object TicketEndpoint extends Endpoint[AuthRequest, ToJson] { def route = { case Method.Post -> Root / "users" / Long(userId) / "tickets" => WithGeneratedId { id => PostUserTicket(userId, id, Main.Db) @@ -138,6 +177,15 @@ object TicketEndpoint extends Endpoint[HttpRequest, ToJson] { } } +// A Finagle filter that authorizes a request: performs conversion ''HttpRequest'' => ''AuthRequest''. +object Authorize extends Filter[HttpRequest, HttpResponse, AuthRequest, HttpResponse] { + def apply(req: HttpRequest, service: Service[AuthRequest, HttpResponse]) = for { + secret <- RequiredHeader("Secret")(req) + rep <- if (secret == "open sesame") service(AuthRequest(req, secret)) + else Unauthorized(Json.obj("error" -> "wrong_secret")).toFuture + } yield rep +} + // A simple Finagle filter that handles all the exceptions, which might be thrown by // a request reader of one of the REST services. object HandleExceptions extends SimpleFilter[HttpRequest, HttpResponse] { @@ -155,7 +203,7 @@ object HandleExceptions extends SimpleFilter[HttpRequest, HttpResponse] { /** * To run the demo from console use: * - * > sbt 'project finch-demo' 'run io.finch.demo.Main' + * > sbt 'project demo' 'run io.finch.demo.Main' */ object Main extends App { @@ -166,15 +214,13 @@ object Main extends App { val Db = new ConcurrentHashMap[Long, User]().asScala // An http endpoint that is composed of user and ticket endpoints. - val httpBackend: Endpoint[HttpRequest, HttpResponse] = + val httpBackend: Endpoint[AuthRequest, HttpResponse] = (UserEndpoint orElse TicketEndpoint) ! TurnModelIntoJson ! TurnIntoHttp[Json] - // A backend endpoint with exception handler and Basic HTTP Auth filter. + // A backend endpoint with exception handler and Auth filter. val backend: Endpoint[HttpRequest, HttpResponse] = - BasicallyAuthorize("user", "password") ! HandleExceptions ! httpBackend + HandleExceptions ! Authorize ! httpBackend // A default Finagle service builder that runs the backend. - val socket = new InetSocketAddress(8080) - val server = Httpx.serve(socket, backend) - Await.ready(server) + Await.ready(Httpx.serve(new InetSocketAddress(8080), backend)) } diff --git a/docs.md b/docs.md index 0ca055dcb..9f442e812 100644 --- a/docs.md +++ b/docs.md @@ -32,7 +32,7 @@ Scala file [Main.scala][1] that is worth reading. The following command may be used to run the demo: ```bash -sbt 'project finch-demo' 'run io.finch.demo.Main' +sbt 'project demo' 'run io.finch.demo.Main' ``` ## Endpoints