Skip to content

dnvriend/akka-http-test

Repository files navigation

akka-http-test

Join the chat at https://gitter.im/dnvriend/akka-http-test Build Status License

A study project how akka-http works. The code below is a bit compacted, so please use it for reference only how the (new) API must be used. It will not compile/work correctly when you just copy/paste it. Check out the working source code for correct usage.

Mastering Akka by Chris Baxter

A great book ETA October 2016 by Packt Publishing, written by Chris Baxter and reviewed by me is Mastering Akka which will contain everything you ever wanted to know about akka-actors, akka-persistence, akka-streams, akka-http and akka-cluster, ConductR, Domain Driven Design and Event Sourcing. Its a book you just should have.

Contribution policy

Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so.

License

This code is open source software licensed under the Apache 2.0 License.

akka http documentation

Akka http now has its own release scheme and is not tied to that of akka anymore. As such, you should update your build file accordingly.

Source Streaming

As of Akka v2.4.9-RC1, akka-http supports completing a request with an Akka Source[T, _], which makes it possible to easily build and consume streaming end-to-end APIs which apply back-pressure throughout the entire stack!

Web Service Clients (RPC)

Akka-Http has a client API and as such RPC's can be created. Take a look at the package com.github.dnvriend.webservices, I have created some example RPC style web service clients for eetnu, iens, postcode, openWeatherApi, based on the generic com.github.dnvriend.webservices.generic.HttpClient client that supports Http and SSL connections with basic authentication or one legged OAuth1 with consumerKey and consumerSecret configuration from application.conf. The RPC clients also support for single RPC without cached connections and the streaming cached connection style where you can stream data to your clients. For usage please see the tests for the RPC clients. Good stuff :)

Web Server

A new HTTP server can be launched using the Http() class. The bindAndHandle() method is a convenience method which starts a new HTTP server at the given endpoint and uses the given 'handler' Flow for processing all incoming connections.

The number of concurrently accepted connections can be configured by overriding akka.http.server.max-connections setting.

import akka.http.scaladsl._
import akka.http.scaladsl.model._
import akka.stream.scaladsl._

def routes: Flow[HttpRequest, HttpResponse, Unit]

Http().bindAndHandle(routes, "0.0.0.0", 8080)

Routes

First some Akka Stream parley:

  • Stream: a continually moving flow of elements,
  • Element: the processing unit of streams,
  • Processing stage: building blocks that build up a Flow or FlowGraph for example map(), filter(), transform(), junction() etc,
  • Source: a processing stage with exactly one output, emitting data elements when downstream processing stages are ready to receive them,
  • Sink: a processing stage with exactly one input, requesting and accepting data elements
  • Flow: a processing stage with exactly one input and output, which connects its up- and downstream by moving/transforming the data elements flowing through it,
  • Runnable flow: A flow that has both ends attached to a Source and Sink respectively,
  • Materialized flow: An instantiation / incarnation / materialization of the abstract processing-flow template.

The abstractions above (Flow, Source, Sink, Processing stage) are used to create a processing-stream template or blueprint. When the template has a Source connected to a Sink with optionally some processing stages between them, such a template is called a Runnable Flow.

The materializer for akka-stream is the ActorMaterializer which takes the list of transformations comprising a akka.stream.scaladsl.Flow and materializes them in the form of org.reactivestreams.Processor instances, in which every stage is converted into one actor.

In akka-http parley, a 'Route' is a Flow[HttpRequest, HttpResponse, Unit] so it is a processing stage that transforms HttpRequest elements to HttpResponse elements.

Streams everywhere

The following reactive-streams are defined in akka-http:

  • Requests on one HTTP connection,
  • Responses on one HTTP connection,
  • Chunks of a chunked message,
  • Bytes of a message entity.

Route Directives

Akka http uses the route directives we know (and love) from Spray:

import akka.http.scaladsl._
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.stream.scaladsl._

def routes: Flow[HttpRequest, HttpResponse, Unit] =
  logRequestResult("akka-http-test") {
    path("") {
      redirect("person", StatusCodes.PermanentRedirect)
    } ~
    pathPrefix("person") {
      complete {
        Person("John Doe", 25)
      }
    } ~
    pathPrefix("ping") {
      complete {
        Ping(TimeUtil.timestamp)
      }
    }
  }

Spray-Json

I'm glad to see that akka-http-spray-json-experimental basically has the same API as spray:

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model.RequestEntity
import akka.http.scaladsl.unmarshalling.Unmarshal
import spray.json.DefaultJsonProtocol._
import spray.json._

case class Person(name: String, age: Int)

val personJson = """{"name":"John Doe","age":25}"""

implicit val personJsonFormat = jsonFormat2(Person)

Person("John Doe", 25).toJson.compactPrint shouldBe personJson

personJson.parseJson.convertTo[Person] shouldBe Person("John Doe", 25)

val person = Person("John Doe", 25)
val entity = Marshal(person).to[RequestEntity].futureValue
Unmarshal(entity).to[Person].futureValue shouldBe person

Custom Marshalling/Unmarshalling

Akka http has a cleaner API for custom types compared to Spray's. Out of the box it has support to marshal to/from basic types (Byte/String/NodeSeq) and so we can marshal/unmarshal from/to case classes from any line format. The API uses the Marshal object to do the marshalling and the Unmarshal object to to the unmarshal process. Both interfaces return Futures that contain the outcome.

The Unmarshal class uses an Unmarshaller that defines how an encoding like eg XML can be converted from eg. a NodeSeq to a custom type, like eg. a Person.

To Marshal class uses Marshallers to do the heavy lifting. There are three kinds of marshallers, they all do the same, but one is not interested in the MediaType , the opaque marshaller, then there is the withOpenCharset marshaller, that is only interested in the mediatype, and forwards the received HttpCharset to the marshal function so that the responsibility for handling the character encoding is up to the developer, and the last one, the withFixedCharset will handle only HttpCharsets that match the marshaller configured one.

An example XML marshaller/unmarshaller:

import akka.http.scaladsl.marshalling.{ Marshal, Marshaller, Marshalling }
import akka.http.scaladsl.model.HttpCharset
import akka.http.scaladsl.model.HttpCharsets._
import akka.http.scaladsl.model.MediaTypes._
import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller }

import scala.xml.NodeSeq

case class Person(name: String, age: Int)

val personXml =
  <person>
    <name>John Doe</name>
    <age>25</age>
  </person>

implicit val personUnmarshaller = Unmarshaller.strict[NodeSeq, Person] { xml 
  Person((xml \\ "name").text, (xml \\ "age").text.toInt)
}

val opaquePersonMarshalling = Marshalling.Opaque(()  personXml)
val openCharsetPersonMarshalling = Marshalling.WithOpenCharset(`text/xml`, (charset: HttpCharset)  personXml)
val fixedCharsetPersonMarshalling = Marshalling.WithFixedCharset(`text/xml`, `UTF-8`, ()  personXml)

val opaquePersonMarshaller = Marshaller.opaque[Person, NodeSeq] { person  personXml }
val withFixedCharsetPersonMarshaller = Marshaller.withFixedCharset[Person, NodeSeq](`text/xml`, `UTF-8`) { person  personXml }
val withOpenCharsetCharsetPersonMarshaller = Marshaller.withOpenCharset[Person, NodeSeq](`text/xml`) { (person, charset)  personXml }

implicit val personMarshaller = Marshaller.oneOf[Person, NodeSeq](opaquePersonMarshaller, withFixedCharsetPersonMarshaller, withOpenCharsetCharsetPersonMarshaller)

"personXml" should "be unmarshalled" in {
  Unmarshal(personXml).to[Person].futureValue shouldBe Person("John Doe", 25)
}

"Person" should "be marshalled" in {
  Marshal(Person("John Doe", 25)).to[NodeSeq].futureValue shouldBe personXml
}

Vendor specific media types

Versioning an API can be tricky. The key is choosing a strategy on how to do versioning. I have found and tried the following stragegies as blogged by Jim Liddell's blog, which is great by the way!

  1. 'The URL is king' in which the URL is encoded in the URL eg. http://localhost:8080/api/v1/person. The downside of this strategy is that the location of a resource may not change, and when we request another representation, the url does change eg. to http://localhost:8080/api/v2/person.
  2. Using a version request parameter like: http://localhost:8080/api/person?version=1. The downside of this stragegy lies in the fact that resource urls must be as lean as possible, and the only exception is for filtering, sorting, searching and paging, as stated by Vinay Sahni in his great blog 'Best Practices for Designing a Pragmatic RESTful API'.
  3. Both bloggers and I agree that using request headers for versioning, and therefor relying on vendor specific media types is a great way to keep the resource urls clean, the location does not change and in code the versioning is only a presentation responsibility, easilly resolved by an in scope mashaller.

When you run the example, you can try the following requests:

# The latest version in JSON
curl -H "Accept: application/json" localhost:8080/person
http :8080/person
# A stream of persons in CSV
curl -H "Accept: text/csv" localhost:8080/persons/stream/100
http :8080/persons/stream/100 Accept:text/csv
# A stream of persons in JSON
curl -H "Accept: application/json" localhost:8080/persons/stream/100
http :8080/persons/stream/100 Accept:application/json
# A list of of persons in JSON
curl -H "Accept: application/json" localhost:8080/persons/strict/100
http :8080/persons/strict/100 Accept:application/json
# The latest version in XML
curl -H "Accept: application/xml" localhost:8080/person
http :8080/person Accept:application/xml
# Vendor specific header for JSON v1
curl -H "Accept: application/vnd.acme.v1+json" localhost:8080/person
http :8080/person Accept:application/vnd.acme.v1+json
# Vendor specific header for JSON v2
curl -H "Accept: application/vnd.acme.v2+json" localhost:8080/person
http :8080/person Accept:application/vnd.acme.v2+json
# Vendor specific header for XML v1
curl -H "Accept: application/vnd.acme.v1+xml" localhost:8080/person
http :8080/person Accept:application/vnd.acme.v2+json
# Vendor specific header for XML v2
curl -H "Accept: application/vnd.acme.v2+xml" localhost:8080/person
http :8080/person Accept:application/vnd.acme.v2+xml

Please take a look at the Marshallers trait for an example how you could implement this strategy and the MarshallersTest how to test the routes using the Accept header and leveraging the media types.

Video

Slides

Northeast Scala Symposium 2015

Projects that use akka-http

About

A study project how akka-http works

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published