-
-
Save mmollaverdi/de79ede5d9054f75b72a to your computer and use it in GitHub Desktop.
| // 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._ | |
| import scala.language.existentials | |
| import scala.language.higherKinds | |
| /////////////////////////// | |
| // 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. | |
| case class HalResource[T, L <: HList](links: List[HalLink], state: T, | |
| embeddedResources: L = HNil)(implicit c: LUBConstraint[L, HalEmbeddedResource[_, _]]) | |
| // TODO Add support for link array. Can also 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, L <: HList](rel: String, embedded: EmbeddedResource[T, L]) | |
| // 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, L] | |
| case class SingleEmbeddedResource[T, L <: HList](embedded: HalResource[T, L]) extends EmbeddedResource[T, L] | |
| case class ArrayEmbeddedResource[T, L <: HList](embedded: List[HalResource[T, L]]) extends EmbeddedResource[T, L] | |
| 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 HalLinkJsonEncoder: EncodeJson[HalLink] = EncodeJson[HalLink] { | |
| halLink => halLinkJsonAssoc(halLink) ->: jEmptyObject | |
| } | |
| object HalEmbeddedResourceJsonAssoc extends Poly1 { | |
| implicit def default[T: EncodeJson, L <: HList, H[U, M <: HList] <: HalEmbeddedResource[U, M]] | |
| (implicit halResourceEncoder: EncodeJson[HalResource[T, L]]) = at[H[T, L]] { | |
| halEmbeddedResource => { | |
| halEmbeddedResource match { | |
| 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 stateJsonAssociations = implicitly[EncodeJson[T]].apply(halResource.state).assoc.getOrElse(List()) | |
| Json.obj(("_links" -> linksJson :: stateJsonAssociations): _*) | |
| } | |
| } | |
| 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 embeddedResourcesJson = jObjectAssocList(halResource.embeddedResources.map(HalEmbeddedResourceJsonAssoc).toList) | |
| val linksJson = jObjectAssocList(halResource.links.map(halLinkJsonAssoc)) | |
| val stateJsonAssociations = implicitly[EncodeJson[T]].apply(halResource.state).assoc.getOrElse(List()) | |
| Json.obj(("_embedded" -> embeddedResourcesJson :: "_links" -> linksJson :: stateJsonAssociations): _*) | |
| } | |
| } | |
| } | |
| ///////////////////////////// | |
| // 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) | |
| case class Agency(id: String, name: String, address: 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 AgentEncoder = EncodeJson[Agent] { a => ("id" := a.id) ->: ("name" := a.name) ->: jEmptyObject } | |
| implicit def ImageEncoder = EncodeJson[Image] { i => ("title" := i.title) ->: jEmptyObject } | |
| implicit def AgencyEncoder = EncodeJson[Agency] { a => ("id" := a.id) ->: ("name" := a.name) ->: ("address" := a.address) ->: jEmptyObject } | |
| } | |
| // And at the end, create your HAL Resource object and use Argonaut to generate your HAL JSON String | |
| object Test extends App { | |
| import StateJsonEncoders._ | |
| import HalResource._ | |
| import HalJsonEncoders._ | |
| val secondLevelEmbedded = HalResource(links = List(HalLink("self", "/agency/1")), | |
| state = Agency("1", "Ray White", "Hawthorn")) | |
| val halSecondLevelEmbeddedResource = HalEmbeddedResource(rel = "agency", embedded = SingleEmbeddedResource( | |
| secondLevelEmbedded)) | |
| val embeddedOne = HalResource(links = List(HalLink("self", "/lister/1")), state = Agent("1", "Jim Smith"), | |
| embeddedResources = halSecondLevelEmbeddedResource :: HNil) | |
| val embeddedTwo = HalResource(links = List(HalLink("self", "/lister/2")), state = Agent("2", "Joe Bird"), | |
| embeddedResources = halSecondLevelEmbeddedResource :: HNil) | |
| val halEmbeddedResourceOne = HalEmbeddedResource(rel = "listers", embedded = ArrayEmbeddedResource( | |
| List(embeddedOne, embeddedTwo))) | |
| val embeddedThree = HalResource(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) | |
| val json = halResource.asJson.spaces2 | |
| println(json) | |
| // Will result in: | |
| /* | |
| { | |
| "_embedded" : { | |
| "listers" : [ | |
| { | |
| "_embedded" : { | |
| "agency" : { | |
| "_links" : { | |
| "self" : { | |
| "href" : "/agency/1" | |
| } | |
| }, | |
| "id" : "1", | |
| "name" : "Ray White", | |
| "address" : "Hawthorn" | |
| } | |
| }, | |
| "_links" : { | |
| "self" : { | |
| "href" : "/lister/1" | |
| } | |
| }, | |
| "id" : "1", | |
| "name" : "Jim Smith" | |
| }, | |
| { | |
| "_embedded" : { | |
| "agency" : { | |
| "_links" : { | |
| "self" : { | |
| "href" : "/agency/1" | |
| } | |
| }, | |
| "id" : "1", | |
| "name" : "Ray White", | |
| "address" : "Hawthorn" | |
| } | |
| }, | |
| "_links" : { | |
| "self" : { | |
| "href" : "/lister/2" | |
| } | |
| }, | |
| "id" : "2", | |
| "name" : "Joe Bird" | |
| } | |
| ], | |
| "image" : { | |
| "_links" : { | |
| "self" : { | |
| "href" : "/image/1" | |
| } | |
| }, | |
| "title" : "Floor Plan" | |
| } | |
| }, | |
| "_links" : { | |
| "self" : { | |
| "href" : "/property/1" | |
| } | |
| }, | |
| "id" : "1", | |
| "address" : "511 Church St, Richmond" | |
| } | |
| */ | |
| } | |
@benhutchison As discussed, an array embedded resource is modeled in a way that all the items in the array are of the same type, e.g. array of listers within a property/listing document, but as demonstrated in the example, you can still have different HalEmbeddedResource's of different type within your document (e.g. a single Image and a list of Agents).
Right. Got to admit it works. I think my discomfort comes from not fully understanding what shapeless is doing to make it work. Somewhere there must be a traversal of the hlist resolving all the component json typeclasses, and that traversal isnt explicitly visible in your solution
Good stuff!
@benhutchinson, the traversal is via the Mapper and Poly1 used in HalResourceWithEmbeddedResourcesJsonEncoder above.
@milessabin yep, we figured that out after watching one of your talks 😄
I think I have found a problem, shown in this fork: https://gist.github.com/benhutchison/0bab46ac6f0eaf5d9c77