Representable
- Last updated 22 Aug 22
Representable maps objects to documents (rendering) and documents to objects (parsing) using representers. Representers define the document structure and the transformation to/from objects.
Representers can define deeply nested object graphs….
It is very popular amongst REST API developers as it tackles both sides of exposing APIs: rendering documents and deserializing incoming documents to object graphs using a very generic approach.
But it’s also very helpful as a generic data transformer. …
In case you’re looking towards implementing a REST API, check out Roar first, which adds hypermedia semantics, media formats and more to Representable
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.
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
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"}}
to_hash and from_hash
Function API
Both rendering and parsing have a rich API that allows you to hook into particular steps and change behavior.
If that still isn’t enough, you can create your own pipeline.
Overview
Function option are passed to property
.
property :id, default: "n/a"
Most options accept a static value, like a string, or a dynamic lambda.
property :id, if: ->(options) { options[:fragment].nil? }
The options
hash is passed to all options and has the following members.
{doc: doc, options: options, represented: represented, decorator: self}
options[:doc] | When rendering, the document as it gets created. When parsing, the entire document.
options[:fragment] | When parsing, this is the fragment read from the document corresponding to this property.
options[:input] | When rendering, this is the value read from the represented object corresponding to this property.
options[:represented] | The currently represented object.
options[:decorator] | The current decorator instance.
options[:binding] | The current binding instance. This allows to access the currently used definition, e.g. `options[:binding][:name]`.
options[:options] | All options that have been passed into the render or parse method.
options[:user_options] | The `:user_options` for the current representer. These are only the [nested options](#nested-user-options) from the user, for a particular representer.
In your option function, you can either receive the entire options hash and use it in the block.
if: ->(options) { options[:fragment].nil? }
Or, and that is the preferred way, use Ruby’s keyword arguments.
if: ->(fragment:, **) { fragment.nil? }
Options
Here’s a list of all available options.
:as |
Renames property |
:getter |
Custom getter logic for rendering |
:setter |
Custom setter logic after parsing |
:if |
Includes property when rendering/parsing when evaluated to true |
:reader |
Overrides entire parsing process for property |
:writer |
Overrides entire rendering process for property |
:skip_parse |
Skips parsing when evaluated to true |
:skip_render |
Skips rendering when evaluated to true |
:parse_filter |
Pipeline to process parsing result |
:render_filter |
Pipeline to process rendering result |
:deserialize |
Override deserialization of nested object |
:serialize |
Override serialization of nested object |
:extend |
Representer to use for parsing or rendering |
:prepare |
Decorate the represented object |
:class |
Class to instantiate when parsing nested fragment |
:instance |
Instantiate object directly when parsing nested fragment |
:render_nil |
Render nil values |
:render_empty |
Render empty collections |
As
If your property name doesn’t match the name in the document, use the :as option.
property :title, as: :name
This will render using the :as
value. Vice-versa for parsing
song.to_json #=> {"name":"Fallout","track":1}
Getter
When rendering, Representable calls the property’s getter on the represented object. For a property named :id
, this will result in represented.id
to retrieve the value for rendering.
You can override this, and instead of having Representable call the getter, run your own logic.
property :id, getter: ->(represented:, **) { represented.uuid.human_readable }
In the rendered document, you will find the UUID now where should be the ID.
decorator.to_json #=> {"id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"}
As helpful as this option is, please do not overuse it. A representer is not a data mapper, but a document transformer. If your underlying data model and your representers diverge too much, consider using a twin to simplify the representer.
Setter
After parsing has happened, the fragment is assigned to the represented object using the property’s setter. In the above example, Representable will call represented.id=(fragment)
.
Override that using :setter
.
property :id,
setter: ->(fragment:, represented:, **) { represented.uuid = fragment.upcase }
Again, don’t overuse this method and consider a twin if you find yourself using :setter
for every property.
If
You can exclude properties when rendering or parsing, as if they were not defined at all. This works with :if
.
property :id, if: ->(user_options:,**) { user_options[:is_admin] }
When parsing (or rendering), the id
property is only considered when is_admin
has been passed in.
This will parse the id
field.
decorator.from_json('{"id":1}', user_options: {is_admin: true})
Reader
To override the entire parsing process, use :reader
. You won’t have access to :fragment
here since parsing hasn’t happened, yet.
property :id,
reader: ->(represented:,doc:,**) { represented.payload = doc[:uuid] || "n/a" }
With :reader
, parsing is completely up to you. Representable will only invoke the function and do nothing else.
Writer
To override the entire rendering process, use :writer
. You won’t have access to :input
here since the value query to the represented object hasn’t happened, yet.
property :id,
writer: ->(represented:,doc:,**) { doc[:uuid] = represented.id }
With :writer
, rendering is completely up to you. Representable will only invoke the function and do nothing else.
Skip Parse
To suppress parsing of a property, use :skip_parse
.
property :id,
skip_parse: ->(fragment:,**) { fragment.nil? || fragment=="" }
No further processing happens with this property, should the option evaluate to true.
Skip Render
To suppress rendering of a property, use :skip_render
.
property :id,
skip_render: ->(represented:,**) { represented.id.nil? }
No further processing happens with this property, should the option evaluate to true.
Parse Filter
Use :parse_filter
to process the parsing result.
property :id,
parse_filter: ->(fragment, options) { fragment.strip }
Just before setting the fragment to the object via the setter, the :parse_filter
is called.
Note that you can add multiple filters, the result from the last will be passed to the next.
Render Filter
Use :render_filter
to process the rendered fragment.
property :id,
render_filter: ->(input, options) { input.strip }
Just before rendering the fragment into the document, the :render_filter
is invoked.
Note that you can add multiple filters, the result from the last will be passed to the next.
Deserialize
When deserializing a nested fragment, the default mechanics after decorating the represented object are to call represented.from_json(fragment)
.
Override this step using :deserialize
.
property :artist,
deserialize: ->(input:,fragment:,**) { input.attributes = fragment }
The :input
option provides the currently deserialized object.
Serialize
When serializing a nested object, the default mechanics after decorating the represented object are to call represented.to_json
.
Override this step using :serialize
.
property :artist,
serialize: ->(represented:,**) { represented.attributes.to_h }
Extend
Alias: :decorator
.
When rendering or parsing a nested object, that represented object needs to get decorated, which is configured via the :extend
option.
You can use :extend
to configure an explicit representer module or decorator.
property :artist, extend: ArtistRepresenter
Alternatively, you could also compute that representer at run-time.
For parsing, this could look like this.
property :artist,
extend: ->(fragment:,**) do
fragment["type"] == "rockstar" ? RockstarRepresenter : ArtistRepresenter
end
For rendering, you could do something as follows.
property :artist,
extend: ->(input:,**) do
input.is_a?(Rockstar) ? RockstarRepresenter : ArtistRepresenter
end
This allows a dynamic polymorphic representer structure.
Prepare
The default mechanics when representing a nested object is decorating the object, then calling the serializer or deserializer method on it.
You can override this step using :prepare
.
property :artist,
prepare: ->(represented:,**) { ArtistRepresenter.new(input) }
Just for fun, you could mimic the original behavior.
property :artist,
prepare: ->(represented:,binding:,**) { binding[:extend].new(represented) }
Class
When parsing a nested fragment, Representable per default creates an object for you. The class can be defined with :class
.
property :artist,
class: Artist
It could also be dynamic.
property :artist,
class: ->(fragment) { fragment["type"] == "rockstar" ? Rockstar : Artist }
Instance
Instead of using :class
you can directly instantiate the represented object yourself using :instance
.
property :artist,
instance: ->(fragment) do
fragment["type"] == "rockstar" ? Rockstar.new : Artist.new
end
Render Nil
In Representable, false
values are considered as a valid value and will be rendered into the document or parsed.
If you want nil
values to be included when rendering, use the :render_nil
option.
property :title,
render_nil: true
Render Empty
Per default, empty collections are rendered (unless they’re nil
). You can suppress rendering.
collection :songs,
render_empty: false
XML
If you’re enjoying the pleasure of working with XML, Representable can help you. It does render and parse XML, too, with an almost identical declarative API.
require "representable/xml"
class SongRepresenter < Representable::Decorator
include Representable::XML
property :title
collection :composers
end
Note that you have to include the Representable::XML
module.
The public API then gives you to_xml
and from_xml
.
Song = Struct.new(:title, :composers)
song = Song.new("Fallout", ["Stewart Copeland", "Sting"])
SongRepresenter.new(song).to_xml
<song>
<title>Fallout</title>
<composers>Stewart Copeland</composers>
<composers>Sting</composers>
</song>
Tag Attributes
You can also map properties to tag attributes in Representable. This works only for the top-level node, though (seen from the representer’s perspective).
class SongRepresenter < Representable::Decorator
include Representable::XML
property :title, attribute: true
collection :composers
end
SongRepresenter.new(song).to_xml
<song title="Fallout">
<composers>Stewart Copeland</composers>
<composers>Sting</composers>
</song>
Naturally, this works both ways.
Mapping Content
The same concept can also be applied to content. If you need to map a property to the top-level node’s content, use the :content
option. Again, top-level refers to the document fragment that maps to the representer.
class SongRepresenter < Representable::Decorator
include Representable::XML
property :title, content: true
end
SongRepresenter.new(song).to_xml
<song>Fallout</song>
Wrapping Collections
It is sometimes unavoidable to wrap tag lists in a container tag.
class AlbumRepresenter < Representable::Decorator
include Representable::XML
collection :songs, as: :song, wrap: :songs
end
Album = Struct.new(:songs)
album = Album.new(["Laundry Basket", "Two Kevins", "Wright and Rong"])
album_representer = AlbumRepresenter.new(album)
album_representer.to_xml
Note that :wrap
defines the container tag name.
<album>
<songs>
<song>Laundry Basket</song>
<song>Two Kevins</song>
<song>Wright and Rong</song>
</songs>
</album>
Namespace
Namespaces in XML allow the use of different vocabularies, or set of names, in one document. Read this great article to share our fascination about them.
Where’s the EXAMPLE CODE?
The Namespace
module is available in Representable >= 3.0.4. It doesn’t work with JRuby due to Nokogiri’s extremely complex implementation. Please wait for Representable 4.0 where we replace Nokogiri.
For future-compat: Namespace
only works in decorator classes, not modules.
Namespace: Default
You can define one namespace per representer using ::namespace
to set the section’s default namespace.
{{ "test/xml_namespace_test.rb:simple-class:../representable" | tsnippet }}
Nested representers can be inline or classes (referenced via :decorator
). Each class can maintain its own namespace.
Without any mappings, the namespace will be used as the default one.
{{ "test/xml_namespace_test.rb:simple-xml:../representable" | tsnippet }}
Namespace: Prefix
After defining the namespace URIs in the representers, you can map them to a document-wide prefix in the top representer via ::namespace_def
.
{{ "test/xml_namespace_test.rb:map-class:../representable" | tsnippet }}
Note how you can also use :namespace
to reference a certain differing prefix per property.
When rendering or parsing, the local property will be extended, e.g. /library/book/isbn
will become /lib:library/lib:book/lib:isbn
.
{{ "test/xml_namespace_test.rb:map-xml:../representable" | tsnippet }}
The top representer will include all namespace definitions as xmlns
attributes.
Namespace: Parse
Namespaces also apply when parsing an XML document to an object structure. When defined, only the known, prefixed tags will be considered.
{{ "test/xml_namespace_test.rb:parse-call:../representable" | tsnippet }}
In this example, only the /lib:library/lib:book/lib:character/hr:name
was parsed.
{{ "test/xml_namespace_test.rb:parse-res:../representable" | tsnippet }}
If your incoming document has namespaces, please do use and specify them properly.
Namespace: Remove
If an incoming document contains namespaces, but you don’t want to define them in your representers, you can automatically remove them.
class AlbumRepresenter < Representable::Decorator
include Representable::XML
remove_namespaces!
end
This will ditch the namespace prefix and parse all properties as if they never had any prefix in the document, e.g. lib:author
becomes author
.
Removing namespaces is a Nokogiri hack. It’s absolutely not recommended as it defeats the purpose of XML namespaces and might result in wrong values being parsed and interpreted.
YAML
Representable also comes with a YAML representer. Like XML, the declarative API is almost identical.
Flow Style Lists
A nice feature is that #collection
also accepts a :style
option which helps having nicely formatted inline (or “flow”) arrays in your YAML - if you want that!
require 'representable/yaml'
class SongRepresenter < Representable::Decorator
include Representable::YAML
property :title
property :id
collection :composers, style: :flow
end
Public API
To render and parse, you invoke to_yaml
and from_yaml
.
Song = Struct.new(:title, :id, :composers)
song = Song.new("Fallout", 1, ["Stewart Copeland", "Sting"])
SongRepresenter.new(song).to_yaml
---
title: Fallout
id: 1
composers: [Stewart Copeland, Sting]
Debugging
Representable is a generic mapper using recursions, pipelines and things that might be hard to understand from the outside. That’s why we got the Debug
module which will give helpful output about what it’s doing when parsing or rendering.
You can extend objects on the run to see what they’re doing.
SongRepresenter.new(song).extend(Representable::Debug).from_json("..")
SongRepresenter.new(song).extend(Representable::Debug).to_json
It’s probably a good idea not to do this in production.
Upgrading Guide
We try to make upgrading as smooth as possible. Here’s the generic documentation, but don’t hesitate to ask for help on Gitter.
2.4 to 3.0
- The 3.0 line runs with Ruby >2.0, only. This is to make extensive use of keyword arguments.
- All deprecations from 2.4 have been removed.
to_hash(user_options: {}) ->(options) { options[:user_options] } ->(user_options:,**) { user_options }
2.3 to 2.4
The 2.4 line contains many new features and got a major internal restructuring. It is a transitional release with deprecations for all changes.
Breakage
:render_filter => lambda { |val, options| "#{val.upcase},#{options[:doc]},#{options[:options][:user_options]}" }
Deprecations
Once your code is migrated to 2.4, you should upgrade to 3.0, which does not have deprecations anymore and only supports Ruby 2.0 and higher.
If you can’t upgrade to 3.0, you can disable slow and annoying deprecations as follows.
Representable.deprecations = false
Positional Arguments
For dynamic options like :instance
or :getter
we used to expose a positional API like instance: ->(fragment, options)
where every option has a slightly different signature. Even worse, for collection
s this would result in a differing signature plus an index like instance: ->(fragment, index, options)
.
From Representable 2.4 onwards, only one argument is passed in for all options with an identical, easily memoizable API. Note that the old signatures will print deprecation warnings, but still work.
For parsing, this is as follows (:instance
is just an example).
property :artist, instance: ->(options) do
options[:input]
options[:fragment] # the parsed fragment
options[:doc] # the entire document
options[:result] # whatever the former function returned,
# usually this is the deserialized object.
options[:user_options] # options passed into the parse method (e.g. from_json).
options[:index] # index of the currently iterated fragment (only with collection)
end
We highly recommend to use keyword arguments if you’re using Ruby 2.1+.
property :artist, instance: ->(fragment:, user_options:, **) do
User Options
When passing dynamic options to to_hash
/from_hash
and friends, in older version you were allowed to pass in the options directly.
decorator.to_hash(is_admin: true)
This is deprecated. You now have to use the :user_options
key to make it compatible with library options.
decorator.to_hash(user_options: { is_admin: true })
Pass Options
The :pass_options
option is deprecated and you should simply remove it, even though it still works in < 3.0. You have access to all the environmental object via options[:binding]
.
In older version, you might have done as follows.
property :artist, pass_options: true,
instance: ->(fragment, options) { options.represented }
Runtime information such as represented
or decorator
is now available via the generic options.
property :artist, instance: ->(options) do
options[:binding] # property Binding instance.
options[:binding].represented # the represented object
options[:user_options] # options from user.
end
The same with keyword arguments.
property :artist, instance: ->(binding:, user_options:, **) do
binding.represented # the represented object
end
Parse Strategy
The :parse_strategy
option is deprecated in favor of :populator
. Please replace all occurrences with the new populator style to stay cool.
If you used a :class
proc with :parse_strategy
, the new API is class: ->(options)
. It used to be class: ->(fragment, user_options)
.
Class and Instance
In older versions you could use :class
and :instance
in combination, which resulted in hard-to-follow behavior. These options work exclusively now.
SkipRender
skip_render: lambda { |options|
# raise options[:represented].inspect
options[:user_options][:skip?] and options[:input].name == "Rancid"
Binding
The :binding
option is deprecated and will be removed in 3.0. You can use your own pipeline and replace the WriteFragment
function with your own.