HAL Specification support library for akka-http.
Licensed under the Apache 2 license.
Installation:
libraryDependencies += "io.pileworx" %% "akka-http-hal" % "1.2.5"
Support for Scala 2.11, 2.12, 2.13.
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 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.
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"))
)))
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.
To expose HAL Browser from your API add the halBrowser route.
val routes = otherRoutes ~ halBrowserRoutes
The browser will be available at /halbrowser.
Find more contributors (hint).