Operation

Operation with Representers

Last updated 29 October 2016

Operation::Representer

Representer are a concept from Representable and Roar and help to parse and render documents, as found in JSON or XML APIs.

Operations usually receive the params hash and pass this to the form’s #validate method. The same works with documents, with the exception that the form needs a representer to deserialize the document.

With Representer included, operations can infer a representer from the contract class. The representer can be further customized in the ::representer block.

class Song::Create < Trailblazer::Operation
  include Representer

  contract do
    property :name
    validates :name, presence: true
  end

  representer do
    # inherited :name
    include Roar::JSON::HAL

    link(:self) { song_path(represented.id) }
  end

  def process(params)
    validate(params[:song]) do # params[:song] is a JSON document.
      contract.save
    end
  end
end

Deserialization

You now invoke the operation with a JSON document, not with a hash anymore.

Song::Create.(song: '{"title": "Fury"}')

In Operation#validate, the incoming params[:song] will now be treated as a document.

The operation’s representer will be passed into the form’s validate and used as the deserializer, as it can read JSON and understands the format’s specific semantics.

If you prefer to use the params hash for deserialization, include Deserializer::Hash.

class Create < Trailblazer::Operation
  include Representer
  include Representer::Deserializer::Hash

You can now pass the params hash into operation call. This will still use the representer, but no JSON parsing will happen.

Validation

After deserialization/population is finished, validation and processing is analogue to a “normal” non-representer operation.

Rendering

The Representer module also imports to_json.

Song::Create.(song: '{"title": "Fury"}').to_json
#=> '{"title": "Fury","_links":{"self":"/songs/1"}}'

In to_json, the operation’s model will be passed to the representer and rendered using the representer.

For a better understanding, here are the pseudo mechanics.

module Representer
  def to_json(options={})
    self.class.representer. # retrieve operation's representer.
      new(represented).     # instantiate decorator. #represented returns #model.
      to_json(options)      # call decorator's rendering.
  end
end

If you want to render the contract instead (or anything else), override Operation#represented.

class Show < Trailblazer::Operation
  include Trailblazer::Operation::Representer

  def represented
    contract
  end

Passing Options

Note that you can also pass your own options to the rendering.

class Show < Trailblazer::Operation
  include Trailblazer::Operation::Representer

  def to_json(*)
    super(
      include:      [:title, :comments],
      user_options: { is_admin: policy.signed_in? }
    )
  end

To learn how Representable processes options, read the docs.

Input and Output Representers

The idea when including Representer is that you want the same representer to deserialize input and render the response document.

Sometimes, this is not desired, and you want to use different representers for handling input and output.

To use a representer for rendering, only, include Rendering.

class Create < Trailblazer::Operation
  extend Representer::DSL
  include Representer::Rendering

  representer do # will be used in #to_json.
    property :genre, as: :songGenre
  end

This will only add Operation#to_json. Parsing will still be done without a representer.

Alternatively, you can implement the rendering yourself by only including a Deserializer module.

class Create < Trailblazer::Operation
  extend Representer::DSL
  include Representer::Deserializer::Hash

  representer do # will be used in #validate.
    property :genre, as: :songGenre
  end

  def to_json(*)
    Create::Representer.new(model).to_json
  end

Note that you need to always extend Representer::DSL to import the representer block.

Composable Interface

You can set your own representer class if you don’t want it to be inferred.

class Create < Trailblazer::Operation
  self.representer_class = SongRepresenter

Responder

The Operation::Responder module adds methods to use an operation instance directly with Rails responders. This is why this module comes from the trailblazer-rails gem.

class Create < Trailblazer::Operation
  include Responder
  include Representer

  representer ..
end

The operation can now be passed to respond_to.

def create
  op = Comment::Create.(params)
  respond_to op
end

This is done automatically when using Trailblazer::Controller#respond.

def create
  respond Comment::Create
end