Maps documents to Ruby objects and back.
Representable maps fragments in documents to attributes in Ruby objects and back. It allows parsing representations giving an object-oriented interface to the document. But that’s only half of it! Representable can also render documents from an object instance.
This keeps your representation knowledge in one place when implementing REST services and clients.
-
Bidirectional - rendering and parsing
-
OOP access to documents
-
Support for JSON and XML
-
Coercion support with virtus
Since you keep forgetting the heroes of your childhood you decide to implement a REST service for storing and querying those. You choose representable for handling representations.
gem 'representable'
Representations are usually defined using a module. This makes them super flexibly, you’ll see.
require 'representable/json' module HeroRepresenter include Representable::JSON property :forename property :surename end
By using #property we declare two simple attributes that should be considered when representing.
To use your representer include it in the matching class. Note that you could reuse a representer in multiple classes. The represented class must have getter and setter methods for each property.
class Hero attr_accessor :forename, :surename include Representable include HeroRepresenter end
Many people dislike including representers on class layer. You might also extend an object at runtime.
Hero.new.extend(HeroRepresenter)
Alternatively, if you don’t like modules (which you shouldn’t), declarations can be put into classes directly. We call that inline representers.
class Hero attr_accessor :forename, :surename include Representable::JSON property :forename property :surename end
Now let’s create and render our first hero.
peter = Hero.new peter.forename = "Peter" peter.surename = "Pan" peter.to_json #=> {"forename":"Peter","surename":"Pan"}
Those two properties are considered when rendering in #to_json.
The cool thing about Representable is: it works bidirectional. By declaring properties you can not only render but also parse!
hook = Hero.from_json('{"forename":"Captain","surename":"Hook"}') hook.forename #=> "Captain"
See how easy this is? You can use an object-oriented method to read from the document.
You need a second domain object. Every hero has a place it comes from.
class Location attr_accessor :title include Representable::JSON property :title end
Peter, where ya’ from?
neverland = Location.new neverland.title = "Neverland"
It makes sense to embed the location in the hero’s document.
module HeroRepresenter property :origin, :class => Location end
Using the :class
option allows you to include other representable objects.
peter.origin = neverland peter.to_json #=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"}}
Don’t forget how easy it is to parse nested representations.
hook = Hero.from_json('{"name":"Captain","surename":"Hook","origin":{"title":"Dark Ocean"}}') hook.origin.inspect #=> #<Location:0x910d7c8 @title="Dark Ocean"> hook.origin.title #=> "Dark Ocean"
Representable just creates objects from the parsed document - nothing more and nothing less.
Heroes have features, special abilities that make ‘em a superhero.
module HeroRepresenter collection :features end
The second representable API method is collection
and, well, declares a collection.
peter.features = ["stays young", "can fly"] peter.to_json #=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"]}
Ok, things start working out. Your hero has a name, an origin and a list of features so far. Why not allow adding buddies to Peter - nobody wants to be alone!
module HeroRepresenter collection :friends, :class => Hero end
Again, we type the collection by using the :class
option.
nick = Hero.new nick.forename = "Nick" el = Hero.new el.forename = "El" peter.friends = [nick, el]
I always wanted to be Peter’s bro… in this example it is possible!
peter.to_json #=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"],"friends":[{"name":"Nick"},{"name":"El"}]}
Hashes can be represented the same way collections work. Here, use the #hash class method.
Need an array represented without any wrapping?
["stays young", "can fly"].extend(Representable::JSON::Collection).to_json #=> "[\"stays young\", \"can fly\"]"
You can use #items to configure the element representations contained in the array.
module FeaturesRepresenter include Representable::JSON::Collection items :class => Hero, :extend => HeroRepresenter end
Collections and hashes can also be deserialized.
The same goes with hashes where #values lets you configure the hash’s values.
module FriendsRepresenter include Representable::JSON::Hash values :class => Hero, :extend => HeroRepresenter end {:stu => Hero.new("Stu"), :clive => Hero.new("Cleavage")}.extend(FriendsRepresenter).to_json
Representable is designed to be very simple. However, a few tweaks are available. What if you want to wrap your document?
module HeroRepresenter self.representation_wrap = true end peter.to_json #=> {"hero":{"name":"Peter","surename":"Pan"}}
You can also provide a custom wrapper.
module HeroRepresenter self.representation_wrap = :boy end peter.to_json #=> {"boy":{"name":"Peter","surename":"Pan"}}
If your accessor name doesn’t match the attribute name in the document, use the :from
matcher.
module HeroRepresenter property :forename, :from => :i_am_called end peter.to_json #=> {"i_am_called":"Peter","surename":"Pan"}
Representable allows you to skip and include properties when rendering or parsing.
peter.to_json(:include => :forename) #=> {"forename":"Peter"}
It gives you convenient :exclude
and :include
options.
You can also define conditions on properties on the class layer.
module HeroRepresenter property :friends, :if => lambda { forename == "Peter" } end When rendering or parsing, the +friends+ property is considered only if the condition block evals to true. Note that the block is executed in instance context, giving you access to instance methods.
Representers roughly follow the DCI pattern when used on objects, only.
Hero.new.extend(HeroRepresenter)
The only difference is that you have to define which representers to use for typed properties.
module HeroRepresenter property :forename property :surename collection :features property :origin, :class => Location collection :friends, :class => Hero, :extend => HeroRepresenter end
There’s no need to specify a representer for the origin
property since the Location
class statically includes its representation. For friends
, we can use :extend
to tell representable which module to mix in dynamically.
Representable allows declaring a document’s syntax and structure while having different formats. Currently, it ships with JSON and XML bindings.
class Hero include Representable::XML end peter.to_xml #=> <hero> <name>Peter</name> <surename>Pan</surename> <location> <title>Neverland</title> </location> <hero> <name>Nick</name> </hero> <hero> <name>El</name> </hero> </hero>
The #to_xml method gives us an XML representation of Peter - great!
You can also map properties to tag attributes in representable.
class Hero attr_accessor :name include Representable::XML property :name, :attribute => true end Hero.new(:name => "Peter Pan").to_xml #=> <hero name="Peter Pan" />
Naturally, this works for both ways.
If you fancy coercion when parsing a document you can use the Coercion module which uses virtus for type conversion. Due to some virtus’ nature this can be used on class layer with inline representers, only for now.
Include virtus in your Gemfile, first.
gem 'virtus'
Use the :type
option to specify the conversion target. Note that :default
still works.
class Hero include Representable::JSON include Virtus extend Representable::Coercion::ClassMethods property :born_at, :type => DateTime, :default => "May 12th, 2012" end
Instead of spreading knowledge about your representations about the entire framework, Representable keeps rendering and parsing representations in one single, testable asset. It is a new abstraction layer missing in many “RESTful” frameworks.
Representable was written with REST representations in mind. However, it is a generic module for working with documents. If you do consider using it for a REST project, check out the Roar framework, which comes with representers, built-in hypermedia support and more. It internally uses Representable and streamlines the process for building hypermedia-driven REST applications.
Representable is a heavily simplified fork of the ROXML gem. Big thanks to Ben Woosley for his inspiring work.
-
Copyright © 2011 Nick Sutterer <[email protected]>
-
ROXML is Copyright © 2004-2009 Ben Woosley, Zak Mandhro and Anders Engstrom.