Skip to content

Commit

Permalink
Fix #128
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Jan 19, 2015
1 parent 39fe68c commit 3cd6333
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 19 deletions.
82 changes: 64 additions & 18 deletions demo/src/main/scala/io/finch/demo/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -71,23 +103,28 @@ 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 {
name <- RequiredParam("name")
_ <- 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
Expand All @@ -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
Expand All @@ -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 =>
Expand All @@ -130,14 +169,23 @@ 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)
}
}
}

// 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] {
Expand All @@ -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 {

Expand All @@ -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))
}
2 changes: 1 addition & 1 deletion docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3cd6333

Please sign in to comment.