diff --git a/.gitignore b/.gitignore index 2704fde..afaa680 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target/ +.idea boot/ lib_managed/ src_managed/ diff --git a/docs/src/main/paradox/backend-actor.md b/docs/src/main/paradox/backend-actor.md new file mode 100644 index 0000000..2bdbe56 --- /dev/null +++ b/docs/src/main/paradox/backend-actor.md @@ -0,0 +1,15 @@ +Backend Actor logic +------------------- + +In this example, the backend only uses one basic actor. In a real system, we would have many actors interacting with each other and perhaps, multiple data stores and microservices. + +An interesting side-note to add here is perhaps about when using actors in applications like this adds value over just providing functions that would return Futures. +In fact, if your logic is stateless and very simple request/reply style, you may not need to back it with an Actor. actors do shine however when you need to keep some form of state and allow various requests to access something in (or *through*) an Actor. The other stellar feature of actors, that futures would not handle, is scaling-out onto a cluster very easily, by using [Cluster Sharding](http://doc.akka.io/docs/akka/current/scala/cluster-sharding.html) or other [location-transparent](http://doc.akka.io/docs/akka/current/scala/general/remoting.html) techniques. + +However, the focus of this tutorial is on how to interact with an Actor backend from within Akka HTTP -- not on the actor itself, so we'll keep it very simple. + +The sample code in the `UserRegistryActor` is very simple. It keeps registered users in a `Set`. Once it receives messages it matches them to the defined cases to determine which action to take: + +@@snip [UserRegistryActor.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala) + +If you feel you need to brush up on your Akka Actor knowledge, the [Getting Started Guide]((http://doc.akka.io/docs/akka/current/scala/guide/index.html)) reviews actor concepts in the context of a simple Internet of Things (IoT) example. diff --git a/docs/src/main/paradox/backend.md b/docs/src/main/paradox/backend.md deleted file mode 100644 index c0505a9..0000000 --- a/docs/src/main/paradox/backend.md +++ /dev/null @@ -1,10 +0,0 @@ -Backend logic -------------- - -In this example, the backend only uses one basic actor. In a real system, we would have many actors interacting with each other and perhaps, multiple data stores and microservices. However, the focus of this tutorial is on how to interact with a backend from within Akka HTTP -- not on the actor itself. - -The sample code in the `UserRegistryActor` is very simple. It keeps registered users in a `Set`. Once it receives messages it matches them to the defined cases to determine which action to take: - -@@snip [UserRegistryActor.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala) - -If you feel you need to brush up on your Akka Actor knowledge, the [Getting Started Guide]((http://doc.akka.io/docs/akka/current/scala/guide/index.html)) reviews actor concepts in the context of a simple Internet of Things (IoT) example. diff --git a/docs/src/main/paradox/server-class.md b/docs/src/main/paradox/http-server.md similarity index 57% rename from docs/src/main/paradox/server-class.md rename to docs/src/main/paradox/http-server.md index b4957fd..6649b44 100644 --- a/docs/src/main/paradox/server-class.md +++ b/docs/src/main/paradox/http-server.md @@ -1,23 +1,33 @@ -Server logic ------------- +HTTP Server logic +----------------- -The main class, `QuickstartServer`, is runnable because it extends `App`, as shown in the following snippet. We will discuss the trait `JsonSupport` later. +The main class, `QuickstartServer`, is runnable because it extends `App`, as shown in the following snippet. +This class is intended to "bring it all together", it is the main class that will run the application, as well +as the class that should bootstrap all actors and other dependencies (database connections etc). @@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #main-class } +Notice that we've separated out the `UserRoutes` trait, in which we'll put all our actual route definitions. +This is a good pattern to follow, especially once your application starts to grow and you'll need some form of +compartmentalizing them into groups of routes handling specific parts of the exposed API. + + ## Binding endpoints Each Akka HTTP `Route` contains one or more `akka.http.scaladsl.server.Directives`, such as: `path`, `get`, `post`, `complete`, etc. There is also a [low-level API](http://doc.akka.io/docs/akka-http/current/scala/http/low-level-server-side-api.html) that allows to inspect requests and create responses manually. For the user registry service, the example needs to support the actions listed below. For each, we can identify a path, the HTTP method, and return value: -|Functionality | Path | HTTP method | Returns | -|--------------------|------------|-----------------|----------------------| -| Create a user | /users | POST | Confirmation message | -| Retrieve a user | /users/$ID | GET | JSON payload | -| Remove a user | /users/$ID | DELETE | Confirmation message | -| Retrieve all users | /users | GET | JSON payload | +| Functionality | HTTP Method | Path | Returns | +|--------------------|-------------|------------|----------------------| +| Create a user | POST | /users | Confirmation message | +| Retrieve a user | GET | /users/$ID | JSON payload | +| Remove a user | DELETE | /users/$ID | Confirmation message | +| Retrieve all users | GET | /users | JSON payload | + +In the `QuickstartServer` source file, the definition of the `Route` delegates to the routes defined in `UserRoutes`: +`lazy val routes: Route = userRoutes`. -In the `QuickstartServer` source file, the definition of the `Route` begins with the line: -`lazy val routes: Route =`. +In larger applications this is where we'd combine the various routes of our application into a big route that is concatenating +the various routes of our services. We'd do this using the concat directive like this: `val route = concat(userRoutes, healthCheckRoutes, ...)` Let's look at the pieces of the example `Route` that bind the endpoints, HTTP methods, and message or payload for each action. @@ -25,15 +35,23 @@ Let's look at the pieces of the example `Route` that bind the endpoints, HTTP me The definition of the endpoint to retrieve and create users look like the following: -@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #users-get-post } +@@snip [UserRoutes.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRoutes.scala) { #users-get-post } +A Route is constructed by nesting various *directives* which route an incoming request to the apropriate handler block. Note the following building blocks from the snippet: **Generic functionality** +The following directives are used in the above example: + * `pathPrefix("users")` : the path that is used to match the incoming request against. * `pathEnd` : used on an inner-level to discriminate “path already fully matched” from other alternatives. Will, in this case, match on the "users" path. -* `~`: concatenates two or more route alternatives. Routes are attempted one after another. If a route rejects a request, the next route in the chain is attempted. This continues until a route in the chain produces a response. If all route alternatives reject the request, the concatenated route rejects the route as well. In that case, route alternatives on the next higher level are attempted. If the root level route rejects the request as well, then an error response is returned that contains information about why the request was rejected. +* `concat`: concatenates two or more route alternatives. Routes are attempted one after another. If a route rejects a request, the next route in the chain is attempted. This continues until a route in the chain produces a response. If all route alternatives reject the request, the concatenated route rejects the route as well. In that case, route alternatives on the next higher level are attempted. If the root level route rejects the request as well, then an error response is returned that contains information about why the request was rejected. + * This can also be achieved using the `~` operator, like this: `exampleRoute ~ anotherRoute`. + However this method is slightly more error-prone since forgetting to add the `~` between routes in subsequent lines + will not result in a compile error (as it would when using the `concat` directive) resulting in only the "last" route to be returned.
+
+ In short other words: you may see the `~` operator used in Akka HTTP apps, however it is recommended to use the `concat` directive as safer alternative. **Retrieving users** @@ -44,30 +62,32 @@ Note the following building blocks from the snippet: * `post` : matches against `POST` HTTP method. * `entity(as[User])` : converts the HTTP request body into a domain object of type User. Implicitly, we assume that the request contains application/json content. We will look at how this works in the @ref:[JSON](json.md) section. -* `complete` : completes a request which means creating and returning a response from the arguments. Note, how the tuple (StatusCodes.Created, "...") of type (StatusCode, String) is implicitly converted to a response with the given status code and a text/plain body with the given string. +* `complete` : completes a request which means creating and returning a response from the arguments. Note, how the tuple `(StatusCodes.Created, "...")` of type `(StatusCode, String)` is implicitly converted to a response with the given status code and a text/plain body with the given string. ### Retrieving and removing a user Next, the example defines how to retrieve and remove a user. In this case, the URI must include the user's id in the form: `/users/$ID`. See if you can identify the code that handles that in the following snippet. This part of the route includes logic for both the GET and the DELETE methods. -@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #users-get-delete } +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRoutes.scala) { #users-get-delete } This part of the `Route` contains the following: **Generic functionality** +The following directives are used in the above example: + * `pathPrefix("users")` : the path that is used to match the incoming request against. +* `concat`: concatenates two or more route alternatives. Routes are attempted one after another. If a route rejects a request, the next route in the chain is attempted. This continues until a route in the chain produces a response. * `path(Segment) { => user` : this bit of code matches against URIs of the exact format `/users/$ID` and the `Segment` is automatically extracted into the `user` variable so that we can get to the value passed in the URI. For example `/users/Bruce` will populate the `user` variable with the value "Bruce." There is plenty of more features available for handling of URIs, see [pattern matchers](http://doc.akka.io/docs/akka-http/current/scala/http/routing-dsl/path-matchers.html#basic-pathmatchers) for more information. -* `~`: concatenates two or more route alternatives. Routes are attempted one after another. If a route rejects a request, the next route in the chain is attempted. This continues until a route in the chain produces a response. If all route alternatives reject the request, the concatenated route rejects the route as well. In that case, route alternatives on the next higher level are attempted. If the root level route rejects the request as well, then an error response is returned that contains information about why the request was rejected. **Retrieving a user** * `get` : matches against `GET` HTTP method. * `complete` : completes a request which means creating and returning a response from the arguments. -Let's break down the "business logic": +Let's break down the logic handling the incoming request: -@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #retrieve-user-info } +@@snip [UserRoutes.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRoutes.scala) { #retrieve-user-info } The `rejectEmptyResponse` here above is a convenience method that automatically unwraps a future, handles an `Option` by converting `Some` into a successful response, returns a HTTP status code 404 for `None`, and passes on to the `ExceptionHandler` in case of an error, which returns the HTTP status code 500 by default. @@ -75,13 +95,21 @@ The `rejectEmptyResponse` here above is a convenience method that automatically * `delete` : matches against the Http directive `DELETE`. -The "business logic" for when deleting a user is straight forward; send an instruction about removing a user to the user registry actor, wait for the response and return an appropriate HTTP status code to the client. +The logic for handling delete requests is as follows: + +@@snip [UserRoutes.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRoutes.scala) { #users-delete-logic } + +So we send an instruction about removing a user to the user registry actor, wait for the response and return an appropriate HTTP status code to the client. + ## The complete Route Below is the complete `Route` definition from the sample application: -@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #all-routes } +@@snip [UserRoutes.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRoutes.scala) { #all-routes } + +Note that one might want to separate those routes into smaller route values and `concat` them together into the `userRoutes` +value - in a similar fashion like we do in the `QuickstartServer` leading to a bit less "dense" code. ## Binding the HTTP server diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 6877f63..0c4fff8 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -175,7 +175,8 @@ Congratulations, you just ran and exercised your first Akka HTTP app! You got a The example is implemented in the following three source files: -* `QuickstartServer.scala` -- contains the main class and Akka HTTP `routes`. +* `QuickstartServer.scala` -- contains the main class which sets-up and all actors, it runs the Akka HTTP `routes`. +* `UserRoutes.scala` -- contains Akka HTTP `routes` that the Server will serve. * `UserRegistryActor.scala` -- implements the actor that handles registration. * `JsonSupport.scala` -- converts the JSON data from requests into Scala types and from Scala types into JSON responses. @@ -183,10 +184,11 @@ First, let's dissect the backend logic. @@@index -* [The backend](backend.md) -* [The server](server-class.md) +* [The Actor backend](backend-actor.md) +* [The HTTP server](http-server.md) * [JSON](json.md) * [Running the application](running-the-application.md) +* [Testing Routes](testing-routes.md) * [IntelliJ IDEA](intellij-idea.md) @@@ diff --git a/docs/src/main/paradox/json.md b/docs/src/main/paradox/json.md index 69c2689..1a6aa12 100644 --- a/docs/src/main/paradox/json.md +++ b/docs/src/main/paradox/json.md @@ -1,22 +1,40 @@ -JSON conversion ---------------- +JSON marshalling +---------------- When exercising the app, you interacted with JSON payloads. How does the example app convert data between JSON format and data that can be used by Scala classes? The answer begins in the server class definition `JsonSupport` trait: -@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #main-class } +@@snip [UserRoutes.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRoutes.scala) { #user-routes-class } This trait is implemented in the `JsonSupport.scala` source file: @@snip [JsonSupport.scala]($g8src$/scala/com/lightbend/akka/http/sample/JsonSupport.scala) +We're using the [Spray JSON](https://github.com/spray/spray-json) library here, which allows us to define json marshallers +(or `formats` how Spray JSON calls them) in a type-safe way. In other words, if we don't provide a format instance for +a type, yet we'd try to return it in a route by calling `complete(someValue)` the code would not compile - saying that +it does not know how to marshal the `SomeValue` type. This has the up-side of us being completely in control over what +we want to expose, and not exposing some type accidentally in our API. + To handle the two different payloads, the trait defines two implicit values; `userJsonFormat` and `usersJsonFormat`. Defining the formatters as `implicit` ensures that the compiler can map the formatting functionality with the case classes to convert. -The `jsonFormatX` methods come from [Spray Json](https://github.com/spray/spray-json). The `X` represents the number of parameters in the underlying case classes: +The `jsonFormatX` methods come from Spray JSON. The `X` represents the number of parameters in the underlying case classes: @@snip [UserRegistryActor.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala) { #user-case-classes } -We won't go into how the formatters are implemented. All you need to remember for now is to define the formatters as implicit and that the formatter used should map the number of parameters belonging to the case class it converts. - -Comment: I was a bit confused by the previous paragraph. Does the user have to write their own formatters or are these available as libraries? +We won't go into how the formatters are implemented - this is done for us by the library. All you need to remember for now is to define the formatters as implicit and that the formatter used should map the number of parameters belonging to the case class it converts. + +@@@ note + + While we used Spray JSON in this example, various other libraries are supported via the [Akka HTTP JSON](https://github.com/hseeberger/akka-http-json) + project, including [Jackson](https://github.com/FasterXML/jackson), [Play JSON](https://www.playframework.com/documentation/2.6.x/ScalaJson) + or [circe](https://circe.github.io/circe/). + + Each library comes with different trade-offs in performance and user-friendlieness. Spray JSON is generally the fastest, though it requires you to write the format + values explicitly. If you'd rather make "everything" automatically marshallable into JSON values you might want to use Jackson or Circe instead. + + If you're not sure, we recommend sticking to Spray JSON as it's the closest in philosophy to Akka HTTP - being explicit about all capabilities. + +@@@ Now that we've examined the example app thoroughly, let's test a few the remaining use cases. + diff --git a/docs/src/main/paradox/running-the-application.md b/docs/src/main/paradox/running-the-application.md index d58681f..d5fe205 100644 --- a/docs/src/main/paradox/running-the-application.md +++ b/docs/src/main/paradox/running-the-application.md @@ -1,5 +1,5 @@ -Final testing -------------- +Running the application +----------------------- When you ran the example for the first time, you were able to create and retrieve multiple users. Now that you understand how the example is implemented, let's confirm that the rest of the functionality works. We want to verify that: @@ -9,21 +9,21 @@ When you ran the example for the first time, you were able to create and retriev To test this functionality, follow these steps. If you need reminders on starting the app or sending requests, refer to the @ref:[instructions](index.md#exercising-the-example) in the beginning. -1. If the Akka HTTP server is still running, stop and restart it. -2. With no users registered, use your tool of choice to: -3. Retrieve a list of users. Hint: use the `GET` method and append `/users` to the URL. +`1.` If the Akka HTTP server is still running, stop and restart it. +`2.` With no users registered, use your tool of choice to: +`3.` Retrieve a list of users. Hint: use the `GET` method and append `/users` to the URL. You should get back an empty list: `{"users":[]}` -4. Try to retrieve a single user named `MrX`. Hint: use the `GET` method and append `users/MrX` to the URL. +`4.` Try to retrieve a single user named `MrX`. Hint: use the `GET` method and append `users/MrX` to the URL. You should get back the message: `User MrX is not registered.` -5. Try adding one or more users. Hint: use the `POST` method, append `/users` to the URL, and format the data in JSON, similar to: `{"name":"MrX","age":31,"countryOfResidence":"Canada"}` +`5.` Try adding one or more users. Hint: use the `POST` method, append `/users` to the URL, and format the data in JSON, similar to: `{"name":"MrX","age":31,"countryOfResidence":"Canada"}` You should get back the message: `User MrX created.` -6. Try deleting a user you just added. Hint: use the `DELETE`, and append `/users/` to the URL. +`6.` Try deleting a user you just added. Hint: use the `DELETE`, and append `/users/` to the URL. You should get back the message: `User MrX deleted.` diff --git a/docs/src/main/paradox/testing-routes.md b/docs/src/main/paradox/testing-routes.md new file mode 100644 index 0000000..4151e63 --- /dev/null +++ b/docs/src/main/paradox/testing-routes.md @@ -0,0 +1,104 @@ +Testing routes +-------------- + +If you remember when we started out with our `QuickstartServer`, we decided to put the routes themselves into a separate +trait. Back there we said that we're doing this to eparate the infrastructure code (setting up the actor system and +wiring up all the dependencies and actors), from the routes, which should only declare what they need to work with, +and can therefore be a bit more focused on their task at hand. This of course leads us to better testability. + +This separation, other than being a good idea on its own, was all for this moment! For when we want to write tests +to cover all our routes, without having to bring up the entire application. + +## Unit testing routes + +There are multiple ways one can test an HTTP application of course, however lets start at the simplest and also quickest +way: unit testing. In this style of testing, we won't even need to spin up an actual server - all the tests will be +executed on the routes directly - without the need of hitting actual network. This is due to Akka HTTP's pure design +and separation between the network layer (represented as a bi-directional `Flow` of byte strings to Http domain objects). + +In other words, unit testing in Akka HTTP is simply "executing" the routes by passing in an `HttpResponse` to the route, +and later inspecting what `HttpResponse` (or `rejection` if the request could not be handled) it resulted in. All this +in-memory, without having to start a real HTTP server - which gives us supreme speed and turn-over time when developing +an application using Akka. + +First we'll need to extend a number of base traits: + +@@snip [QuickstartServer.scala]($g8srctest$/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala) { #test-top } + +Here we're using ScalaTest which provides the testing *style* `WordSpec` and the `Matchers` trait which provides +the `something should === (somethingElse)` syntax [and more](http://www.scalatest.org/user_guide/using_matchers). +Next we inherit the Akka HTTP provided `ScalatestRouteTest` bridge trait that provides Route specific testing facilities, +and binds into ScalaTest's lifecycle methods such that the `ActorSystem` is started and stopped automatically for us. + + +@@@ note + +If you're using Specs2 instead, you can simply extend the `Specs2RouteTest` support trait instead. + +@@@ + + +Next we'll need to bring into the test class our routes that we want to test. We're doing this by extending the `UserRoutes` trait in the spec itself - this allows us to bring all marshallers into scope for the tests to use, as well as makes it possible to implement all abstract members of that trait in the test itself - all in in a fully type-safe way. + +We'll need to provide it with an `ActorSystem`, which is done by the fact that the `ScalatestRouteTest` trait +already provides a field called `system: ActorSystem`. Next we need to implement the `userRegistryActor: ActorRef` that the routes are interacting with we'll create a TestProbe instead - which will allow us to verify the route indeed did send a message do the Actor or not etc. + +@@snip [QuickstartServer.scala]($g8srctest$/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala) { #set-up } + +We could create an actor that replies with a mocked response here instead if we wanted to, this is especially useful if +the route awaits an response from the actor before rendering the `HttpResponse` to the client. Read about the [Akka TestKit ](http://doc.akka.io/docs/akka/current/scala/testing.html) and it's utilities like `TestProbe` if this is something you'd like to learn more about. + +Let's write our first test, in which we'll hit the `/users` endpoint with a `GET` request: + +@@snip [QuickstartServer.scala]($g8srctest$/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala) { #actual-test } + +We simply construct a raw `HttpRequest` object and pass it into the route using the `~>` testing operator provided by `ScalatestRouteTest`. Next we do the same and pipe the result of that route into a check block, so the full syntax is: +`request ~> route ~> check { }`. This syntax allows us to not worry about the asynchronous nature of the request handling. +After all, the route is a function of `HttpRequest => Future[HttpResponse]` - here we don't need to explicitly write code +that's awaiting on the response, it's handled for us. + +Inside the check block we can inspect all kinds of attributes of the received response, like `status`, `contentType` and +of course the full response which we can easily convert to a string for testing using `responseAs[String]`. This infrastructure +is using the same marshalling infrastructure as our routes, so if the response was a `User` JSON, we could say `responseAs[User]` and write our assertions on the actual object. + +In the next test we'd like test a `POST` endpoint, so we need to send an entity to the endpoint in order to create a new `User`. This time, instead of using the raw `HttpRequest` to build the request we'll use a small DSL provided by the Akka HTTP. The DSL allows you to write `Post("/hello)` instead of having to declare the full thing in the raw API (which would have been: `HttpRequest(method = HttpMethods.POST, uri = "/hello")`), and next we'll add the User JSON into the request body: + +@@snip [QuickstartServer.scala]($g8srctest$/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala) { #testing-post } + +So in order to add the entity we've used the `Marshal(object).to[TargetType]` syntax, which uses the same marshalling +infrastructure that is used when we `complete(object)`. Since we extend the `UserRoutes` trait in this test, all the +necessary implicits for the marshalling to work this way are also present in scope of the test. This is another reason +why it's so convenient to extend the Routes trait when testing it - everything the actual code was using, we also have at +our disposal when writing the test. + +This concludes the basics of unit testing HTTP routes, to learn more please refer to the +[Akka HTTP TestKit documentation](). + +### Complete unit unit test code listing + +For reference, here's the entire unit test code: + +@@snip [QuickstartServer.scala]($g8srctest$/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala) + + +## A note Integration testing routes + +While definitions of "what a pure unit-test is" are sometimes a subject of fierce debates in programming communities, +we refer to the above testing style as "route unit testing" since it's light weight and allows to test the routes in +isolation, especially if their dependencies would be mocked our with test stubs, instead of hitting real APIs. + +Sometimes however one wants to test the complete "full application", including starting a real HTTP server + +@@@ warning + + Some network specific features like timeouts, behaviour of entities (streamed directly from the network, instead of + in memory objects like in the unit testing style) may behave differently in the unit-testing style showcased above. + + If you want to test specific timing and entity draining behaviours of your apps you may want to add full integration tests for them. For most routes this should not be needed, however we'd recommend doing so when using more of the streaming features of Akka HTTP. + +@@@ + +Usually such tests would be implemented by starting the application the same way as we started it in the `QuickstartServer`, +in `beforeAll` (in ScalaTest), then hitting the API with http requests using the HTTP Client and asserting on the responses, +finally shutting down the server in `afterAll` (in ScalaTest). + diff --git a/src/main/g8/.gitignore b/src/main/g8/.gitignore new file mode 100644 index 0000000..818019b --- /dev/null +++ b/src/main/g8/.gitignore @@ -0,0 +1,2 @@ +*.class +.idea diff --git a/src/main/g8/build.sbt b/src/main/g8/build.sbt index c286488..4af7219 100644 --- a/src/main/g8/build.sbt +++ b/src/main/g8/build.sbt @@ -11,9 +11,9 @@ lazy val root = (project in file(".")). libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-http-xml" % akkaHttpVersion, "com.typesafe.akka" %% "akka-stream" % akkaVersion, - "com.typesafe.akka" %% "akka-http-xml" % akkaHttpVersion, - "com.typesafe.akka" %% "akka-stream" % akkaVersion, + "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test, "org.scalatest" %% "scalatest" % "3.0.1" % Test ) diff --git a/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/JsonSupport.scala b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/JsonSupport.scala index ffebd52..e9208e7 100644 --- a/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/JsonSupport.scala +++ b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/JsonSupport.scala @@ -1,10 +1,14 @@ package com.lightbend.akka.http.sample import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import spray.json.DefaultJsonProtocol +import com.lightbend.akka.http.sample.UserRegistryActor.ActionPerformed +import spray.json.{ DefaultJsonProtocol, RootJsonFormat } trait JsonSupport extends SprayJsonSupport { - import DefaultJsonProtocol._ + import DefaultJsonProtocol._ // import the default encoders for primitive types (Int, String, Lists etc) + implicit val userJsonFormat = jsonFormat3(User) implicit val usersJsonFormat = jsonFormat1(Users) + + implicit val actionPerformedJsonFormat = jsonFormat1(ActionPerformed) } diff --git a/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/QuickstartServer.scala b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/QuickstartServer.scala index 45b5579..948cd11 100644 --- a/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/QuickstartServer.scala +++ b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/QuickstartServer.scala @@ -1,30 +1,21 @@ package com.lightbend.akka.http.sample import akka.actor.{ ActorRef, ActorSystem } -import akka.pattern.ask -import akka.util.Timeout - -import scala.concurrent.duration._ -import akka.stream.ActorMaterializer import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.Http.ServerBinding -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.{ ExceptionHandler, Route } -import akka.http.scaladsl.server.directives.MethodDirectives.delete -import akka.http.scaladsl.server.directives.MethodDirectives.get -import akka.http.scaladsl.server.directives.MethodDirectives.post -import akka.http.scaladsl.server.directives.RouteDirectives.complete -import akka.http.scaladsl.server.directives.PathDirectives.path +import akka.http.scaladsl.server.Route +import akka.stream.ActorMaterializer +import akka.util.Timeout -import scala.concurrent.ExecutionContext -import scala.concurrent.Future +import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.duration._ import scala.io.StdIn -import scala.util.{ Failure, Success } -import com.lightbend.akka.http.sample.UserRegistryActor._ //#main-class -object QuickstartServer extends App with JsonSupport { +object QuickstartServer extends App + with UserRoutes { + + // set up ActorSystem and other dependencies here //#main-class //#server-bootstrapping implicit val system: ActorSystem = ActorSystem("helloAkkaHttpServer") @@ -36,56 +27,10 @@ object QuickstartServer extends App with JsonSupport { val userRegistryActor: ActorRef = system.actorOf(UserRegistryActor.props, "userRegistryActor") - // Required by the `ask` (?) method below - implicit val timeout = Timeout(5 seconds) - - //#all-routes + //#main-class lazy val routes: Route = - //#users-get-post - //#users-get-delete - pathPrefix("users") { - //#users-get-delete - pathEnd { - get { - val users: Future[Users] = (userRegistryActor ? GetUsers).mapTo[Users] - complete(users) - } ~ - post { - entity(as[User]) { user => - val userCreated: Future[ActionPerformed] = (userRegistryActor ? CreateUser(user)).mapTo[ActionPerformed] - onComplete(userCreated) { r => - r match { - case Success(ActionPerformed(description)) => complete((StatusCodes.Created, description)) - case Failure(ex) => complete((StatusCodes.InternalServerError, ex)) - } - } - } - } - } ~ - //#users-get-post - //#users-get-delete - path(Segment) { name => - get { - //#retrieve-user-info - val maybeUser: Future[Option[User]] = (userRegistryActor ? GetUser(name)).mapTo[Option[User]] - rejectEmptyResponse { - complete(maybeUser) - } - //#retrieve-user-info - } ~ - delete { - val userDeleted: Future[ActionPerformed] = (userRegistryActor ? DeleteUser(name)).mapTo[ActionPerformed] - onComplete(userDeleted) { r => - r match { - case Success(ActionPerformed(description)) => complete((StatusCodes.OK, description)) - case Failure(ex) => complete((StatusCodes.InternalServerError, ex)) - } - } - } - } - //#users-get-delete - } - //#all-routes + userRoutes // from the UserRoutes trait + //#main-class //#http-server val serverBindingFuture: Future[ServerBinding] = Http().bindAndHandle(routes, "localhost", 8080) @@ -93,7 +38,10 @@ object QuickstartServer extends App with JsonSupport { StdIn.readLine() serverBindingFuture .flatMap(_.unbind()) - .onComplete(_ => system.terminate()) + .onComplete { done => + done.failed.map { ex => log.error(ex, "Failed unbinding") } + system.terminate() + } //#http-server //#main-class } diff --git a/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala index 7a910ac..7a667eb 100644 --- a/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala +++ b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala @@ -1,11 +1,10 @@ package com.lightbend.akka.http.sample import akka.actor.{ Actor, ActorLogging, Props } -import scala.collection.mutable.Set //#user-case-classes -case class User(name: String, age: Int, countryOfResidence: String) -case class Users(users: Seq[User]) +final case class User(name: String, age: Int, countryOfResidence: String) +final case class Users(users: Seq[User]) //#user-case-classes object UserRegistryActor { @@ -21,18 +20,18 @@ object UserRegistryActor { class UserRegistryActor extends Actor with ActorLogging { import UserRegistryActor._ - val users: Set[User] = Set.empty[User] + var users = Set.empty[User] def receive = { case GetUsers => - sender ! Users(users.toSeq) + sender() ! Users(users.toSeq) case CreateUser(user) => users += user - sender ! ActionPerformed(s"User ${user.name} created.") + sender() ! ActionPerformed(s"User ${user.name} created.") case GetUser(name) => - sender ! users.find(_.name == name) + sender() ! users.find(_.name == name) case DeleteUser(name) => - users.find(_.name == name) map { user => users -= user } - sender ! ActionPerformed(s"User ${name} deleted.") + users.find(_.name == name) foreach { user => users -= user } + sender() ! ActionPerformed(s"User ${name} deleted.") } } diff --git a/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRoutes.scala b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRoutes.scala new file mode 100644 index 0000000..1168a7d --- /dev/null +++ b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRoutes.scala @@ -0,0 +1,96 @@ +package com.lightbend.akka.http.sample + +import akka.actor.{ ActorRef, ActorSystem } +import akka.event.Logging + +import scala.concurrent.duration._ +import akka.stream.ActorMaterializer +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.Http.ServerBinding +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server.{ ExceptionHandler, Route } +import akka.http.scaladsl.server.directives.MethodDirectives.delete +import akka.http.scaladsl.server.directives.MethodDirectives.get +import akka.http.scaladsl.server.directives.MethodDirectives.post +import akka.http.scaladsl.server.directives.RouteDirectives.complete +import akka.http.scaladsl.server.directives.PathDirectives.path + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.util.{ Failure, Success } +import com.lightbend.akka.http.sample.UserRegistryActor._ +import akka.pattern.ask +import akka.util.Timeout + +//#user-routes-class +trait UserRoutes extends JsonSupport { + //#user-routes-class + + // we leave these abstract, since they will be provided by the App + implicit def system: ActorSystem + + lazy val log = Logging(system, classOf[UserRoutes]) + + // other dependencies that UserRoutes use + def userRegistryActor: ActorRef + + // Required by the `ask` (?) method below + implicit lazy val timeout = Timeout(5.seconds) // usually we'd obtain the timeout from the system's configuration + + //#all-routes + //#users-get-post + //#users-get-delete + lazy val userRoutes = + pathPrefix("users") { + concat( + //#users-get-delete + pathEnd { + concat( + get { + val users: Future[Users] = + (userRegistryActor ? GetUsers).mapTo[Users] + complete(users) + }, + post { + entity(as[User]) { user => + val userCreated: Future[ActionPerformed] = + (userRegistryActor ? CreateUser(user)).mapTo[ActionPerformed] + onSuccess(userCreated) { performed => + log.info("Created user [{}]: {}", user.name, performed.description) + complete((StatusCodes.Created, performed)) + } + } + } + ) + }, + //#users-get-post + //#users-get-delete + path(Segment) { name => + concat( + get { + //#retrieve-user-info + val maybeUser: Future[Option[User]] = + (userRegistryActor ? GetUser(name)).mapTo[Option[User]] + rejectEmptyResponse { + complete(maybeUser) + } + //#retrieve-user-info + }, + delete { + //#users-delete-logic + val userDeleted: Future[ActionPerformed] = + (userRegistryActor ? DeleteUser(name)).mapTo[ActionPerformed] + onSuccess(userDeleted) { performed => + log.info("Deleted user [{}]: {}", name, performed.description) + complete((StatusCodes.OK, performed)) + } + //#users-delete-logic + } + ) + } + ) + //#users-get-delete + } + //#all-routes +} diff --git a/src/main/g8/src/test/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala b/src/main/g8/src/test/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala new file mode 100644 index 0000000..e2b1234 --- /dev/null +++ b/src/main/g8/src/test/scala/com/lightbend/akka/http/sample/UserRoutesSpec.scala @@ -0,0 +1,86 @@ +package com.lightbend.akka.http.sample + +//#test-top +import akka.actor.ActorRef +import akka.http.javadsl.server.Rejections +import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model._ +import akka.http.scaladsl.server.SchemeRejection +import akka.http.scaladsl.testkit.ScalatestRouteTest +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.{ Matchers, WordSpec } + +//#set-up +class UserRoutesSpec extends WordSpec with Matchers with ScalaFutures with ScalatestRouteTest + with UserRoutes { + //#test-top + + // Here we need to implement all the abstract members of UserRoutes. + // We use the real UserRegistryActor to test it while we hit the Routes, + // but we could "mock" it by implementing it in-place or by using a TestProbe() + override val userRegistryActor: ActorRef = + system.actorOf(UserRegistryActor.props, "userRegistry") + + lazy val routes = userRoutes + + //#set-up + + //#actual-test + "UserRoutes" should { + "return no users if no present (GET /users)" in { + // note that there's no need for the host part in the uri: + val request = HttpRequest(uri = "/users") + + request ~> routes ~> check { + status should ===(StatusCodes.OK) + + // we expect the response to be json: + contentType should ===(ContentTypes.`application/json`) + + // and no entries should be in the list: + entityAs[String] should ===("""{"users":[]}""") + } + } + //#actual-test + + //#testing-post + "be able to add users (POST /users)" in { + val user = User("Kapi", 42, "jp") + val userEntity = Marshal(user).to[MessageEntity].futureValue // futureValue is from ScalaFutures + + // using the RequestBuilding DSL: + val request = Post("/users").withEntity(userEntity) + + request ~> routes ~> check { + status should ===(StatusCodes.Created) + + // we expect the response to be json: + contentType should ===(ContentTypes.`application/json`) + + // and we know what message we're expecting back: + entityAs[String] should ===("""{"description":"User Kapi created."}""") + } + } + //#testing-post + + "be able to remove users (DELETE /users)" in { + // user the RequestBuilding DSL provided by ScalatestRouteSpec: + val request = Delete(uri = "/users/Kapi") + + request ~> routes ~> check { + status should ===(StatusCodes.OK) + + // we expect the response to be json: + contentType should ===(ContentTypes.`application/json`) + + // and no entries should be in the list: + entityAs[String] should ===("""{"description":"User Kapi deleted."}""") + } + } + //#actual-test + } + //#actual-test + + //#set-up +} +//#set-up