This is an automated email from the ASF dual-hosted git repository. fanningpj pushed a commit to branch wip-adding-tests in repository https://gitbox.apache.org/repos/asf/incubator-pekko-http-quickstart-scala-g8.git
commit 2de7d16b7e168684c83afa65ca0abbb3f0e9719b Author: henrikengstrom <[email protected]> AuthorDate: Fri Jun 16 17:51:26 2017 -0400 Revamping of seed template into a "Hello World" Akka Http tutorial. Completely new sample code and docs. --- build.sbt | 6 ++ docs/build.sbt | 10 ++ docs/src/main/paradox/backend.md | 10 ++ docs/src/main/paradox/images/hello-akka-http.png | Bin 0 -> 280372 bytes docs/src/main/paradox/images/idea-open-project.png | Bin 0 -> 127616 bytes .../main/paradox/images/idea-running-project.png | Bin 0 -> 150403 bytes docs/src/main/paradox/index.md | 86 +++++++++++++++ docs/src/main/paradox/intellij-idea.md | 26 +++++ docs/src/main/paradox/json.md | 18 ++++ docs/src/main/paradox/running-the-application.md | 86 +++++++++++++++ docs/src/main/paradox/server-class.md | 117 +++++++++++++++++++++ project/build.properties | 2 +- project/paradox.sbt | 2 + src/main/g8/build.sbt | 14 ++- src/main/g8/project/build.properties | 2 +- .../g8/src/main/scala/com/example/WebServer.scala | 32 ------ .../main/scala/com/example/WebServerHttpApp.scala | 25 ----- .../main/scala/com/example/routes/BaseRoutes.scala | 17 --- .../scala/com/example/routes/SimpleRoutes.scala | 20 ---- .../lightbend/akka/http/sample/JsonSupport.scala | 9 ++ .../akka/http/sample/QuickstartServer.scala | 98 +++++++++++++++++ .../akka/http/sample/UserRegistryActor.scala | 32 ++++++ .../scala/com/example/WebServerHttpAppSpec.scala | 42 -------- .../scala/com/example/routes/BaseRoutesSpec.scala | 22 ---- .../com/example/routes/SimpleRoutesSpec.scala | 32 ------ 25 files changed, 508 insertions(+), 200 deletions(-) diff --git a/build.sbt b/build.sbt index d937000..098e2f1 100644 --- a/build.sbt +++ b/build.sbt @@ -10,3 +10,9 @@ lazy val root = (project in file(".")). scriptedLaunchOpts ++= List("-Xms1024m", "-Xmx1024m", "-XX:ReservedCodeCacheSize=128m", "-XX:MaxPermSize=256m", "-Xss2m", "-Dfile.encoding=UTF-8"), resolvers += Resolver.url("typesafe", url("http://repo.typesafe.com/typesafe/ivy-releases/"))(Resolver.ivyStylePatterns) ) + +// Documentation for this project: +// sbt "project docs" "~ paradox" +// open docs/target/paradox/site/main/index.html +lazy val docs = (project in file("docs")) + .enablePlugins(ParadoxPlugin) diff --git a/docs/build.sbt b/docs/build.sbt new file mode 100644 index 0000000..7222d72 --- /dev/null +++ b/docs/build.sbt @@ -0,0 +1,10 @@ +// Uses the out of the box generic theme. +paradoxTheme := Some(builtinParadoxTheme("generic")) + +scalaVersion := "2.12.2" + +paradoxProperties in Compile ++= Map( + "snip.g8root.base_dir" -> "../../../../src/main/g8", + "snip.g8src.base_dir" -> "../../../../src/main/g8/src/main/", + "snip.g8srctest.base_dir" -> "../../../../src/main/g8/src/test/" +) diff --git a/docs/src/main/paradox/backend.md b/docs/src/main/paradox/backend.md new file mode 100644 index 0000000..d14110e --- /dev/null +++ b/docs/src/main/paradox/backend.md @@ -0,0 +1,10 @@ +Backend +------- + +It is probably an exaggeration to call this for a "backend" since it, in this sample, only consists of a basic actor. In a real system, we would have many actors interacting with each other and perhaps a database or microservices. However, since the focus of this tutorial is on Akka Http, it is not very important what this backend does but more how to interact with a backend from within Akka Http. Hopefully, you already have a good grasp of how to communicate back and forth between Akka [...] + +If you feel you should brush up your Akka Actor knowledge, then the [getting started guide]((http://developer.lightbend.com/guides/akka-quickstart-scala/)) for Akka actors tutorial is a good start. + +The sample code in the `UserRegistryActor` is very simple. It keeps registered users in a `Set`. Once it receives messages it will match those into what action it should take: + +@@snip [UserRegistryActor.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala) diff --git a/docs/src/main/paradox/images/hello-akka-http.png b/docs/src/main/paradox/images/hello-akka-http.png new file mode 100644 index 0000000..10ba689 Binary files /dev/null and b/docs/src/main/paradox/images/hello-akka-http.png differ diff --git a/docs/src/main/paradox/images/idea-open-project.png b/docs/src/main/paradox/images/idea-open-project.png new file mode 100644 index 0000000..9c051a4 Binary files /dev/null and b/docs/src/main/paradox/images/idea-open-project.png differ diff --git a/docs/src/main/paradox/images/idea-running-project.png b/docs/src/main/paradox/images/idea-running-project.png new file mode 100644 index 0000000..12e0310 Binary files /dev/null and b/docs/src/main/paradox/images/idea-running-project.png differ diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md new file mode 100644 index 0000000..8b8cab6 --- /dev/null +++ b/docs/src/main/paradox/index.md @@ -0,0 +1,86 @@ +Akka Http Quickstart with Scala +=============================== + +The Akka HTTP modules implement a full server- and client-side HTTP stack on top of akka-actor and akka-stream. It’s not a web-framework but rather a more general toolkit for providing and consuming HTTP-based services. While interaction with a browser is of course also in scope, it is not the primary focus of Akka HTTP. + +Akka HTTP follows a rather open design and many times offers several different API levels for "doing the same thing." The users get to pick the API level of abstraction that is most suitable for their applications. This approach means that, if you have trouble achieving something using a high-level API, there’s a good chance that you can get it done with a low-level API, which offers more flexibility but might require you to write more application code. + +This Hello World example demonstrates some Akka HTTP fundamentals. Within 30 minutes, you should be able to download, run the sample application, and use this documentation to guide you through this example's building blocks. + +## Prerequisite + +Having a basic understanding of Akka actors will help the reader of this guide. There is a [getting started guide]((http://developer.lightbend.com/guides/akka-quickstart-scala/)) for Akka actors in if you feel like brushing up your knowledge thereof. + +## Downloading the example + +The Hello World example for Scala is a zipped project that includes a distribution of sbt (build tool). You can run it on Linux, MacOS, or Windows. The only prerequisite is Java 8. + +Download and unzip the example: + +1. Download the zip file from Lightbend Tech Hub by clicking `CREATE A PROJECT FOR ME`. +2. Extract the zip file to a convenient location: + +* On Linux and OSX systems, open a terminal and use the command unzip akka-quickstart-scala.zip. Note: On OSX, if you unzip using Archiver, you also have to make the sbt files executable: + +``` +$ chmod u+x ./sbt +$ chmod u+x ./sbt-dist/bin/sbt +``` + +* On Windows, use a tool such as File Explorer to extract the project. + +## Running the example + +To run Hello World: + +In a console, change directories to the top level of the unzipped project. + +For example, if you used the default project name, `akka-http-quickstart-scala`, and extracted the project to your root directory, from the root directory, enter: cd akka-http-quickstart-scala + +Enter `./sbt` on OSX/Linux or run `sbt.bat` on Windows to start [sbt](http://www.scala-sbt.org). + +sbt downloads project dependencies. The `>` prompt indicates sbt has started in interactive mode. + +At the sbt prompt, enter `run`. + +sbt builds the project and runs Hello World. + +The output should look something like this: + +``` +[info] Loading global plugins from /Users/x/.sbt/0.13/plugins +... +[info] Running com.lightbend.akka.http.sample.QuickstartServer +Server online at http://localhost:8080/ +Press RETURN to stop... +``` + +The command above will start an Akka Http server. We will look at how to invoke the available endpoints of the server later in this guide. + +Congratulations, you just ran your first Akka Http app. Now take a look at what happened under the covers. + +## What Hello World does + +When Hello World starts up it creates an ActorSystem and registers so-called routes that are available in this ActorSystem. The routes are bound to a port, in this case, `localhost:8080`. Below is an overview of what things look like when the application starts: + + + +In this sample application, we are implementing a [REST service](https://en.wikipedia.org/wiki/Representational_state_transfer). The idea is to build a simple user registry service. + +The endpoints available are `/users` and `/user` with some attached HTTP directives and parameters/payloads. When invoked, endpoints will interact with an actor, "userRegistryActor", which represents the business logic of this sample application. + +In the following sections, we will take a detailed look at the individual pieces of this application. + +## The Akka HTTP philosophy + +Akka HTTP is designed specifically as "not-a-framework," not because we don’t like frameworks, but for use cases where a framework is not the right choice. Akka HTTP is for building integration layers based on HTTP and as such tries to "stay on the sidelines." Therefore, a typical application doesn't sit on top of Akka HTTP but instead on top of whatever makes sense and uses Akka HTTP merely for the HTTP integration needs. + +@@@index + +* [The server](server-class.md) +* [The backend](backend.md) +* [JSON](json.md) +* [Running the application](running-the-application.md) +* [IntelliJ IDEA](intellij-idea.md) + +@@@ diff --git a/docs/src/main/paradox/intellij-idea.md b/docs/src/main/paradox/intellij-idea.md new file mode 100644 index 0000000..7aaeb71 --- /dev/null +++ b/docs/src/main/paradox/intellij-idea.md @@ -0,0 +1,26 @@ +IntelliJ IDEA +------------- + +[IntelliJ](https://www.jetbrains.com/idea/) from JetBrains is one of the leading IDEs in the Java/Scala community, and it has excellent support for Akka Http. This section will guide you through setting up, testing and running the sample project. + +## Setting up the project + +Open IntelliJ and select File -> Open... and point to the directory where you have installed the sample project. There should be a pop-up like this: + + + +Fill out the settings according to the above and press `OK` to import the project. If IntelliJ will warn about missing Scala SDK, it is only to follow the instructions to add support. + +## Inspecting the code + +If we open up the file `src/main/scala/com/lightbend/akka/http/sample/QuickstartServer.scala` we can see a lot of lines beginning with //# .... These lines are used as directives for this documentation. To get rid of these lines from the source code we can utilize the awesome Find/Replace functionality in IntelliJ. Select Edit -> Find -> Replace in Path.... Check the Regex box and add the following regex [//#].* and click on Replace in Find Window.... Select to replace all occurrences an [...] + +## Running the application + +Right click on the file `src/main/scala/com/lightbend/akka/http/sample/QuickstartServer.scala` and select Run 'QuickstartServer' and the output should look like this: + + + +## Tutorial done! + +Congratulations! We have now learned enough concepts to get started with building real-world Akka Http applications. Of course, there is plenty of more that we can do with Akka Http and the [documentation](http://doc.akka.io/docs/akka-http/current/scala/http/index.html) is a good starting point if there is something more you need. diff --git a/docs/src/main/paradox/json.md b/docs/src/main/paradox/json.md new file mode 100644 index 0000000..bf91eab --- /dev/null +++ b/docs/src/main/paradox/json.md @@ -0,0 +1,18 @@ +JSON +---- + +In the server class, we saw that JSON is somehow converted into Scala classes and vice versa. In this section, we shall look at how this is implemented and what you need to do to make it work. + +Let's take a look at the server class definition once again: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #main-class } + +See the `JsonSupport` up there? This is a trait that we have created and it looks like this: + +@@snip [JsonSupport.scala]($g8src$/scala/com/lightbend/akka/http/sample/JsonSupport.scala) + +The above trait defines two implicit values; `userJsonFormat` and `usersJsonFormat`. To do so we use the `jsonFormatX` methods, from [Spray Json](), where X is representing the number of parameters in the underlying case classes: + +@@snip [UserRegistryActor.scala]($g8src$/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala) { #user-case-classes } + +By defining the Formatters as `implicit`, we ensure that the compiler can map the formatting functionality with the case classes we want to convert. We won't go into the underlying functionality for how the formatters are implemented. All we have to remember for now is to define the formatters as implicit and that the Formatter used should map the number of parameters of "its" case class. diff --git a/docs/src/main/paradox/running-the-application.md b/docs/src/main/paradox/running-the-application.md new file mode 100644 index 0000000..6dafe7c --- /dev/null +++ b/docs/src/main/paradox/running-the-application.md @@ -0,0 +1,86 @@ +Running the application +----------------------- + +You can run the Hello World application from the command line or an IDE. The final topic in this guide describes how to run it from IntelliJ IDEA. However, before we get there, let’s take a quick look at the build tool: sbt. + +## The build files + +sbt uses a build.sbt file to handle the project. This project’s build.sbt file looks like this: + +@@snip [build.sbt]($g8root$/build.sbt) + +## Running the project + +We run the application from a console/terminal window: + +1. Enter `./sbt` on OSX/Linux or `sbt.bat` on Windows + +sbt downloads project dependencies. The `>` prompt indicates sbt has started in interactive mode. + +2. At the sbt prompt, enter `run`. + +The output should look like this: + +``` +... +[info] Running com.lightbend.akka.http.sample.QuickstartServer +Server online at http://localhost:8080/ +Press RETURN to stop... +``` + +## Testing the application + +The Akka Http server is now running, and we will use the [cURL](https://en.wikipedia.org/wiki/CURL) command to test the application. If you should prefer to use your browser to test the service than a tool like [RESTClient](http://restclient.net/) may be good to install. + +Open another console/terminal window to investigate the functionality of the application. + +We start by looking at the existing users (there should be none as we just launched the application): + +``` +$ curl http://localhost:8080/users +{"users":[]} +``` + +The next step is to add a couple of users: +``` +$ curl -H "Content-type: application/json" -X POST -d '{"name": "MrX", "age": 31, "countryOfResidence": "Canada"}' http://localhost:8080/user +User MrX created. + +$ curl -H "Content-type: application/json" -X POST -d '{"name": "Anonymous", "age": 55, "countryOfResidence": "Iceland"}' http://localhost:8080/user +User Anonymous created. + +$ curl -H "Content-type: application/json" -X POST -d '{"name": "Bill", "age": 67, "countryOfResidence": "USA"}' http://localhost:8080/user +User Bill created. +``` + +We can try to retrieve user information for some various users now: + +``` +$ curl http://localhost:8080/user/MrX +{"name":"MrX","age":31,"countryOfResidence":"Canada"} + +$ curl http://localhost:8080/user/SomeUnknownUser +User SomeUnknownUser is not registered. +``` + +Now, when we inquire the system for all existing users it looks like this: + +``` +$ curl http://localhost:8080/users +{"users":[{"name":"Anonymous","age":55,"countryOfResidence":"Iceland"},{"name":"MrX","age":31,"countryOfResidence":"Canada"},{"name":"Bill","age":67,"countryOfResidence":"USA"}]} +``` + +Next, we should make sure that the delete functionality works as expected: + +``` +$ curl -X DELETE http://localhost:8080/user/Bill +User Bill deleted. + +$ curl http://localhost:8080/user/Bill +User Bill is not registered. + +$ curl http://localhost:8080/users +{"users":[{"name":"Anonymous","age":55,"countryOfResidence":"Iceland"},{"name":"MrX","age":31,"countryOfResidence":"Canada"}]} +``` + +We have now tried all the functionality available in this sample. The next step is to see how we can use an IDE to work with the application. diff --git a/docs/src/main/paradox/server-class.md b/docs/src/main/paradox/server-class.md new file mode 100644 index 0000000..45e4782 --- /dev/null +++ b/docs/src/main/paradox/server-class.md @@ -0,0 +1,117 @@ +The main class +---------------- + +Let's dissect the main class, `QuickstartServer`. We make this class runnable by extending `App` (we will discuss the trait `JsonSupport` later): + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #main-class } + +Now that we have a class to run we should add some Akka Http fundamentals with which we will build our RESTful web service: + +* define routes bound to endpoints and HTTP directives +* create a server bound to an IP and port that will handle all requests +* add error handling for when something goes wrong + +Let us take a look at each of these steps here below. + +## Routes + +For our service we want to define the following endpoints: + +| Path | Http directive | Intent | Returns | +|-------------|-----------------|--------------------|----------------------| +| /user | POST | Create a new user | Confirmation message | +| /user/$ID | GET | Retrieve a user | JSON payload | +| /user/$ID | DELETE | Remove a user | Confirmation message | +| /users | GET | Retrieve all users | JSON payload | + +Akka Http provides a [domain-specific language](https://en.wikipedia.org/wiki/Domain-specific_language) (DSL) to simplify the routes/endpoints definition. Each route is composed of one or more `akka.http.scaladsl.server.Directives`, e.g. `path`, `get`, `post`, `complete`, etc. + +### Creating a new user + +Let us take a look at the source code for the first endpoint, the `/user` URI with a `POST` directive, used to create a new user: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #user-post } + +The snippet above contains a couple of interesting building blocks: + +* `path` : matches against the incoming URI, in this case, we want to get all requests that matches `user`. +* `post` : matches against the incoming Http directive, in this case, we are matching against `POST`. +* `entity(as[User])` : automatically converts the incoming payload, in this case, we expect JSON, into an entity. We will look more at this functionality in the @ref:[JSON](json.md) section. +* `complete` : used to reply back to the request. The `StatusCodes.Created` is translated to Http response code 201. We also send back information to the caller in the form of a string. + +When this `Route` is called, we want to create a new user, and we do so by sending a message to the actor `userRegistryActor`. We will look at the implementation of this actor later. + +### Retrieving and removing a user + +Next we need to define how to retrieve and remove a user, i.e. for the case when the URI `/user/$ID` is used where `$ID` is the id of the user: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #user-get-delete } + +This Route snippet contains a couple of interesting concepts: + +* `path("user" / Segment) { => user` : this bit of code matches against URIs of the exact format `/user/$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 `/user/Bruce` will populate the `user` variable with the value "Bruce." +* `get` : matches against the incoming Http directive. + +Let's break down the "business logic" in the first Route: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #retrieve-user-info } + +The code above uses the so-called [ask](http://doc.akka.io/docs/akka/current/scala/actors.html#send-messages) in Akka. This will send a message asynchronously and return a `Future` representing a _possible_ reply. The code above maps the reply to the type `UserInfo`. When the future completes, it will use the second part of the code to evaluate to either `Success`, with or without a result, or a `Failure`. Regardless of the outcome of the future, we should return something to the request [...] + +The remaining directives used for this route are: + +* `~` : fuses `Route`s together - this will become more apparent when you see the complete `Route` definition here below. +* `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 and return a status code to the client (which in this case is `StatusCodes.OK`, i.e. Http 200) + +### Retrieving all users + +Finally we should implement functionality to retrieve all registered users: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #users-get } + +This code is based on the same structure as the code above, send a message to the user registry actor using an `ask` and pass on the `Future` to the `complete` method. + +Why do we not use an `onComplete` as we did for when retrieving a particular user? The difference is that when we use `GetUsers` there will always be something returned; an empty list means that there are no registered users. However, when we asked for a particular user ID, there might be the case that there is no such user recorded and we need a way to tell this to the client. When sent a `GetUser(name)` the user registry actor therefore send back an `Option[UserInfo]`. It could be that [...] + +### The complete Route + +Below is the complete `Route` definition used in the sample application: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #all-routes } + +So far we have referred to `Route` without explaining what it is but now is the time to do so. Under the hood, Akka Http uses [Akka Streams](http://doc.akka.io/docs/akka/current/scala/stream/index.html). We don't have time to cover Akka Streams here, but if you are interested, you should take a look at the Hello World sample application for Streams. Since Akka Http is built on top of Akka Streams, it means that some concepts of Streams are available for us to use. In the case of `Route` [...] + +## Http server + +To set up an Akka Http server we must first define some implicit values that will be used by the server: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #server-bootstrapping } + +What does the above mean and why do we need it? + +* `ActorSystem` : the context in which actors will run. What actors, you may wonder? Akka Streams uses actors under the hood, and the actor system defined in this `val` will be picked up and used by Streams. +* `ActorMaterializer` : also Akka Streams related - it uses the materializer to allocate all the necessary resources it needs to run. + +With that defined we can move on to instantiate the server: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #http-server } + +We provide three parameters; `routes`, the hostname, and the port. That's it! When running this program, we will have an Akka Http server on our machine (localhost) on port 8080. Note that starting a server happens asynchronously and therefore a `Future` is returned by the `bindAndHandle` method. + +We should also add code for stopping the server. To do so we use the `StdIn.readLine()` method that will wait until RETURN is pressed on the keyboard. When that happens we `flatMap` the `Future` returned when we started the server to get to the `unbind()` method. Unbinding is also an asynchronous function and when the `Future` returned by `unbind()` is completes we make sure that the actor system is properly terminated. + +## Error handling + +Finally, we should take a look at how to handle errors. We know, as the astute engineers we are, that errors will happen. We should prepare our program for this and error handling should not be an afterthought when we build systems. + +In this sample, we use a very simple exception handler which catches all unexpected exceptions and responds back to the client with an `InternalServerError` (HTTP 500) with an error message and for what URI the exception happened. We extract the URI by using the `extractUri` directive. + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) { #exception-handler } + +## The complete server code + +Here is the complete server code used in the sample: + +@@snip [QuickstartServer.scala]($g8src$/scala/com/lightbend/akka/http/sample/QuickstartServer.scala) diff --git a/project/build.properties b/project/build.properties index 27e88aa..64317fd 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.13 +sbt.version=0.13.15 diff --git a/project/paradox.sbt b/project/paradox.sbt new file mode 100644 index 0000000..01bcc5c --- /dev/null +++ b/project/paradox.sbt @@ -0,0 +1,2 @@ +// sbt-paradox, used for documentation +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.11") diff --git a/src/main/g8/build.sbt b/src/main/g8/build.sbt index f8ade85..030ddd5 100644 --- a/src/main/g8/build.sbt +++ b/src/main/g8/build.sbt @@ -1,18 +1,16 @@ -lazy val akkaHttpVersion = "$akka_http_version$" -lazy val akkaVersion = "$akka_version$" +lazy val akkaHttpVersion = "10.0.9" +lazy val akkaVersion = "2.5.2" lazy val root = (project in file(".")). settings( inThisBuild(List( organization := "com.example", - scalaVersion := "$scala_version$" + scalaVersion := "2.12.2" )), name := "$name$", libraryDependencies ++= Seq( - "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, - "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 + "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-stream" % akkaVersion ) ) diff --git a/src/main/g8/project/build.properties b/src/main/g8/project/build.properties index 27e88aa..64317fd 100644 --- a/src/main/g8/project/build.properties +++ b/src/main/g8/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.13 +sbt.version=0.13.15 diff --git a/src/main/g8/src/main/scala/com/example/WebServer.scala b/src/main/g8/src/main/scala/com/example/WebServer.scala deleted file mode 100644 index 2baafa7..0000000 --- a/src/main/g8/src/main/scala/com/example/WebServer.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.example - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Directives -import akka.stream.ActorMaterializer -import com.example.routes.{ BaseRoutes, SimpleRoutes } - -import scala.io.StdIn - -object WebServer extends Directives with SimpleRoutes { - def main(args: Array[String]) { - - implicit val system = ActorSystem("my-system") - implicit val materializer = ActorMaterializer() - // needed for the future flatMap/onComplete in the end - implicit val executionContext = system.dispatcher - - val bindingFuture = Http().bindAndHandle(routes, "localhost", 8080) - - println(s"Server online at http://localhost:8080/\nPress RETURN to stop...") - StdIn.readLine() // let it run until user presses return - bindingFuture - .flatMap(_.unbind()) // trigger unbinding from the port - .onComplete(_ => system.terminate()) // and shutdown when done - } - - // Here you can define all the different routes you want to have served by this web server - // Note that routes might be defined in separated traits like the current case - val routes = BaseRoutes.baseRoutes ~ simpleRoutes - -} diff --git a/src/main/g8/src/main/scala/com/example/WebServerHttpApp.scala b/src/main/g8/src/main/scala/com/example/WebServerHttpApp.scala deleted file mode 100644 index e41be52..0000000 --- a/src/main/g8/src/main/scala/com/example/WebServerHttpApp.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.example - -import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport.defaultNodeSeqMarshaller -import akka.http.scaladsl.server.{ HttpApp, Route } - -/** - * Server will be started calling `WebServerHttpApp.startServer("localhost", 8080)` - * and it will be shutdown after pressing return. - */ -object WebServerHttpApp extends HttpApp with App { - // Routes that this WebServer must handle are defined here - // Please note this method was named `route` in versions prior to 10.0.7 - def routes: Route = - pathEndOrSingleSlash { // Listens to the top `/` - complete("Server up and running") // Completes with some text - } ~ - path("hello") { // Listens to paths that are exactly `/hello` - get { // Listens only to GET requests - complete(<html><body><h1>Say hello to akka-http</h1></body></html>) // Completes with some text - } - } - - // This will start the server until the return key is pressed - startServer("localhost", 8080) -} diff --git a/src/main/g8/src/main/scala/com/example/routes/BaseRoutes.scala b/src/main/g8/src/main/scala/com/example/routes/BaseRoutes.scala deleted file mode 100644 index bd1ce87..0000000 --- a/src/main/g8/src/main/scala/com/example/routes/BaseRoutes.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.routes - -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.server.directives.PathDirectives.pathEndOrSingleSlash -import akka.http.scaladsl.server.directives.RouteDirectives.complete - -/** - * Routes can be defined in separated classes like shown in here - */ -object BaseRoutes { - - // This route is the one that listens to the top level '/' - lazy val baseRoutes: Route = - pathEndOrSingleSlash { // Listens to the top `/` - complete("Server up and running") // Completes with some text - } -} diff --git a/src/main/g8/src/main/scala/com/example/routes/SimpleRoutes.scala b/src/main/g8/src/main/scala/com/example/routes/SimpleRoutes.scala deleted file mode 100644 index 177981b..0000000 --- a/src/main/g8/src/main/scala/com/example/routes/SimpleRoutes.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.routes - -import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport.defaultNodeSeqMarshaller -import akka.http.scaladsl.server.directives.MethodDirectives.get -import akka.http.scaladsl.server.directives.PathDirectives.path -import akka.http.scaladsl.server.directives.RouteDirectives.complete - -/** - * Routes can be defined in separated classes like shown in here - */ -trait SimpleRoutes { - - // This `val` holds one route (of possibly many more that will be part of your Web App) - lazy val simpleRoutes = - path("hello") { // Listens to paths that are exactly `/hello` - get { // Listens only to GET requests - complete(<html><body><h1>Say hello to akka-http</h1></body></html>) // Completes with some html page - } - } -} 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 new file mode 100644 index 0000000..6f19b73 --- /dev/null +++ b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/JsonSupport.scala @@ -0,0 +1,9 @@ +package com.lightbend.akka.http.sample + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import spray.json.DefaultJsonProtocol + +trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { + implicit val userJsonFormat = jsonFormat3(User) + implicit val usersJsonFormat = jsonFormat1(Users) +} 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 new file mode 100644 index 0000000..1279967 --- /dev/null +++ b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/QuickstartServer.scala @@ -0,0 +1,98 @@ +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 scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.io.StdIn +import scala.util.{ Failure, Success } +import com.lightbend.akka.http.sample.UserRegistryActor._ + +//#main-class +object QuickstartServer extends App with JsonSupport { + //#main-class + //#server-bootstrapping + implicit val system: ActorSystem = ActorSystem("helloAkkaHttpServer") + implicit val materializer: ActorMaterializer = ActorMaterializer() + //#server-bootstrapping + + // Needed for the Future and its methods flatMap/onComplete in the end + implicit val executionContext: ExecutionContext = system.dispatcher + + val userRegistryActor: ActorRef = system.actorOf(UserRegistryActor.props, "userRegistryActor") + implicit val timeout = Timeout(5 seconds) + + //#exception-handler + implicit val exceptionHandler = ExceptionHandler { + case e: Exception => + extractUri { uri => + complete((StatusCodes.InternalServerError, s"Exception ${e.getMessage} happened for URI: $uri.")) + } + } + //#exception-handler + + //#all-routes + lazy val routes: Route = + //#user-post + path("user") { + post { + entity(as[User]) { user => + userRegistryActor ! CreateUser(user) + complete((StatusCodes.Created, s"User ${user.name} created.")) + } + } + } ~ //#user-post + //#user-get-delete + path("user" / Segment) { name => + get { + //#retrieve-user-info + val userInfo: Future[UserInfo] = (userRegistryActor ? GetUser(name)).mapTo[UserInfo] + onComplete(userInfo) { r => + r match { + case Success(UserInfo(Some(user))) => complete(user) + case Success(UserInfo(None)) => complete((StatusCodes.OK, s"User $name is not registered.")) + case Failure(ex) => complete((StatusCodes.InternalServerError, ex)) + } + } + //#retrieve-user-info + } ~ + delete { + userRegistryActor ! DeleteUser(name) + complete((StatusCodes.OK, s"User $name deleted.")) + } + } ~ //#user-get-delete + //#users-get + path("users") { + get { + val users: Future[Users] = (userRegistryActor ? GetUsers).mapTo[Users] + complete(users) + } + } //#users-get + //#all-routes + + //#http-server + val serverBindingFuture: Future[ServerBinding] = Http().bindAndHandle(routes, "localhost", 8080) + println(s"Server online at http://localhost:8080/\nPress RETURN to stop...") + StdIn.readLine() + serverBindingFuture + .flatMap(_.unbind()) + .onComplete(_ => system.terminate()) + //#http-server + //#main-class +} +//#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 new file mode 100644 index 0000000..718f63e --- /dev/null +++ b/src/main/g8/src/main/scala/com/lightbend/akka/http/sample/UserRegistryActor.scala @@ -0,0 +1,32 @@ +package com.lightbend.akka.http.sample + +import akka.actor.{ Actor, ActorLogging, Props } +import scala.collection.mutable.Set + +case class UserInfo(maybeUser: Option[User]) +//#user-case-classes +case class User(name: String, age: Int, countryOfResidence: String) +case class Users(users: Seq[User]) +//#user-case-classes + +object UserRegistryActor { + final case object GetUsers + final case class CreateUser(user: User) + final case class GetUser(name: String) + final case class DeleteUser(name: String) + + def props: Props = Props[UserRegistryActor] +} + +class UserRegistryActor extends Actor with ActorLogging { + import UserRegistryActor._ + + val users: Set[User] = Set.empty[User] + + def receive = { + case GetUsers => sender ! Users(users.toSeq) + case CreateUser(user) => users += user + case GetUser(name) => sender ! UserInfo(users.find(_.name == name)) + case DeleteUser(name) => users.find(_.name == name) map { user => users -= user } + } +} diff --git a/src/main/g8/src/test/scala/com/example/WebServerHttpAppSpec.scala b/src/main/g8/src/test/scala/com/example/WebServerHttpAppSpec.scala deleted file mode 100644 index 349ed19..0000000 --- a/src/main/g8/src/test/scala/com/example/WebServerHttpAppSpec.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.example - -import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport.defaultNodeSeqUnmarshaller -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.testkit.ScalatestRouteTest -import org.scalatest.{ Matchers, WordSpec } - -import scala.xml.NodeSeq - -class WebServerHttpAppSpec extends WordSpec with Matchers with ScalatestRouteTest { - - "WebServiceHttpApp" should { - "answer to any request to `/`" in { - Get("/") ~> WebServerHttpApp.routes ~> check { - status shouldBe StatusCodes.OK - responseAs[String] shouldBe "Server up and running" - } - Post("/") ~> WebServerHttpApp.routes ~> check { - status shouldBe StatusCodes.OK - responseAs[String] shouldBe "Server up and running" - } - } - "answer to GET requests to `/hello`" in { - Get("/hello") ~> WebServerHttpApp.routes ~> check { - status shouldBe StatusCodes.OK - responseAs[NodeSeq] shouldBe <html><body><h1>Say hello to akka-http</h1></body></html> - } - } - "not handle a POST request to `/hello`" in { - Post("/hello") ~> WebServerHttpApp.routes ~> check { - handled shouldBe false - } - } - "respond with 405 when not issuing a GET to `/hello` and route is sealed" in { - Put("/hello") ~> Route.seal(WebServerHttpApp.routes) ~> check { - status shouldBe StatusCodes.MethodNotAllowed - } - } - } - -} diff --git a/src/main/g8/src/test/scala/com/example/routes/BaseRoutesSpec.scala b/src/main/g8/src/test/scala/com/example/routes/BaseRoutesSpec.scala deleted file mode 100644 index fa7d616..0000000 --- a/src/main/g8/src/test/scala/com/example/routes/BaseRoutesSpec.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.routes - -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.testkit.ScalatestRouteTest -import org.scalatest.{ Matchers, WordSpec } - -class BaseRoutesSpec extends WordSpec with Matchers with ScalatestRouteTest { - - "BaseRoute" should { - "answer to any request to `/`" in { - Get("/") ~> BaseRoutes.baseRoutes ~> check { - status shouldBe StatusCodes.OK - responseAs[String] shouldBe "Server up and running" - } - Post("/") ~> BaseRoutes.baseRoutes ~> check { - status shouldBe StatusCodes.OK - responseAs[String] shouldBe "Server up and running" - } - } - } - -} diff --git a/src/main/g8/src/test/scala/com/example/routes/SimpleRoutesSpec.scala b/src/main/g8/src/test/scala/com/example/routes/SimpleRoutesSpec.scala deleted file mode 100644 index 0c38e13..0000000 --- a/src/main/g8/src/test/scala/com/example/routes/SimpleRoutesSpec.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.routes - -import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport.defaultNodeSeqUnmarshaller -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.testkit.ScalatestRouteTest -import org.scalatest.{ Matchers, WordSpec } - -import scala.xml.NodeSeq - -class SimpleRoutesSpec extends WordSpec with Matchers with ScalatestRouteTest with SimpleRoutes { - - "SimpleRoute" should { - "answer to GET requests to `/hello`" in { - Get("/hello") ~> simpleRoutes ~> check { - status shouldBe StatusCodes.OK - responseAs[NodeSeq] shouldBe <html><body><h1>Say hello to akka-http</h1></body></html> - } - } - "not handle a POST request to `/hello`" in { - Post("/hello") ~> simpleRoutes ~> check { - handled shouldBe false - } - } - "respond with 405 when not issuing a GET to `/hello` and route is sealed" in { - Put("/hello") ~> Route.seal(simpleRoutes) ~> check { - status shouldBe StatusCodes.MethodNotAllowed - } - } - } - -} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
