This post introduces the new Roar gem, a framework-agnostic REST framework for Ruby that puts focus on object-oriented REST documents.
More object-orientation?
The calls for more object-orientation in Ruby frameworks are growing ever louder. Most web frameworks, like Rails, come with a nice database abstraction, a Rack-like HTTP endpoint, routing, and a template engine. It’s the lack of additional abstraction layers that make many programmers ask for “more OOP”.
Already yawning? Yeah, I wrote about that earlier this year. Let’s talk about working solutions now. Let’s talk about REST.
REST is about Documents!
REST is not only about pretty URLs. The problem ain’t URLs at all. Most frameworks have a decent routing layer to connect resource logic (aka controllers) to URLs. However, the key of REST is pushing documents between clients and servers, so why don’t we finally focus on documents?
It’s the document parsing, processing and rendering that makes our heads spinning.
Note: I here use the terms representation and document interchangable just because I can!
Parsing and Processing
Both clients and services have to parse incoming documents. Consider this JSON document.
{"title": "Lemon",
"colors": ["green"]}
This could be sent to a resource to create a new fruit, or be retrieved by some client which issued a GET request.
In Rails, and many HTTP gems, this is automatically parsed using a JSON gem.
params = JSON['{"title": "Lemon",
"colors": ["green"]}']
params["title"] #=> "Lemon"
params["colors"][0] #=> "green"
We’re using a hash access to read properties from that document. This is handy! Now, assume the sender forgot to include the colors.
params = JSON['{"title": "Lemon"}']
params["colors"][0]
#=>NoMethodError: undefined method `[]' for nil:NilClass
An exception, shit! While processing the parsed document (aka hash), we have to take care of missing keys, wrong types etc. We as the consumer have to know about all the pitfalls with this particular document.
Rendering
Now that our parsing and processing code is finished, how do we render outgoing representations? We could use #to_json.
@lemon.to_json(:only => [:title, :colors])
Being a quick solution for rendering small models this approach hits the wall when we have nested models but still want a plain JSON representation, or when we want to embed hypermedia. Also, we put “REST” logic into our database layer. It’s up to you to judge.
What about template languages? Let’s try jbuilder for JSON rendering.
Jbuilder.encode do |json|
json.title @lemon.title
json.colors @lemon.colors
end
Nice! But, wait. How do I test this? Can I test this separately? What if I want different representations for the same model? Do I need different templates? And… is it cool to have format knowledge both in our parsing layer and in our rendering layer? I mean, both need to know about the title property and the colors array.
Representers: OOP to our Documents!
I won’t further discuss the shown concepts here. Let me just criticize that they are not object-oriented and encourage the distribution of document internals about the entire framework.
Here’s how representers can handle all that and make you a happy man at the same time!.
require 'roar/representer/json'
module FruitRepresenter
include Roar::Representer::Base
property :title
collection :colors
end
This is all we need to declare the basic syntax of the fruit document. Note that it resides in a module which can be used anywhere in your app.
Rendering a Lemon
Now let’s say our database layer provides us a class Fruit with ActiveRecord-like behaviour.
lemon = Fruit.new(:title => "Lemon")
puts lemon.title #=> "Lemon"
Since the representer knows everything about the document format it can be used for rendering JSON. We just have to include the FruitRepresenter module into the class and new methods will be available.
Fruit.class_eval do # DB layer.
include Roar::Representer::JSON
include FruitRepresenter
end
lemon.to_json
#=> "{\"title\":\"Lemon\",\"colors\":[]}"
Awesome, the FruitRepresenter module gives us a #to_json method. Internally, this method compiles the JSON document by asking the represented object (aka lemon) for its properties and values. Remember, the representer knows about those properties, since we declared them. It does not know anything about ActiveRecord and the like.
Now, what about parsing?
Parsing a Lemon
The great thing about representers is: they work bidirectional. That alone is one of the main advantages to many other gems out there. Bidirectional? Let’s just see what that is.
orange = Fruit.from_json("{\"title\":\"Orange\"}")
puts orange.inspect
#=> #<Fruit:0x9df73a8 @title="Orange", @colors=[]>
We can parse representations and create objects from it! That’s bidirectional: rendering and parsing is combined in one representer.
Again, the representer does not know anything about the underlying database – it just creates an object it was mixed into, reads from the document and assigns properties using polite setters. Nothing more, bro.
Don’t like the class method? If you already have an instance, you can use #from_json on the instance.
orange.from_json("{\"title\":\"Yummy Orange\"}")
orange.title #=> "Yummy Orange"
Nesting Representers
Now that we have fruits, lets put ‘em in a bowl. A fruit bowl contains a list of fruits.
bowl = Bowl.new
bowl.items << lemon
bowl.items << orange
In order to bind the fruit bowl to a REST resource we write another representer module.
module BowlRepresenter
include Roar::Representer::Base
property :location
collection :items, :as => Fruit
end
Bowl.class_eval do # DB layer.
include Roar::Representer::JSON
include BowlRepresenter
end
Properties can be typed using the :as option. The target class must be a representer, too. That’s the only requirement.
bowl.to_json
#=> {"items":[
{"title":"Lemon","colors":[]},
{"title":"Orange","colors":[]}
]}
Typed properties allow us to build more complex representations without breaking with OOP. The collection assignment makes building lists as easy as possible. And if you still need to tweak things, there are some helpful configuration options around – more on that in one of the next posts.
Representers love HATEOAS
HATEOAS? Check out this talk I gave if you want to learn about Hypermedia and HATEOAS.
Hypermedia helps clients to operate the API without having to compute URLs. We want hypermedia in our documents.
module BowlRepresenter
include Roar::Representer::Feature::Hypermedia
link :self do
"http://bowls/#{location}"
end
end
Including the Hypermedia feature dynamically adds a new method #link to our representer module. What does it do?
bowl.location = :desk
bowl.to_json
#=> {
"items":[
{"title":"Lemon","colors":[]},
{"title":"Orange","colors":[]}],
"links":[{"rel":"self","href":"http://bowls/desk"}]
}
Seems like an easy way to embed links in our documents. In roar-rails the Rails URL helpers work like a charm within the link blocks – making it super simple to provide hypermedia to consumers and still being railsy.
Representers on the Client Side
Ok, representers basically are plain modules. Why not use ‘em on the client side as well? The client doesn’t have access to the model layer – if it does, something’s wrong! However, it’s absolutely ok to share the representer modules.
class Bowl
include Roar::Representer::JSON
include Roar::Representer::Feature::HttpVerbs
include BowlRepresenter # from a gem.
end
See how we simply use an empty class with the mixed-in representer on the client side? Remember, we don’t want to have access to the database here.
The next step is creating a new fruit bowl.
bowl = Bowl.new
bowl.location = "kitchen"
bowl.post!("http://bowls")
This code is enough to send a POST request to the specified URL. The bowl instance will serialize a JSON document, send it to the URL using the HttpVerbs feature, receive the new bowl, deserialize it and update its properties accordingly. You may know this behaviour from REST client gems like ActiveResource – here, with less code and manageable complexity.
Consume Hypermedia, it’s gooood.
The representer also makes it easy to extract hypermedia links from documents.
bowl.links[:self]
#=> "http://bowls/kitchen"
No parsing needed, the representer knows where to find your hypermedia. How cool is that?
XML is not a Crime!
Roar comes with both JSON and XML representers.
module BowlRepresenter
include Roar::Representer::XML
end
bowl.to_xml
#=> '<bowl>
# <link rel="self" href="http://bowl/kitchen"/>
#</bowl>'
Now choose if you want JSON or XML – it’s up to you! I still don’t understand the XML hating – did I miss something or is it just “too java”?
Roar – and more?
The Roar project is still evolving. However, the current API is almost stable. New features, new concepts, testing helpers, etc will be added over time just like in every framework. Roar is framework-agnostic and can be used anywhere, as long as it’s Ruby.
Its modulare feature architecture makes it easy to extend your documents. Currently, we have some exciting on-going brainstorming and hacking on features like client-side caching in the representer instance, generic error handling, validations, DSLs, … We need you to try it out, complain, propose, help, and spread the word. Thank you for reading until here, you rock!