Representable: Function API
Last updated 05 May 2017 representable v3.0Function 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 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