Last active
September 1, 2015 23:44
-
-
Save mmollaverdi/de79ede5d9054f75b72a 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._ | |
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" | |
} | |
*/ | |
} | |
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 😄
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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).