Representable API
Last updated 05 May 2017 representable v3.0Representable API
In Representable, we differentiate between three APIs.
The declarative API is how we define representers. You can learn how to use those representers by reading about the very brief public API. Representable is extendable without having to hack existing code: the function API documents how to use its options to achieve what you need.
Declarative API
To render objects to documents or parse documents to objects, you need to define a representer.
A representer can either be a class (called decorator) or a module (called representer module). Throughout the docs, we will use decorators as they are cleaner and faster, but keep in mind you can also use modules.
require 'representable/json'
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :id
property :title
end
A representer simply defines the fields that will be mapped to the document using property
or collection
. You can then decorate an object and render or parse. Here’s an example.
# Given a Struct like this
Song = Struct.new(:id, :title) #=> Song
# You can instantiate it with the following
song = Song.new(1, "Fallout") #=> #<struct Song id=1, title="Fallout">
# This object doesn't know how to represent itself in JSON
song.to_json #=> NoMethodError: undefined method `to_json'
# But you can decorate it with the above defined representer
song_representer = SongRepresenter.new(song)
# Relax and let the representer do its job
song_representer.to_json #=> {"id":1,"title":"Fallout"}
The details are being discussed in the public API section.
Representer Modules
Instead of using classes as representers, you can also leverage modules which will then get mixed into the represented object.
A representer module is also a good way to share configuration and logic across decorators.
module SongRepresenter
include Representable::JSON
property :id
property :title
end
The API in a module representer is identical to decorators. However, the way you apply them is different.
song.extend(SongRepresenter).to_json #=> {"id":1,"title":"Fallout"}
There’s two drawbacks with this approach.
- You pollute the represented object with the imported representer methods (e.g.
to_json
). - Extending an object at run-time is costly and with many
extend
s there will be a notable performance decrease.
Throughout this documentation, we will use decorator as examples to encourage this cleaner and faster approach.
Collections
Not everything is a scalar value. Sometimes an object’s property can be a collection of values. Use collection
to represent arrays.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :id
property :title
collection :composer_ids
end
The new collection composer_ids
has to be enumeratable object, like an array.
Song = Struct.new(:id, :title, :composer_ids)
song = Song.new(1, "Fallout", [2, 3])
song_representer = SongRepresenter.new(song)
song_representer.to_json #=> {"id":1,"title":"Fallout","composer_ids":[2,3]}
Of course, this works also for parsing. The incoming composer_ids
will override the old collection on the represented object.
Nesting
Representable can also handle compositions of objects. This works for both property
and collection
.
For example, a song could nest an artist object.
Song = Struct.new(:id, :title, :artist)
Artist = Struct.new(:id, :name)
artist = Artist.new(2, "The Police")
song = Song.new(1, "Fallout", artist)
Here’s a better view of that object graph.
#<struct Song
id=1,
title="Fallout",
artist=#<struct Artist
id=2,
name="The Police">>
Inline Representer
The easiest way to nest representers is by using an inline representer.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :id
property :title
property :artist do
property :id
property :name
end
end
Note that you can have any levels of nesting.
Explicit Representer
Sometimes you want to compose two existing, stand-alone representers.
class ArtistRepresenter < Representable::Decorator
include Representable::JSON
property :id
property :name
end
To maximize reusability of representers, you can reference a nested representer using the :decorator
option.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :id
property :title
property :artist, decorator: ArtistRepresenter
end
This is identical to an inline representer, but allows you to reuse ArtistRepresenter
elsewhere.
Note that the :extend
and :decorator
options are identical. They can both reference a decorator or a module.
Nested Rendering
Regardless of the representer types you use, rendering will result in a nested document.
SongRepresenter.new(song).to_json
#=> {"id":1,"title":"Fallout","artist":{"id":2,"name":"The Police"}}
Nested Parsing
When parsing, per default Representable will want to instantiate an object for every nested, typed fragment.
You have to tell Representable what object to instantiate for the nested artist:
fragment.
class SongRepresenter < Representable::Decorator
# ..
property :artist, decorator: ArtistRepresenter, class: Artist
end
This happens via the :class
option. Now, the document can be parsed and a nested Artist
will be created by the parsing.
song = Song.new # nothing set.
SongRepresenter.new(song).
from_json('{"id":1,"title":"Fallout","artist":{"id":2,"name":"The Police"}}')
song.artist.name #=> "The Police"
The default behavior is - admittedly - very primitive. Representable’s parsing allow rich mapping, object creation and runtime checks. Read about populators to learn how that works.
Document Nesting
Not always does the structure of the desired document map to your objects. The ::nested
method allows structuring properties within a separate section while still mapping the properties to the outer object.
Imagine the following document. json_fragment = «END {“title”: “Roxanne”, “details”: {“track”: 3, “length”: “4:10”} } END
However, in the Song
class, there’s no such concept as details
.
Song = Struct.new(:title, :track, :length)
song = Song.new #=> #<struct Song title=nil, track=nil, length=nil>
Both track and length are properties of the song object itself. Representable gives you ::nested to map the virtual details
section to the song instance.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :title
nested :details do
property :track
property :length
end
end
song_representer = SongRepresenter.new(song)
song_representer.from_json(json_fragment)
Accessors for the nested properties will still be called on the song object. And as always, this works both ways - for rendering and parsing.
Wrapping
You can automatically wrap a document.
class SongRepresenter < Representable::Decorator
include Representable::JSON
self.representation_wrap= :song
property :title
property :id
end
This will add a container for rendering and parsing.
SongRepresenter.new(song).to_json
#=> {"song":{"title":"Fallout","id":1}}
Setting self.representation_wrap = true
will advice representable to figure out the wrap itself by inspecting the represented object class.
Note that representation_wrap
is a dynamic function option.
self.representation_wrap = ->(user_options:) { user_options[:my_wrap] }
This would allow to provide the wrap manually.
song_representer.to_json(user_options: { my_wrap: "hit" })
Suppressing Nested Wraps
When reusing a representer for a nested document, you might want to suppress its representation_wrap=
for the nested fragment.
Reusing SongRepresenter
from the last section in a nested setup allows suppressing the wrap via the :wrap
option.
class AlbumRepresenter < Representable::Decorator
include Representable::JSON
collection :songs,
decorator: SongRepresenter, # SongRepresenter defines representation_wrap.
wrap: false # turn off :song wrap.
end
The representation_wrap
from the nested representer now won’t be rendered or parsed…
Album = Struct.new(:songs)
album = Album.new
album.songs = [song]
AlbumRepresenter.new(album).to_json
.. and will result in:
{"songs":[{"title":"Fallout","id":1}]}
Otherwise it would respect the representation_wrap=
set in the nested decorator (SongRepresenter) and will render:
{"songs":[{"song":{"title":"Fallout","id":1}}]}
Note that this only works for JSON and Hash at the moment.
Inheritance
Properties can be inherited across representer classes and modules.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :id
property :title
end
What if you need a refined representer to also add the artist. Use inheritance.
class SongWithArtistRepresenter < SongRepresenter
property :artist do
property :name
end
end
All configuration from SongRepresenter
will be inherited, making the properties on SongWithArtistRepresenter
: id
, title
, and artist
. The original SongRepresenter
will stay as it is.
Artist = Struct.new(:name)
SongWithArtist = Struct.new(:id, :title, :artist)
artist = Artist.new("Ivan Lins")
song_with_artist = SongWithArtist.new(1, "Novo Tempo", artist)
# Using the same object with the two representers
song_representer = SongRepresenter.new(song_with_artist)
song_with_artist_representer = SongWithArtistRepresenter.new(song_with_artist)
song_representer.to_json
#=> {"id":1,"title":"Novo Tempo"}
song_with_artist_representer.to_json
#=> {"id":1,"title":"Novo Tempo","artist":{"name":"Ivan Lins"}}
Composition
You can also use modules and decorators together to compose representers.
module GenericRepresenter
include Representable::JSON
property :id
end
This can be included in other representers and will extend their configuration.
class SongRepresenter < Representable::Decorator
include GenericRepresenter
property :title
end
As a result, SongRepresenter
will contain the good old id
and title
property.
Overriding Properties
You might want to override a particular property in an inheriting representer. Successively calling property(name)
will override the former definition - exactly as you know it from overriding methods in Ruby.
class CoverSongRepresenter < SongRepresenter
include Representable::JSON
property :title, as: :name # overrides that definition.
end
Partly Overriding Properties
Instead of fully replacing a property, you can extend it with :inherit
. This will add your new options and override existing options in case the one you provided already existed.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :title, as: :name, render_nil: true
end
You can now inherit properties but still override or add options.
class CoverSongRepresenter < SongRepresenter
include Representable::JSON
property :title, as: :songTitle, default: "n/a", inherit: true
end
Using the :inherit, this will result in a property having the following options.
property :title,
as: :songTitle, # overridden in CoverSongRepresenter.
render_nil: true # inherited from SongRepresenter.
default: "n/a" # defined in CoverSongRepresenter.
The :inherit
option works for both inheritance and module composition.
Inherit With Inline Representers
:inherit
also works applied with inline representers.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :title
property :artist do
property :name
end
end
You can now override or add properties within the inline representer.
class HitRepresenter < SongRepresenter
include Representable::JSON
property :artist, inherit: true do
property :email
end
end
Results in a combined inline representer as it inherits.
property :artist do
property :name
property :email
end
Naturally, :inherit
can be used within the inline representer block.
Note that the following also works.
class HitRepresenter < SongRepresenter
include Representable::JSON
property :artist, as: :composer, inherit: true
end
This renames the property but still inherits all the inlined configuration.
Basically, :inherit
copies the configuration from the parent property, then merges in your options from the inheriting representer. It exposes the same behaviour as super
in Ruby - when using :inherit
the property must exist in the parent representer.
Feature
If you need to include modules in all inline representers automatically, register it as a feature.
class AlbumRepresenter < Representable::Decorator
include Representable::JSON
feature Link # imports ::link
link "/album/1"
property :hit do
link "/hit/1" # link method imported automatically.
end
Nested representers will include
the provided module automatically.
Execution Context
Readers and Writers for properties will usually be called on the represented
object. If you want to change that, so the accessors get called on the decorator instead, use :exec_context
.
class SongRepresenter < Representable::Decorator
property :title, exec_context: :decorator
def title
represented.name
end
end
Callable Options
While lambdas are one option for dynamic options, you might also pass a “callable” object to a directive.
class Sanitizer
include Uber::Callable
def call(represented, fragment, doc, *args)
fragment.sanitize
end
end
Note how including Uber::Callable
marks instances of this class as callable. No respond_to?
or other magic takes place here.
property :title, parse_filter: Santizer.new
This is enough to have the Sanitizer
class run with all the arguments that are usually passed to the lambda (preceded by the represented object as first argument).
Read/Write Restrictions
Using the :readable
and :writeable
options access to properties can be restricted.
property :title, readable: false
This will leave out the title
property in the rendered document. Vice-versa, :writeable
will skip the property when parsing and does not assign it.
Coercion
If you need coercion when parsing a document you can use the Coercion module which uses virtus for type conversion.
Include Virtus in your Gemfile, first.
gem 'virtus', ">= 0.5.0"
Use the :type
option to specify the conversion target. Note that :default
still works.
class SongRepresenter < Representable::Decorator
include Representable::JSON
include Representable::Coercion
property :recorded_at, type: DateTime, default: "May 12th, 2012"
end
Coercing values only happens when rendering or parsing a document. Representable does not create accessors in your model as virtus
does.
Note that we think coercion in the representer is wrong, and should happen on the underlying object. We have a rich coercion/constraint API for twins.
Symbol Keys
When parsing, Representable reads properties from hashes using their string keys.
song.from_hash("title" => "Road To Never")
To allow symbol keys also include the AllowSymbols
module.
class SongRepresenter < Representable::Decorator
include Representable::Hash
include Representable::Hash::AllowSymbols
# ..
end
This will give you a behavior close to Rails’ HashWithIndifferentAccess
by stringifying the incoming hash internally.
Defaults
The defaults
method allows setting options that will be applied to all property definitions of a representer.
class SongRepresenter < Representable::Decorator
include Representable::JSON
defaults render_nil: true
property :id
property :title
end
This will include render_nil: true
in both id
and title
definitions, as if you’d provided that option each time.
You can also have dynamic option computation at compile-time.
class SongRepresenter < Representable::Decorator
include Representable::JSON
defaults do |name, options|
{ as: name.camelize }
end
The options
hash combines the user’s and Representable computed options.
property :id, skip: true
defaults do |name, options|
options[:skip] ? { as: name.camelize } : {}
end
Note that the dynamic defaults
block always has to return a hash.
Combining those two forms also works.
class SongRepresenter < Representable::Decorator
include Representable::JSON
defaults render_nil: true do |name|
{ as: name.camelize }
end
All defaults are inherited to subclasses or including modules.
Standalone Hash
If it’s required to represent a bare hash object, use Representable::JSON::Hash
instead of Representable::JSON
.
This is sometimes called a lonely hash.
require "representable/json/hash"
class SongsRepresenter < Representable::Decorator
include Representable::JSON::Hash
end
You can then use this hash decorator on instances of Hash
.
hash = {"Nick" => "Hyper Music", "El" => "Blown In The Wind"}
SongsRepresenter.new(hash).to_json
#=> {"Nick":"Hyper Music","El":"Blown In The Wind"}
This works both ways.
A lonely hash starts to make sense especially when the values are nested objects that need to be represented, too. You can configure the nested value objects using the values
method. This works exactly as if you were defining an inline representer, accepting the same options.
class SongsRepresenter < Representable::Decorator
include Representable::JSON::Hash
values class: Song do
property :title
end
end
You can now represents nested objects in the hash, both rendering and parsing-wise.
hash = {"Nick" => Song.new("Hyper Music")}
SongsRepresenter.new(hash).to_json
In XML, use XML::Hash
. If you want to store hash attributes in tag attributes instead of dedicated nodes, use XML::AttributeHash
.
Standalone Collection
Likewise, you can represent lonely collections, instances of Array
.
require "representable/json/collection"
class SongsRepresenter < Representable::Decorator
include Representable::JSON::Collection
items class: Song do
property :title
end
end
Here, you define how to represent items in the collection using items
.
Note that the items can be simple scalar values or deeply nested objects.
ary = [Song.new("Hyper Music"), Song.new("Screenager")]
SongsRepresenter.new(ary).to_json
#=> [{"title":"Hyper Music"},{"title":"Screenager"}]
Note that this also works for XML.
Standalone Collection: to_a
Another trick to represent collections is using a normal representer with exactly one collection property named to_a
.
class SongsRepresenter < Representable::Decorator
include Representable::JSON # note that this is a plain representer.
collection :to_a, class: Song do
property :title
end
end
You can use this representer the way you already know and appreciate, but directly on an array.
ary = []
SongsRepresenter.new(ary).from_json('[{"title": "Screenager"}]')
In order to grab the collection for rendering or parsing, Representable will now call array.to_a
, which returns the array itself.
Automatic Collection Representer
Instead of explicitly defining representers for collections using a “lonely collection”, you can let Representable do that for you.
You define a singular representer, Representable will infer the collection representer.
Rendering a collection of objects comes for free, using for_collection
.
songs = Song.all
SongRepresenter.for_collection.new(songs).to_json
#=> '[{"title": "Sevens"}, {"title": "Eric"}]'
SongRepresenter.for_collection
will return a collection representer class.
For parsing, you need to provide the class for the nested items. This happens via collection_representer
in the representer class.
class SongRepresenter < Representable::Decorator
include Representable::JSON
property :title
collection_representer class: Song
end
You can now parse collections to Song
instances.
json = '[{"title": "Sevens"}, {"title": "Eric"}]'
SongRepresenter.for_collection.new([]).from_json(json)
Note: the implicit collection representer internally is implemented using a lonely collection. Everything you pass to ::collection_representer
is simply provided to the ::items
call in the lonely collection. That allows you to use :populator
and all the other goodies, too.
Automatic Singular and Collection
In case you don’t want to know whether or not you’re working with a collection or singular model, use represent
.
# singular
SongRepresenter.represent(Song.find(1)).to_json
#=> '{"title": "Sevens"}'
# collection
SongRepresenter.represent(Song.all).to_json
#=> '[{"title": "Sevens"}, {"title": "Eric"}]' ```
represent
figures out the correct representer for you. This works for parsing, too.
Public API
When decorating an object with a representer, the object needs to provide readers for every defined property
- and writers, if you’re planning to parse.
Accessors
In our small SongRepresenter
example, the represented object has to provide #id
and #title
for rendering.
Song = Struct.new(:id, :title)
song = Song.new(1, "Fallout")
Rendering
You can render the document by decorating the object and calling the serializer method.
SongRepresenter.new(song).to_json #=> {"id":1, title":"Fallout"}
When rendering, the document fragment is read from the represented object using the getter (e.g. Song#id
).
Since we use Representable::JSON
the serializer method is #to_json
.
For other format engines the serializer method will have the following name.
Representable::JSON#to_json
-
Representable::JSON#to_hash
(provides a hash instead of string) Representable::Hash#to_hash
Representable::XML#to_xml
Representable::YAML#to_yaml
Parsing
Likewise, parsing will read values from the document and write them to the represented object.
song = Song.new
SongRepresenter.new(song).from_json('{"id":1, "title":"Fallout"}')
song.id #=> 1
song.title #=> "Fallout"
When parsing, the read fragment is written to the represented object using the setter (e.g. Song#id=
).
For other format engines, the deserializing method is named analogue to the serializing counterpart, where to
becomes from
. For example, Representable::XML#from_xml
will parse XML if the format engine is mixed into the representer.
User Options
You can provide options when representing an object using the user_options:
option.
song_representer.to_json(user_options: { is_admin: true })
Note that the :user_options
will be accessible on all levels in a nested representer. They act like a “global” configuration and are passed to all option functions.
Here’s an example where the :if
option function evaluates a dynamic user option.
property :id, if: ->(options) { options[:user_options][:is_admin] }
This property is now only rendered or parsed when :is_admin
is true.
Using Ruby 2.1’s keyword arguments is highly recommended - to make that look a bit nicer.
property :id, if: ->(user_options:, **) { user_options[:is_admin] }
Nested User Options
Representable also allows passing nested options to particular representers. You have to provide the property’s name to do so.
song_representer.to_json(artist: { user_options: { is_admin: true } })
This will pass the option to the nested artist
, only. Note that this works with any level of nesting.
Include and Exclude
Representable supports two top-level options.
:include
allows defining a set of properties to represent. The remaining will be skipped.
song_representer.to_json(include: [:id]) #=> {"id":1}
The other, :exclude
, will - you might have guessed it already - skip the provided properties and represent the remaining.
song_representer.to_json(exclude: [:id, :artist])
#=> {"title":"Fallout"}
As always, these options work both ways, for rendering and parsing.
Note that you can also nest :include
and :exclude
.
song_representer.to_json(artist: { include: [:name] })
#=> {"id":1, "title":"Fallout", "artist":{"name":"Sting"}}