├── .gitignore ├── README.md ├── SETUP.md ├── build.sbt ├── project └── build.properties └── src ├── main └── scala │ └── com │ └── xebia │ └── exercise1 │ ├── Main.scala │ ├── Receptionist.scala │ ├── ReverseActor.scala │ └── ReverseRequest.scala └── test └── scala └── com └── xebia └── exercise1 ├── ReceptionistSpec.scala ├── ReverseActorSpec.scala └── TestSupport.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Web Project 4 | node_modules 5 | dist 6 | .tmp 7 | .sass-cache 8 | bower_components 9 | coverage 10 | test-results.xml 11 | 12 | # Scala project 13 | *.class 14 | *.log 15 | dist/* 16 | target/ 17 | lib_managed/ 18 | src_managed/ 19 | .scala_dependencies 20 | .idea* 21 | .sublime 22 | *.sublime-* 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | If you don't have a working Scala development setup yet, please see the [setup instructions](SETUP.md) 2 | 3 | Exercise 1 4 | ========== 5 | 6 | In this exercise you will learn about a pattern known as **the Receptionist**. 7 | 8 | The Receptionist has the following responsibilities: 9 | 10 | - Receive messages from the outside world using some endpoint. 11 | - Create child actors. 12 | - delegate to child actors to do the work. The child actors respond back to the receptionist for a request/response interaction (or 13 | the receptionist notifies child actors 'fire and forget style' which is not part of this exercise.) 14 | - Supervise the child actors. (the default supervision strategy is used, supervision is not part of this exercise.) 15 | 16 | ### Objective 17 | 18 | The objective of this exercise is to get familiar with the **Receptionist** pattern. 19 | You will learn how a child actor is created by the Receptionist and how work is delegated to the child actor. 20 | Although [Spray](http://spray.io) is used in the exercise it is not required to know Spray to complete the exercise, since all required code to hook up to HTTP is provided. 21 | 22 | ### What is already prepared 23 | 24 | A **Main** App which uses the Spray-can HTTP IO extension. This Main app creates the ActorSystem, starts the HTTP extension and registers the **Receptionist** Actor as HTTP listener. 25 | If you comment out the **ReverseActorSpec** the /reverse path should work. 26 | The Receptionist listens on the path "/reverse" for a HTTP POST of a JSON entity, for instance: 27 | 28 | 29 | { 30 | "value" : "reverse this!" 31 | } 32 | 33 | An example using the **httpie** command line tool: 34 | 35 | http POST localhost:8000/reverse value="reverse this\\!" 36 | 37 | The result should be something like: 38 | 39 | HTTP/1.1 200 OK 40 | { 41 | "value": "!siht esrever" 42 | } 43 | 44 | In this exercise you will replace the default string reversal code inside the ReverseRoute (which is mixed into the Receptionist) with a call to a child actor: the ReverseActor. 45 | 46 | ### The Exercise 47 | 48 | Look for the TODO's in the project and follow the instructions. 49 | The following tasks will need to be completed: 50 | 51 | Implement the ReverseActor (reverse a string and send back to sender). 52 | 53 | - define ReverseActor 54 | - define Reverse case class and ReverseResult case class 55 | - Make the existing ReverseActorSpec pass. 56 | - create the child reverse actor in the Receptionist 57 | - remove the direct implementation and delegate to the ReverseActor. 58 | 59 | The Receptionist receives a ReverseRequest which needs to be converted to the message that the ReverseActor understands. 60 | 61 | - use the ask pattern, mapTo a typed future 62 | - run the tests, all tests should pass 63 | 64 | Run the application 65 | 66 | - Run the application by using 'sbt run' from the project root (this starts the spray-can server on localhost:8000) 67 | - Test with httpie (or curl or your fav HTTP command line tool) 68 | The example below shows a call using httpie: 69 | 70 | http POST localhost:8000/reverse value="reverse this\\!" 71 | 72 | 73 | ### Next Exercise 74 | A better way to create the child **ReverseActor** for easier unit testing of the Receptionist. Go to [Exercise 2](https://github.com/RayRoestenburg/scala-io-exercise-2) 75 | -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | Setting up the environment 2 | ----------------------------------- 3 | 4 | * Install git 5 | * Clone example project: 6 | ``` 7 | git clone https://github.com/RayRoestenburg/scala-io-exercise-1.git 8 | git clone https://github.com/RayRoestenburg/scala-io-exercise-2.git 9 | git clone https://github.com/RayRoestenburg/scala-io-exercise-3.git 10 | git clone https://github.com/RayRoestenburg/scala-io-exercise-4.git 11 | git clone https://github.com/RayRoestenburg/scala-io-exercise-5.git 12 | ``` 13 | 14 | * Install sbt. Please use version 0.13.5. See http://www.scala-sbt.org/release/docs/Getting-Started/Setup.html 15 | * Configure SBT, which runs on the JVM, to have some more memory than the default 64Mb: 16 | ``` 17 | export SBT_OPTS = "-Dfile.encoding=UTF8 -Xmx2048M -XX:MaxPermSize=1024m -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled " 18 | ``` 19 | The MaxPermSize should be omitted when using Java 8. 20 | * Install your IDE/Editor of choice: 21 | * IntelliJ 22 | * (download from https://www.jetbrains.com/idea/download/) 23 | * Setup sbt-idea plugin globally: 24 | Add this code to your `~/.sbt/0.13/plugins/plugins.sbt` file: 25 | ``` 26 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") 27 | ``` 28 | * Sublime Text 3 29 | * (download from http://www.sublimetext.com/3) 30 | * Setup sbt-sublime plugin globally: 31 | Add this code to your `~/.sbt/0.13/plugins/plugins.sbt` file: 32 | ``` 33 | addSbtPlugin("com.orrsella" % "sbt-sublime" % "1.0.9") 34 | ``` 35 | * Scala IDE (Eclipse) 36 | * (download from http://scala-ide.org/) 37 | * Setup sbt-eclipse plugin globally: 38 | Add this code to your `~/.sbt/0.13/plugins/plugins.sbt` file: 39 | ``` 40 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") 41 | ``` 42 | * Start `sbt` in the project directory 43 | * Generate a project for your IDE/Editor of choice: 44 | * Type `gen-idea` to generate Intellij IDEA project 45 | * Type `gen-sublime` to generate Sublime Text project 46 | * Type `eclipse` to generate Eclipse project 47 | * Run `test` in sbt to get started 48 | * Verify that the tests work 49 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "exercise-1" 2 | 3 | organization := "xebia" 4 | 5 | version := "1.0" 6 | 7 | scalaVersion := "2.11.1" 8 | 9 | scalacOptions := Seq("-encoding", "utf8", 10 | "-target:jvm-1.7", 11 | "-feature", 12 | "-language:implicitConversions", 13 | "-language:postfixOps", 14 | "-unchecked", 15 | "-deprecation", 16 | "-Xlog-reflective-calls" 17 | ) 18 | 19 | mainClass := Some("com.xebia.exercise1.Main") 20 | 21 | resolvers ++= Seq("Sonatype Releases" at "http://oss.sonatype.org/content/repositories/releases", 22 | "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/", 23 | "Spray Repository" at "http://repo.spray.io/", 24 | "Spray Nightlies" at "http://nightlies.spray.io/", 25 | "Base64 Repo" at "http://dl.bintray.com/content/softprops/maven") 26 | 27 | libraryDependencies ++= { 28 | val akkaVersion = "2.3.4" 29 | val sprayVersion = "1.3.1" 30 | Seq( 31 | "com.typesafe.akka" %% "akka-actor" % akkaVersion, 32 | "com.typesafe.akka" %% "akka-slf4j" % akkaVersion, 33 | "io.spray" %% "spray-caching" % sprayVersion, 34 | "io.spray" %% "spray-can" % sprayVersion, 35 | "io.spray" %% "spray-client" % sprayVersion, 36 | "io.spray" %% "spray-routing" % sprayVersion, 37 | "io.spray" %% "spray-json" % "1.2.6", 38 | "ch.qos.logback" % "logback-classic" % "1.0.12", 39 | "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test", 40 | "io.spray" %% "spray-testkit" % sprayVersion % "test", 41 | "org.specs2" %% "specs2" % "2.3.13" % "test" 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.5 2 | -------------------------------------------------------------------------------- /src/main/scala/com/xebia/exercise1/Main.scala: -------------------------------------------------------------------------------- 1 | package com.xebia 2 | package exercise1 3 | 4 | import akka.actor.{Props, ActorSystem} 5 | import akka.io.IO 6 | 7 | import spray.can.Http 8 | import spray.can.Http.Bind 9 | 10 | object Main extends App { 11 | 12 | implicit val system = ActorSystem("exercise-1") 13 | 14 | val receptionist = system.actorOf(Props[Receptionist], "receptionist") 15 | 16 | IO(Http) ! Bind(listener= receptionist, interface = "0.0.0.0", port=8000) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/com/xebia/exercise1/Receptionist.scala: -------------------------------------------------------------------------------- 1 | package com.xebia 2 | package exercise1 3 | 4 | import scala.concurrent.duration._ 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | import akka.util.Timeout 8 | 9 | import spray.routing._ 10 | import spray.httpx.SprayJsonSupport._ 11 | 12 | 13 | class Receptionist extends HttpServiceActor 14 | with ReverseRoute { 15 | implicit def executionContext = context.dispatcher 16 | 17 | //TODO add a createChild method which creates a child actor from a specified Props and name 18 | 19 | def receive = runRoute(reverseRoute) 20 | } 21 | 22 | trait ReverseRoute extends HttpService { 23 | // we need this so we can use Futures and Timeout 24 | implicit def executionContext: ExecutionContext 25 | 26 | //TODO define a val that returns the one ActorRef to the reverse actor using the createChild method 27 | 28 | def reverseRoute: Route = path("reverse") { 29 | post { 30 | entity(as[ReverseRequest]) { request => 31 | implicit val timeout = Timeout(20 seconds) 32 | 33 | import akka.pattern.ask 34 | 35 | //TODO replace the next line by asking the actor to Reverse 36 | //and converting (hint: mapping) the resulting Future[ReverseResult] to a Future[ReverseResponse] 37 | val futureResponse = Future.successful(ReverseResponse(request.value.reverse)) 38 | 39 | complete(futureResponse) 40 | } 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/main/scala/com/xebia/exercise1/ReverseActor.scala: -------------------------------------------------------------------------------- 1 | package com.xebia 2 | package exercise1 3 | 4 | import akka.actor.{Actor, Props} 5 | 6 | object ReverseActor { 7 | //TODO define messages for reverse actor here (Reverse, ReverseResult) 8 | //TODO define props and name for ReverseActor here 9 | } 10 | 11 | class ReverseActor { // TODO extend from Actor 12 | import ReverseActor._ 13 | 14 | //TODO write your receive method here, respond with a ReverseResult 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/xebia/exercise1/ReverseRequest.scala: -------------------------------------------------------------------------------- 1 | package com.xebia 2 | package exercise1 3 | 4 | import spray.json.DefaultJsonProtocol 5 | 6 | case class ReverseRequest(value:String) 7 | 8 | object ReverseRequest extends DefaultJsonProtocol { 9 | implicit val format = jsonFormat1(ReverseRequest.apply) 10 | } 11 | 12 | case class ReverseResponse(value:String) 13 | 14 | object ReverseResponse extends DefaultJsonProtocol { 15 | implicit val format = jsonFormat1(ReverseResponse.apply) 16 | } 17 | -------------------------------------------------------------------------------- /src/test/scala/com/xebia/exercise1/ReceptionistSpec.scala: -------------------------------------------------------------------------------- 1 | package com.xebia 2 | package exercise1 3 | 4 | import akka.actor.ActorRefFactory 5 | 6 | import spray.testkit.Specs2RouteTest 7 | import spray.http.StatusCodes 8 | import spray.httpx.SprayJsonSupport._ 9 | 10 | import org.specs2.mutable.Specification 11 | 12 | 13 | class ReceptionistSpec extends Specification 14 | with Specs2RouteTest { 15 | 16 | val subject = new ReverseRoute { 17 | implicit def actorRefFactory: ActorRefFactory = system 18 | implicit def executionContext = system.dispatcher 19 | 20 | //TODO implement createChild here as well (hint: you cannot use the context since the context is not available here) 21 | } 22 | 23 | "The Receptionist" should { 24 | "Respond with a JSON response that contains a reversed string value" in { 25 | 26 | Post("/reverse", ReverseRequest("some text to reverse")) ~> subject.reverseRoute ~> check { 27 | status === StatusCodes.OK 28 | val response = responseAs[ReverseResponse] 29 | response.value must beEqualTo("esrever ot txet emos") 30 | } 31 | 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/scala/com/xebia/exercise1/ReverseActorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.xebia 2 | package exercise1 3 | 4 | import org.specs2.mutable.Specification 5 | 6 | import spray.testkit.Specs2RouteTest 7 | 8 | import TestSupport._ 9 | 10 | class ReverseActorSpec extends Specification { 11 | 12 | "The ReverseActor" should { 13 | "Reverse a string that it receives" in new AkkaTestkitContext() { 14 | // You can use the TestKit here, it is provided by the AkkaTestkitContext 15 | // The ImplicitSender is also available so you can expect responses to be sent to the testActor 16 | // which can be asserted with the TestKit expect... methods. 17 | // 18 | 19 | import ReverseActor._ 20 | 21 | //TODO make this test work, uncomment the commented lines below 22 | 23 | // val reverseActor = system.actorOf(props, name) 24 | // 25 | // reverseActor ! Reverse("reverse this!") 26 | // 27 | // expectMsg(ReverseResult("!siht esrever")) 28 | 29 | expectNoMsg() 30 | 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/scala/com/xebia/exercise1/TestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.xebia.exercise1 2 | 3 | import akka.actor.ActorSystem 4 | import akka.testkit.{ImplicitSender, TestKit} 5 | 6 | import org.specs2.specification.After 7 | 8 | object TestSupport { 9 | 10 | /** Simple specs2 bridge for Akka TestKit. */ 11 | abstract class AkkaTestkitContext(actorSystem:ActorSystem) extends TestKit(actorSystem) with ImplicitSender with After { 12 | private var owner = false 13 | 14 | def this() = { 15 | this(ActorSystem()) 16 | owner = true 17 | } 18 | 19 | def after { 20 | if(owner) system.shutdown() 21 | } 22 | } 23 | } 24 | --------------------------------------------------------------------------------