Ruby On REST 3: One Model, Multiple Representations!
Monday, January 9th, 2012Happy New Year to all my fellow readers! I hope you had a good party!
In this post I wanna clarify why representers in Roar are much more versatile than one might think at first sight.
Model != Resource
I know, Rails teaches us that every resource is a model with exactly one representation. This is wrong. Not only can a resource be just anything, ranging from simple 1-to-1 mappings of database rows to virtual models like a workflow but also can your business models be represented in completely different contexts.
Let’s get back to our fruity example to learn more about this.
orange = Fruit.find_by_title("Orange") orange.extend(FruitRepresenter).to_json #=> {"title":"Orange","colors":["orange"]}
In the last discussion we had a Fruit and a Bowl model which were represented in their respective resources. While the fruit just contains some properties the bowl was a composition thereof. This is pretty “rails-conform” (I use this word with a negative undertone) and can simply be abstracted to hundreds of projects where people expose users, blog posts or events in their resources. This is what we call a 1-to-1 mapping of model to resource.
Let’s have some Booze!
We wrote a simple FruitRepresenter for mapping apples, lemons or oranges to documents, and we had the BowlRepresenter to provide documents for fruit collections. That was nice. But you can do more with fruits. Why not open a distillery and make some good booze from pears or grapes?
What that means for our REST API is that we have to represent fruits as ingredients. We also want to document what fruits are contained in a bottle of schnaps.
module IngredientRepresenter include Roar::Representer::JSON property :title collection :tastes_like end
The new IngredientRepresenter looks a bit similar to our FruitRepresenter, however, it represents a fruit in another context.
pear = Fruit.find_by_title("Pear") pear.extend(IngredientRepresenter).to_json #=> undefined method `tastes_like'
An exception. While the pear exposes a #title method it does not have a #tastes_like method. Why did I do that? I wanted to emphasize that the representer doesn’t guess anything from the represented object (the pear). It is up to the fruit to provide decent accessors like the #tastes_like reader. No magic or implicit semantics in Roar. And this is a good thing.
Another Context, Another Module.
We could easily push the missing methods into the IngredientRepresenter itself, but we will learn in an upcoming post that it is better to provide additional accessors in a separate module.
module Ingredient def tastes_like TastesLike.query(title) # returns array. end def tastes_like=(*) end end
In the reader, we query the world-famous TastesLike⢠web service. Since we don’t need a writer, the second method ain’t doing nothing.
pear.extend(Ingredient, IngredientRepresenter).to_json #=> {"title":"Pear","tastes_like":["pear","mango"]}
Cool, this is a fruit represented in a completely different context. What about the parsing, does that work, too?
apple = Fruit.new apple.extend(Ingredient, IngredientRepresenter) apple.from_json("{\"title\":\"Apple\", \"tastes_like\":[\"Pear\",\"Apple\",\"Mango\"]}") #=> #<Fruit title="Apple", colors=[]>
Yepp, parsing an ingredient and mapping it to a fruit also works fine.
Fruits in a Bottle.
Let’s see how collections can be represented in different contexts.
module BottleRepresenter include Roar::Representer::JSON collection :fruits, :class => Fruit, :extend => [Ingredient, IngredientRepresenter] end
The bottle representer knows that it’s composed of Fruit instances. It does use the IngredientRepresenter for parsing and rendering, though. BTW, if you don’t like the long :extend assignment you are free to “hard-include” the modules directly in your models.
Summary
So what we’re doing here is having one model used in two completely different contexts: the conventional “fruit” context and the “ingredient” context. Imagine you’d be doing this with #as_json and different hash-parsing algorithms in your controllers – good luck.
