Last active
August 29, 2015 14:15
-
-
Save mmollaverdi/62eef644e65d9af444b4 to your computer and use it in GitHub Desktop.
Modeling a HAL Resource and providing JSON encoders for that in Scala - using Shapeless Heterogenous lists and Argonaut
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // The following, models a HAL Resource based on HAL specification: | |
| // http://stateless.co/hal_specification.html | |
| // And provides Argonaut JSON encoders for that model | |
| // (Argonaut is a purely functional Scala JSON library) | |
| // http://argonaut.io/ | |
| import shapeless._ | |
| import shapeless.ops.hlist.{ToTraversable, Mapper} | |
| import argonaut._, Argonaut._ | |
| /////////////////////////// | |
| // The model (case classes) | |
| /////////////////////////// | |
| // A HAL Resource has some links, some state and a list of embedded resources. | |
| // http://stateless.co/info-model.png | |
| // Embedded resources can each have different types of state, hence the use of shapeless Heterogenous lists. | |
| // The implicit LUBConstraint value puts a constraint on the elements of HList to be subtypes of HalEmbeddedResource. | |
| // I've simplified the model by assuming that embedded resources within a HAL resource cannot contain another level of embedded resources; | |
| // therefore avoiding recursion in my case class definitions. | |
| case class HalResource[T, L <: HList](links: List[HalLink], state: T, | |
| embeddedResources: L = HNil)(implicit c: LUBConstraint[L, HalEmbeddedResource[_]]) | |
| // Can be extended further to support templated links, as well as other link attributes such as name, title, type, etc. | |
| case class HalLink(rel: String, href: String) | |
| // Each embedded resource has a "rel" (relation) attribute which is used as the key name for that resource | |
| // inside "_embedded" tag in a HAL resource. | |
| case class HalEmbeddedResource[T](rel: String, embedded: EmbeddedResource[T]) | |
| // An embedded resource can be either a single resource (e.g. a single customer doucment embedded within an order document), | |
| // or an array of resources (e.g. order items) | |
| sealed trait EmbeddedResource[T] | |
| case class SingleEmbeddedResource[T](embedded: Embedded[T]) extends EmbeddedResource[T] | |
| case class ArrayEmbeddedResource[T](embedded: List[Embedded[T]]) extends EmbeddedResource[T] | |
| case class Embedded[X](links: List[HalLink], state: X) | |
| object HalResource { | |
| // This provides the implicit evidence that an empty HList (HNil) contains only elements which are of type HalEmbeddedResource[_] !!!!! | |
| implicit val hnilLUBConstraint: LUBConstraint[HNil.type, HalEmbeddedResource[_]] = | |
| new LUBConstraint[HNil.type, HalEmbeddedResource[_]] {} | |
| } | |
| ///////////////////////// | |
| // Argonaut Json Encoders | |
| ///////////////////////// | |
| object HalJsonEncoders { | |
| private def halLinkJsonAssoc: HalLink => JsonAssoc = { case HalLink(rel, href) => rel := Json.obj("href" := href) } | |
| implicit def EmbeddedJsonEncoder[T: EncodeJson]: EncodeJson[Embedded[T]] = EncodeJson[Embedded[T]] { | |
| case Embedded(links, state) => | |
| Json.obj("_links" -> jObjectAssocList(links.map(halLinkJsonAssoc))) | |
| .deepmerge(implicitly[EncodeJson[T]].apply(state)) | |
| } | |
| object HalEmbeddedResourceJsonAssoc extends Poly1 { | |
| implicit def default[T: EncodeJson, L[T] <: HalEmbeddedResource[T]] = at[L[T]] { | |
| case HalEmbeddedResource(rel, SingleEmbeddedResource(embedded)) => rel := embedded | |
| case HalEmbeddedResource(rel, ArrayEmbeddedResource(embedded)) => rel := embedded | |
| } | |
| } | |
| implicit def HalResourceWithNoEmbeddedResourcesJsonEncoder[T: EncodeJson, L <: HNil] | |
| : EncodeJson[HalResource[T, L]] = EncodeJson[HalResource[T, L]] { | |
| halResource => { | |
| val linksJson = jObjectAssocList(halResource.links.map(halLinkJsonAssoc)) | |
| val stateJson = implicitly[EncodeJson[T]].apply(halResource.state) | |
| Json.obj("_links" -> linksJson).deepmerge(stateJson) | |
| } | |
| } | |
| implicit def HalResourceWithEmbeddedResourcesJsonEncoder[T: EncodeJson, L <: HList, M <: HList] | |
| (implicit m: Mapper[HalEmbeddedResourceJsonAssoc.type, L] { type Out = M}, | |
| n: ToTraversable.Aux[M , List, JsonAssoc]): EncodeJson[HalResource[T, L]] = EncodeJson[HalResource[T, L]] { | |
| halResource => { | |
| val linksJson = jObjectAssocList(halResource.links.map(halLinkJsonAssoc)) | |
| val embeddedResourcesJson = jObjectAssocList(halResource.embeddedResources.map(HalEmbeddedResourceJsonAssoc).toList) | |
| val stateJson = implicitly[EncodeJson[T]].apply(halResource.state) | |
| Json.obj("_links" -> linksJson, "_embedded" -> embeddedResourcesJson).deepmerge(stateJson) | |
| } | |
| } | |
| } | |
| ///////////////////////////// | |
| // And this is how you use it | |
| ///////////////////////////// | |
| // First you need to define different type of States which you need in your HAL resource and embedded resources | |
| case class Property(id: String, address: String) | |
| case class Agent(id: String, name: String) | |
| case class Image(title: String) | |
| // Then provide Argonaut encoders for those types | |
| object StateJsonEncoders { | |
| implicit def PropertyEncoder = EncodeJson[Property] { p => ("id" := p.id) ->: ("address" := p.address) ->: jEmptyObject } | |
| implicit def ListerEncoder = EncodeJson[Agent] { l => ("id" := l.id) ->: ("name" := l.name) ->: jEmptyObject } | |
| implicit def ImageEncoder = EncodeJson[Image] { i => ("title" := i.title) ->: jEmptyObject } | |
| } | |
| // And at the end, create your HAL Resource object and use Argonaut to generate your HAL JSON String | |
| Object Test { | |
| import StateJsonEncoders._ | |
| import HalResource._ | |
| val embeddedOne = Embedded(links = List(HalLink("self", "/lister/1")), state = Agent("1", "Jim Smith")) | |
| val embeddedTwo = Embedded(links = List(HalLink("self", "/lister/2")), state = Agent("2", "Joe Bird")) | |
| val halEmbeddedResourceOne = HalEmbeddedResource(rel = "listers", embedded = ArrayEmbeddedResource(List(embeddedOne, embeddedTwo))) | |
| val embeddedThree = Embedded(links = List(HalLink("self", "/image/1")), state = Image("Floor Plan")) | |
| val halEmbeddedResourceTwo = HalEmbeddedResource(rel = "image", embedded = SingleEmbeddedResource(embeddedThree)) | |
| val halResource = HalResource(links = List(HalLink("self", "/property/1")), | |
| state = Property("1", "511 Church St, Richmond"), | |
| embeddedResources = halEmbeddedResourceOne :: halEmbeddedResourceTwo :: HNil) | |
| println(halResource.asJson.spaces2) | |
| // Will result in: | |
| /* | |
| { | |
| "id" : "1", | |
| "address" : "511 Church St, Richmond", | |
| "_links" : { | |
| "self" : { | |
| "href" : "/property/1" | |
| } | |
| }, | |
| "_embedded" : { | |
| "listers" : [ | |
| { | |
| "_links" : { | |
| "self" : { | |
| "href" : "/lister/1" | |
| } | |
| }, | |
| "id" : "1", | |
| "name" : "Jim Smith" | |
| }, | |
| { | |
| "_links" : { | |
| "self" : { | |
| "href" : "/lister/2" | |
| } | |
| }, | |
| "id" : "2", | |
| "name" : "Joe Bird" | |
| } | |
| ], | |
| "image" : { | |
| "_links" : { | |
| "self" : { | |
| "href" : "/image/1" | |
| } | |
| }, | |
| "title" : "Floor Plan" | |
| } | |
| } | |
| } | |
| */ | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment