diff --git a/akka-docs/modules/code/Global.scala b/akka-docs/modules/code/Global.scala new file mode 100644 index 0000000000..021e30d5b1 --- /dev/null +++ b/akka-docs/modules/code/Global.scala @@ -0,0 +1,6 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +//#global +object Global extends com.typesafe.play.mini.Setup(akka.docs.http.PlayMiniApplication) +//#global \ No newline at end of file diff --git a/akka-docs/modules/code/akka/docs/http/PlayMiniApplication.scala b/akka-docs/modules/code/akka/docs/http/PlayMiniApplication.scala new file mode 100644 index 0000000000..15c0de0ac5 --- /dev/null +++ b/akka-docs/modules/code/akka/docs/http/PlayMiniApplication.scala @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.docs.http + +//#imports +import com.typesafe.play.mini.{ POST, GET, Path, Application } +import play.api.mvc.{ Action, AsyncResult } +import play.api.mvc.Results._ +import play.api.libs.concurrent._ +import play.api.data._ +import play.api.data.Forms._ +import akka.pattern.ask +import akka.util.Timeout +import akka.util.duration._ +import akka.actor.{ ActorSystem, Props, Actor } +import scala.collection.mutable.{ Map ⇒ MutableMap } +//#imports + +//#playMiniDefinition +object PlayMiniApplication extends Application { + //#playMiniDefinition + private val system = ActorSystem("sample") + //#regexURI + private final val StatementPattern = """/account/statement/(\w+)""".r + //#regexURI + private lazy val accountActor = system.actorOf(Props[AccountActor]) + implicit val timeout = Timeout(1000 milliseconds) + + //#route + def route = { + //#routeLogic + //#simpleGET + case GET(Path("/ping")) ⇒ Action { + Ok("Pong @ " + System.currentTimeMillis) + } + //#simpleGET + //#regexGET + case GET(Path(StatementPattern(accountId))) ⇒ Action { + AsyncResult { + //#innerRegexGET + (accountActor ask Status(accountId)).mapTo[Int].asPromise.map { r ⇒ + if (r >= 0) Ok("Account total: " + r) + else BadRequest("Unknown account: " + accountId) + } + //#innerRegexGET + } + } + //#regexGET + //#asyncDepositPOST + case POST(Path("/account/deposit")) ⇒ Action { implicit request ⇒ + //#formAsyncDepositPOST + val (accountId, amount) = commonForm.bindFromRequest.get + //#formAsyncDepositPOST + AsyncResult { + (accountActor ask Deposit(accountId, amount)).mapTo[Int].asPromise.map { r ⇒ Ok("Updated account total: " + r) } + } + } + //#asyncDepositPOST + //#asyncWithdrawPOST + case POST(Path("/account/withdraw")) ⇒ Action { implicit request ⇒ + val (accountId, amount) = commonForm.bindFromRequest.get + AsyncResult { + (accountActor ask Withdraw(accountId, amount)).mapTo[Int].asPromise.map { r ⇒ + if (r >= 0) Ok("Updated account total: " + r) + else BadRequest("Unknown account or insufficient funds. Get your act together.") + } + } + } + //#asyncWithdrawPOST + //#routeLogic + } + //#route + + //#form + val commonForm = Form( + tuple( + "accountId" -> nonEmptyText, + "amount" -> number(min = 1))) + //#form +} + +//#cases +case class Status(accountId: String) +case class Deposit(accountId: String, amount: Int) +case class Withdraw(accountId: String, amount: Int) +//#cases + +//#actor +class AccountActor extends Actor { + var accounts = MutableMap[String, Int]() + + //#receive + def receive = { + //#senderBang + case Status(accountId) ⇒ sender ! accounts.getOrElse(accountId, -1) + //#senderBang + case Deposit(accountId, amount) ⇒ sender ! deposit(accountId, amount) + case Withdraw(accountId, amount) ⇒ sender ! withdraw(accountId, amount) + } + //#receive + + private def deposit(accountId: String, amount: Int): Int = { + accounts.get(accountId) match { + case Some(value) ⇒ + val newValue = value + amount + accounts += accountId -> newValue + newValue + case None ⇒ + accounts += accountId -> amount + amount + } + } + + private def withdraw(accountId: String, amount: Int): Int = { + accounts.get(accountId) match { + case Some(value) ⇒ + if (value < amount) -1 + else { + val newValue = value - amount + accounts += accountId -> newValue + newValue + } + case None ⇒ -1 + } + } + //#actor +} \ No newline at end of file diff --git a/akka-docs/modules/http.rst b/akka-docs/modules/http.rst index 8388d65702..97978ed5f5 100644 --- a/akka-docs/modules/http.rst +++ b/akka-docs/modules/http.rst @@ -7,8 +7,194 @@ HTTP .. contents:: :local: -Play! ------ +Play2-mini +---------- +The Akka team recommends the `Play2-mini `_ framework when building RESTful +service applications that integrates with Akka. It provides a REST API on top of `Play2 `_. -Akka will recommend using `Play! Mini `_ +Getting started +--------------- + +First you must make your application aware of play-mini. +In SBT you just have to add the following to your _libraryDependencies_:: + + libraryDependencies += "com.typesafe" %% "play-mini" % "2.0-RC1-SNAPSHOT" + +Sample Application +------------------ + +To illustrate how easy it is to wire a RESTful service with Akka we will use a sample application. +The aim of the application is to show how to use play-mini and Akka in combination. Do not put too much +attention on the actual business logic itself, which is a extremely simple bank application, as building a bank +application is a little more complex than what's shown in the sample... + +The application should support the following URL commands: + - GET /ping - returns a Pong message with the time of the server (used to see if the application is up and running) + - GET /account/statement/{accountId} - returns the account statement + - POST /account/deposit - deposits money to an account (and creates a new one if it's not already existing) + - POST /account/withdraw - withdraws money from an account + +Error messages will be returned in case of any misuse of the application, e.g. withdrawing more money than an +account has etc. + +Getting started +--------------- + +To build a play-mini application you first have to make your object extend com.typesafe.play.mini.Application: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: playMiniDefinition + +The next step is to implement the mandatory method ``route``: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: route + :exclude: routeLogic + +It is inside the ``route`` method that all the magic happens. +In the sections below we will show how to set up play-mini to handle both GET and POST HTTP calls. + +Simple GET +---------- + +We start off by creating the simplest method we can - a "ping" method: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: simpleGET + +As you can see in the section above play-mini uses Scala's wonderful pattern matching. +In the snippet we instruct play-mini to reply to all HTTP GET calls with the URI "/ping". +The ``Action`` returned comes from Play! and you can find more information about it `here `_. + +.. _Advanced-GET: + +Advanced GET +------------ + +Let's try something more advanced, retrieving parameters from the URI and also make an asynchronous call to an actor: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: regexGET + +The regular expression looks like this: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: regexURI + +In the snippets above we extract a URI parameter with the help of a simple regular expression and then we pass this +parameter on to the underlying actor system. As you can see ``AsyncResult`` is being used. This means that the call to +the actor will be performed asynchronously, i.e. no blocking. + +The asynchronous call to the actor is being done with a ``ask``, e.g.:: + + (accountActor ask Status(accountId)) + +The actor that receives the message returns the result by using a standard *sender !* +as can be seen here: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: senderBang + +When the result is returned to the calling code we use some mapping code in Play to convert a Akka future to a Play future. +This is shown in this code: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: innerRegexGET + +In this snippet we check the result to decide what type of response we want to send to the calling client. + +Using HTTP POST +--------------- + +Okay, in the sections above we have shown you how to use play-mini for HTTP GET calls. Let's move on to when the user +posts values to the application. + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: asyncDepositPOST + +As you can see the structure is almost the same as for the :ref:`Advanced-GET`. The difference is that we make the +``request`` parameter ``implicit`` and also that the following line of code is used to extract parameters from the POST. + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: formAsyncDepositPOST + +The code snippet used to map the call to parameters looks like this: + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + :include: form + +Apart from the mapping of parameters the call to the actor looks is done the same as in :ref:`Advanced-GET`. + +The Complete Code Sample +------------------------ + +Below is the complete application in all its beauty. + +Global.scala (/src/main/scala/Global.scala): + +.. includecode:: code/Global.scala + +PlayMiniApplication.scala (/src/main/scala/akka/docs/http/PlayMiniApplication.scala): + +.. includecode:: code/akka/docs/http/PlayMiniApplication.scala + +Build.scala (/project/Build.scala): + +.. code-block:: scala + + import sbt._ + import Keys._ + + object PlayMiniApplicationBuild extends Build { + lazy val root = Project(id = "play-mini-application", base = file("."), settings = Project.defaultSettings).settings( + libraryDependencies += "com.typesafe" %% "play-mini" % "2.0-RC1-SNAPSHOT", + mainClass in (Compile, run) := Some("play.core.server.NettyServer")) + } + +Running the Application +----------------------- + +Firstly, start up the application by opening a command terminal and type:: + + > sbt + > run + +Now you should see something similar to this in your terminal window:: + + [info] Running play.core.server.NettyServer + Play server process ID is 2523 + [info] play - Application started (Prod) + [info] play - Listening for HTTP on port 9000... + +In this example we will use the awesome `cURL `_ command to interact with the application. +Fire up a command terminal and try the application out:: + + First we check the status of a couple of accounts: + > curl http://localhost:9000/account/statement/TheDudesAccount + Unknown account: TheDudesAccount + > curl http://localhost:9000/account/statement/MrLebowskisAccount + Unknown account: MrLebowskisAccount + + Now deposit some money to the accounts: + > curl -d "accountId=TheDudesAccount&amount=1000" http://localhost:9000/account/deposit + Updated account total: 1000 + > curl -d "accountId=MrLebowskisAccount&amount=500" http://localhost:9000/account/deposit + Updated account total: 500 + + Next thing is to check the status of the account: + > curl http://localhost:9000/account/statement/TheDudesAccount + Account total: 1000 + > curl http://localhost:9000/account/statement/MrLebowskisAccount + Account total: 500 + + Fair enough, let's try to withdraw some cash shall we: + > curl -d "accountId=TheDudesAccount&amount=999" http://localhost:9000/account/withdraw + Updated account total: 1 + > curl -d "accountId=MrLebowskisAccount&amount=999" http://localhost:9000/account/withdraw + Unknown account or insufficient funds. Get your act together. + > curl -d "accountId=MrLebowskisAccount&amount=500" http://localhost:9000/account/withdraw + Updated account total: 0 + +Yeah, it works! +Now we leave it to the astute reader of this document to take advantage of the power of play-mini and Akka. \ No newline at end of file diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 7ad4be2a9f..3d2b17b63b 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -473,7 +473,7 @@ object Dependencies { val tutorials = Seq(Test.scalatest, Test.junit) - val docs = Seq(Test.scalatest, Test.junit) + val docs = Seq(Test.scalatest, Test.junit, playMini) val zeroMQ = Seq(Test.scalatest, Test.junit, protobuf, Dependency.zeroMQ) } @@ -497,6 +497,7 @@ object Dependency { val Slf4j = "1.6.4" val Spring = "3.0.5.RELEASE" val Zookeeper = "3.4.0" + val PlayMini = "2.0-RC1-SNAPSHOT" } // Compile @@ -533,6 +534,7 @@ object Dependency { val zookeeper = "org.apache.hadoop.zookeeper" % "zookeeper" % V.Zookeeper // ApacheV2 val zookeeperLock = "org.apache.hadoop.zookeeper" % "zookeeper-recipes-lock" % V.Zookeeper // ApacheV2 val zeroMQ = "org.zeromq" %% "zeromq-scala-binding" % "0.0.3" // ApacheV2 + val playMini = "com.typesafe" % "play-mini_2.9.1" % V.PlayMini // Provided