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 access to documents

  • Support for JSON, XML and YAML.

  • Coercion support with virtus

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. We call that inline representers.

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. The document definition in your representer module can also be used for parsing documents and assigning property values.

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 #=> #<Location:0x910d7c8 @title="Dark Ocean">
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"}]}

Hashes

Hashes can be represented the same way collections work. Here, use the #hash class method.

Lonely Collections

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. Note that this also works for XML.

Lonely Hashes

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

In XML, if you want to store hash attributes in tag attributes instead of dedicated nodes, use XML::AttributeHash.

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 and Conditions

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.

False and Nil Values

Since 1.2 false values are considered when parsing and rendering. That particularly means properties that used to be unset (i.e. nil) after parsing might be false now. Vice versa, false values that weren’t included in the rendered document will be visible now.

If you want nil values to be included when rendering, use the :render_nil option.

property :surename, :render_nil => true

DCI

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.

XML support

Representable allows declaring a document’s syntax and structure while having different formats. Currently, it ships with JSON, XML and YAML 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!

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
#=> <hero name="Peter Pan" />

Naturally, this works for both ways.

Wrapping collections

It is sometimes unavoidable to wrap tag lists in a container tag.

module AlbumRepresenter
  include Representable::XML

  collection :songs, :from => :song, :wrap => :songs
end

Note that :wrap defines the container tag name.

Album.new.to_xml #=> 
  <album>
    <songs>
      <song>Laundry Basket</song>
      <song>Two Kevins</song>
      <song>Wright and Rong</song>
    </songs>
  </album>

YAML Support

Representers also come in handy if you need to render or parse YAML. The YAML module works exactly like the others.

module HotBandsRepresenter
  include Representable::YAML

  property :for
  collection :names
end

Now, just call #to_yaml to render or #from_yaml to parse.

HotBands.new(:for => "Nick", :names => ["Bad Religion", "Van Halen", "Mozart"]).
  extend(HotBandsRepresenter).
  to_yaml

#=> ---
for: Nick
names:
- Bad Religion
- Van Halen
- Mozart

Nested Objects

The YAML parser does handle nested objects just like JSON and XML does it.

Flow Style Lists

If you want flow style (aka inline style) lists, use the :style option. See www.yaml.org/spec/1.2/spec.html#id2790088 for more infos on flow sequences.

module HotBandsRepresenter
  include Representable::YAML

  collection :names, :style => :flow
end

#=> ---
names: [Bad Religion, Van Halen, Mozart]

Need anything else for YAML? Let me know.

Coercion

If you fancy coercion when parsing a document you can use the Coercion module which uses virtus for type conversion.

Include virtus in your Gemfile, first. Be sure to include virtus 0.5.0 or greater.

gem 'virtus'

Use the :type option to specify the conversion target. Note that :default still works.

module HeroRepresenter
  include Representable::JSON
  include Virtus
  include Representable::Coercion

  property :born_at, :type => DateTime, :default => "May 12th, 2012"
end

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, 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.