= Representable
Maps documents to Ruby objects and back.
== Introduction
_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.
== Features
* Bidirectional - rendering and parsing
* OOP documents
* Support for JSON, XML and MessagePack
== Example
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'
== Defining Representations
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.
class Hero
attr_accessor :forename, :surename
include Representable::JSON
property :forename
property :surename
end
== Rendering
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.
== Parsing
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.
== Nesting
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"}}
== Parsing Nested Documents
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 #=> #
hook.origin.title #=> "Dark Ocean"
Representable just creates objects from the parsed document - nothing more and nothing less.
== Simple Collections
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"]}
== Typed Collections
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"}]}
== Customizing
=== Wrapping
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"}}
=== Mapping
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"}
=== Filtering
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.
== DCI
Representers roughly follow the {DCI}[http://en.wikipedia.org/wiki/Data,_context_and_interaction] 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.
== XML support
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
#=>
Peter
Pan
Neverland
Nick
El
The #to_xml method gives us an XML representation of Peter - great!
=== Mapping tag attributes
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
#=>
Naturally, this works for both ways.
== More
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}[http://github.com/apotonick/roar], which comes with representers, built-in hypermedia support and more. It internally uses Representable and streamlines the process for building hypermedia-driven REST applications.
== Copyright
Representable is a heavily simplified fork of the ROXML gem. Big thanks to Ben Woosley for his inspiring work.
* Copyright (c) 2011 Nick Sutterer
* ROXML is Copyright (c) 2004-2009 Ben Woosley, Zak Mandhro and Anders Engstrom.