pileworx / akka-http-hal

HAL (Hypermedia Application Language) specification support for akka-http

GitHub

akka-http-hal

HAL Specification support library for akka-http.

Licensed under the Apache 2 license.

Build Status Codacy Badge Codacy Badge

Getting Started

Installation:

libraryDependencies += "io.pileworx" %% "akka-http-hal" % "1.2.5"

Support for Scala 2.11, 2.12, 2.13.

Usage

Create your marshaller:

trait FooProtocol extends DefaultJsonProtocol {
  implicit val fooFormat = jsonFormat3(FooDto)
}

You can import a collection of IANA Relations (Self, Next, etc):

import io.pileworx.akka.http.rest.hal.Relations._

Create a resource adapter:

trait FooAdapter extends FooProtocol {
  def fooLink(rel: String, id: String) = rel -> Link(href = s"/foos/$id")
  def foosLink(rel: String) = rel -> Link(href = "/foos")

  def newResource(id: String): JsValue = {
    ResourceBuilder(
      withLinks = Some(Map(
        fooLink(Self, id),
        foosLink(Up)
      ))
    ).build
  }

  def notFoundResource: JsValue = {
    ResourceBuilder(
      withLinks = Some(Map(contactsLink(Up)))
    ).build
  }

  def toResources(foos: Seq[FooDto]): JsValue = {
    ResourceBuilder(
      withEmbedded = Some(Map(
        "foos" -> foos.map(f => toResource(f))
      )),
      withLinks = Some(Map(foosLink(Self)))
    ).build
  }

  def toResource(foo: FooDto): JsValue = {
    ResourceBuilder(
      withData = Some(foo.toJson),
      withLinks = Some(Map(
        fooLink(Self, foo.id),
        foosLink(Up)
      ))
    ).build
  }
}

Create your routes:

trait FooRestPort extends FooAdapter {

  val fooService = new FooService with FooComponent

  val fooRoutes = path("foos") {
    get {
      complete {
        fooService.getAll.map(f => toResources(f))
      }
    } ~
    post {
      entity(as[CreateFooCommand]) { newFoo =>
        complete {
          Created -> fooService.add(newFoo).map(id => newResource(id))
        }
      }
    }
  } ~
  pathPrefix("foos" / Segment) { id =>
    get {
      complete {
        fooService.getById(id).map {
          case Some(f) => Marshal(toResource(f)).to[HttpResponse]
          case _ => Marshal(NotFound -> notFoundResource).to[HttpResponse]
        }
      }
    }
  }
}

Curies Support

Curies are supported in two ways.

The first is per resource:

ResourceBuilder(
  withCuries = Some(Seq(
    Curie(name = "ts", href = "http://typesafe.com/{rel}")
))).build

The second, and most likely more common way, is to set them globally:

ResourceBuilder.curies(Seq(
  Curie(name = "ts", href = "http://typesafe.com/{rel}"),
  Curie(name = "akka", href = "http://akka.io/{rel}")
))

Note: If you mix global and resource based curies they will be combined. Currently we do not check for duplicate entries.

For the links pointing to a curie, just prefix the key with the curie name and colon (ex "ts:info"). If a colon is found in a key, we do not alter the href by adding X-Forwarded data or the request host/port.

Array of Links Support

If you require an array of links:

{
  "_links": {
    "multiple_links": [
      {
        "href": "http://www.test.com?foo=bar",
        "name": "one"
      },
      {
        "href": "http://www.test.com?bar=baz",
        "name": "two"
      }
    ]
  }
}

This can be achieved by using the Links class which accepts a Sequence of Link:

Map(
  "multiple_links" -> Links(Seq(
    Link(href = url, name = Some("one")),
    Link(href = url, name = Some("two"))
)))

HttpRequest Support

By default the HAL links will not include the host or port.

If you would like host, port, or path prefix included, provide the HttpRequest.

def toResource(foo: FooDto, req: HttpRequest): JsValue = {
  ResourceBuilder(
    withRequest = req,
    withData = Some(foo.toJson),
    withLinks = Some(Map(
      fooLink("self", foo.id),
      foosLink("parent")
    ))
  ).build
}

This will produce a link with either the current host's information OR construct the links based on the X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port, and X-Forwarded-Prefix headers.

HAL Browser

To expose HAL Browser from your API add the halBrowser route.

val routes = otherRoutes ~ halBrowserRoutes

The browser will be available at /halbrowser.

TODO

Find more contributors (hint).