Skip to content

Instantly share code, notes, and snippets.

@benhutchison
Forked from mmollaverdi/HalResource.scala
Last active August 29, 2015 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benhutchison/0bab46ac6f0eaf5d9c77 to your computer and use it in GitHub Desktop.
Save benhutchison/0bab46ac6f0eaf5d9c77 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]
//*** The problem IMO: `embedded` is a homogeneous List where all the sub-resources must be of the same type
//Embedded resources with different types cannot be accomodated here without losing type information
//Another way of saying it: here type `L` cannot vary between different elements of `embedded`
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)
//*** Change state to an Image so the two embedded resources are not homogeneous ***
val embeddedTwo = HalResource(links = List(HalLink("self", "/lister/2")), state = Image("Floor Plan"),
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"
}
*/
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment