Chaining futures in Scala
Suppose I want to make coffee. This involves 4 steps:
- 1a. grind coffee beans
- 1b. heat water
- combine
- filter
Suppose I want to make coffee. This involves 4 steps:
Building a rest API with akka and spray is easy. This is how I did it: SprayApiApp:
import akka.actor.{ActorSystem, Props}
import akka.io.IO
import akka.pattern.ask
import akka.util.Timeout
import spray.can.Http
import scala.concurrent.duration._
object SprayApiApp extends App {
//we need an ActorSystem to host our application in
implicit val system = ActorSystem("SprayApiApp")
//create apiActor
val apiActor = system.actorOf(Props[ApiActor], "apiActor")
//timeout needs to be set as an implicit val for the ask method (?)
implicit val timeout = Timeout(5.seconds)
//start a new HTTP server on port 8080 with apiActor as the handler
IO(Http) ? Http.Bind(apiActor, interface = "localhost", port = 8080)
}
ApiActor:
import akka.actor.{ActorLogging, Actor}
import spray.http.MediaTypes
import spray.httpx.SprayJsonSupport._
import spray.json.DefaultJsonProtocol
import spray.routing._
object RobotProtocol extends DefaultJsonProtocol {
//Our domain class
case class Robot(name: String)
//We use the default json marshalling for Robot.
//There are multiple jsonFormat methods in DefaultJsonProtocol. Depending on how many parameters the model class has.
//Robot has just one, so we use jsonFormat1
implicit val RobotFormat = jsonFormat1(Robot)
}
import RobotProtocol._
class ApiActor extends Actor with HttpService with ActorLogging {
//A list of our domain objects
var robots = List(Robot("R2D2"), Robot("Asimo"))
//The HttpService trait defines only one abstract member, which
//connects the services environment to the enclosing actor or test
def actorRefFactory = context
//This actor only runs our route, but you could add
//other things here, like request stream processing or timeout handling
def receive = runRoute(apiRoute)
//Notice that both path methods return a Route. We need to chain them together with ~
val apiRoute: Route =
path("robots") {
get { //with get we will return our current list of robots
log.info("Building get route")
complete {
log.info("Executing get route")
//complete will return the result in an appropriate format
//With SprayJsonSupport it knows how to marshall a List to json
//With RobotFormat it knows how to marshall Robot
robots
}
} ~ post { //With post we will add a robot
log.info("Building post route")
handleWith { robot: Robot => //handleWith will unmarshall the input
log.info("Executing post route")
robots = robot :: robots
robot //handleWith will also marshall the result. Here we simply return the new robot.
}
}
} ~ path("") { //When we go to localhost:8080/ just show a link to localhost:8080/robots
respondWithMediaType(MediaTypes.`text/html`) { //XML is marshalled to `text/xml` by default, so we simply override here
complete {
[The list of robots](/robots)
}
}
}
}
When you start using Scala, it's tempting to also start using sbt. You can also use your favorite build tool: Gradle. This is my default Gradle build file:
apply plugin: 'scala'
repositories {
mavenCentral()
}
dependencies {
compile group: 'org.scala-lang', name: 'scala-library', version: '2.11.5'
compile group: 'com.typesafe.akka', name: 'akka-actor_2.11', version: '2.3.9'
compile group: 'com.typesafe.akka', name: 'akka-remote_2.11', version: '2.3.9'
testCompile group: 'org.scalatest', name: 'scalatest_2.11', version: '2.2.4'
}
//run the akka application
task run(type: JavaExec, dependsOn: classes) {
//object to run. (The one that extends App)
main = 'pi.Pi'
classpath sourceSets.main.runtimeClasspath
classpath configurations.runtime
}
//run scala tests. These are not automatically picked up by gradle,
//so we run them like this.
task spec(dependsOn: ['testClasses'], type: JavaExec) {
main = 'org.scalatest.tools.Runner'
args = ['-R', 'build/classes/test', '-o']
classpath = sourceSets.test.runtimeClasspath
}