Presentability - Getting Started

To set up your own presentation layer, there are three steps:

  • Set up a Module as a presenter collection.

  • Declare one or more presenters within the collection.

  • Use the collection to present entities from a service or other limited interface.

For the purposes of this document, we’ll pretend we’re responsible for creating a JSON web service for Acme Widgets, Inc. We’ve declared all of our company’s code inside the Acme namespace. We’re using a generic Sinatra-like web framework that lets you declare endpoints like so:

get '/status_check' do |parameters|
    return { status: 'success' }.to_json
end

Create a Presenter Collection

A presenter collection is just a Module somewhere within your namespace that one can use to access the declared presenters. You designate it as a presenter collection simply by extending Presentability.

We’ll declare ours under Acme and call it Presenters:

require 'presentability'

require 'acme'

module Acme::Presenters
    extend Presentability

    # Presenters will be declared here

end # module Acme::Presenters

Since we haven’t declared any presenters, the collection isn’t really all that useful yet, so let’s declare some.

Declare Presenters

A presenter is an object that is responsible for constructing a representation of another object. There are a number of reasons to use a presenter:

  • Security: avoid exposing sensitive data from your domain objects to the public, e.g., passwords, internal prices, numeric IDs, etc.

  • Transform: normalize and flatten complex objects to some standard form, e.g., convert Time object timestamps to RFC 2822 form.

  • Consistency: change your model layer independently of your service’s entities, e.g., adding a new column to a model doesn’t automatically expose it in the service layer.

The representation is just a simple object that serves as an intermediate form for the transformed object until it is ultimately encoded. The default representation is an empty Hash, but you can customize it to suit your needs.

To declare a presenter, we’ll call the presenter_for method on the presenter collection module, and then call expose or expose_collection for each attribute that should be exposed.

The first argument to presenter_for is the type of object the presenter is for, which can be specified in a couple of different ways. The easiest is to just pass in the class itself. The domain class the Acme service is built around is the Widget, so let’s declare a presenter for it:

require 'acme/widget'

module Acme::Presenters
    extend Presentability

    presenter_for Acme::Widget do
        expose :name
    end

end # module Acme::Presenters

To present an object, call .present on the collection module with the object to be presented, and it will return a representation that is a Hash with a single :name key-value pair:

widget = Acme::Widget.where( name: 'The Red One' )
Acme::Presenters.present( widget )
# => { name: "The Red One" }

If we want to add a sku field to all widgets served by our service, we just add another exposure:

expose :sku
widget = Acme::Widget.where( name: 'The Red One' )
Acme::Presenters.present( widget )
# => { name: "The Red One", sku: 'DGG-17044-0822' }

Overriding Exposures

Sometime you want to alter the value that appears for a particular field. Say, for example, that the SKU that we exposed in our Widget presenter has an internal-only suffix in the form: -xxxx that we’d like to avoid exposing in a public-facing service. We can accomplish this by adding a block to it that alters the field from the model. Inside this block, the original object can be accessed via the subject method, so we can call the original #sku method and truncate it:

expose :sku do
    original = self.subject.sku
    return original.sub(/-\d{4}/, '')
end

Now the last part of the SKU will be removed in the representation:

widget = Acme::Widget.where( name: 'The Red One' )
Acme::Presenters.present( widget )
# => { name: "The Red One", sku: 'DGG-17044' }

Exposure Options

You can also pass zero or more options as a keyword Hash when presenting:

Acme::Presenters.present( widget, internal: true )

There are a few ways options can be used out of the box:

Exposure Aliases

Sometimes you want the field in the representation to have a different name than the method on the model object:

presenter_for Acme::Company do
    expose :id
    expose :legal_entity, as: :name
    expose :icon_url, as: :icon
end

In the representation, the #legal_entity method will be called on the Company being presented and the return value associated with the :name key, and the same for #icon_url and :icon:

{ id: 4, name: "John's Small Grapes", icon: "grapes-100.png" }

Conditional Exposure

You can make an exposure conditional on an option being passed or not:

# Don't include the price if presented with `public: true` option set
expose :price, unless: :public

# Only include the settings if presented with `detailed: true` option set
expose :settings, if: :detailed

Collection Exposure

A common use-case for conditional presentations is when you want an entity in a collection to be a less-detailed version. E.g.,

presenter_for Acme::User do
    expose :id
    expose :username
    expose :email

    expose :settings, unless: :in_collection
end

You can pass in_collection: true when you’re presenting, but you can also use the present_collection convenience method which sets it for you:

users = Acme::User.where( :activated ).limit( 20 )
Acme::Presenters.present_collection( users )
# => [{ id: 1, username: 'fran', email: 'fran@example.com'}, ...]

Custom Options

You also have access to the presenter options (via the #options method) in a overridden exposure block. With this you can build your own presentation logic:

presenter_for Acme::Widget do
    expose :name
    expose :sku

    expose :scancode do
        self.subject.make_scancode( self.options[:scantype] )
    end
end

# In your service:
widget = Acme::Widget[5]
Acme::Presenters.present( widget, scantype: :qr )
# { name: "Duck Quackers", sku: 'HBG-121-0424', scancode: '<qrcode data>'}

Declare Serializers

Oftentimes your model objects include values which are themselves not inherently serializable to your representation format. To help with this, you can also declare a “serializer” for one or more classes in your collection using the .serializer_for method:

require 'time' # for Time#rfc2822

module Acme::Presenters
    extend Presentability

    serializer_for :IPAddr, :to_s
    serializer_for Time, :rfc2822
    serializer_for Set, :to_a

end # module Acme::Presenters

Now when one of your models includes any of the given types, the corresponding method will be called on it and the result used as the value instead.

Use the Roda Plugin

If you’re using the excellent Roda web framework, Presentability includes a plugin for using it in your Roda application. To enable it, in your app just require your collection and enable the plugin. That will enable you to use #present and #present_collection in your routes:

require 'roda'
require 'acme/presenters'

class Acme::WebService < Roda

    plugin :presenters, collection: Acme::Presenters
    plugin :json

    route do |r|
        r.on "users" do
            r.is do
                # GET /users
                r.get do
                    users = Acme::User.where( :activated )
                    present_collection( users.all )
                end
            end
        end
    end
end # class Acme::WebService