Skip to content

Commit

Permalink
Added testing docs and some more separation
Browse files Browse the repository at this point in the history
  • Loading branch information
ktoso committed Aug 14, 2017
1 parent f69ebe5 commit 2ceb3a8
Show file tree
Hide file tree
Showing 15 changed files with 422 additions and 129 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,4 +1,5 @@
target/
.idea
boot/
lib_managed/
src_managed/
Expand Down
15 changes: 15 additions & 0 deletions 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.
10 changes: 0 additions & 10 deletions docs/src/main/paradox/backend.md

This file was deleted.

@@ -1,39 +1,57 @@
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.

### Retrieving and creating users

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. <br/>
<br/>
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**

Expand All @@ -44,44 +62,54 @@ 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.

**Deleting a user**

* `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

Expand Down
8 changes: 5 additions & 3 deletions docs/src/main/paradox/index.md
Expand Up @@ -175,18 +175,20 @@ 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.

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)

@@@
32 changes: 25 additions & 7 deletions 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.

16 changes: 8 additions & 8 deletions 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:

Expand All @@ -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/<NAME>` to the URL.
`6.` Try deleting a user you just added. Hint: use the `DELETE`, and append `/users/<NAME>` to the URL.

You should get back the message: `User MrX deleted.`

Expand Down

0 comments on commit 2ceb3a8

Please sign in to comment.