Operation

Trailblazer::Rails

Last updated 08 December 2016 trailblazer-rails v0.4.0

This documents the version compatible with Trailblazer 1.1.

Operation::Controller

The Operation::Controller module provides four shorthand methods to run and present operations.

Note that you’re free to invoke operations manually at any time by invoking them directly.

It works in Rails but should also be fine in Sinatra, Lotus and other frameworks that expose a params object.

Overview

You have four methods to pick from.

  • Use #present if you’re only interested in presenting the operation’s model.
  • Use #form to render the form object. This will not run the operation.
  • Use #run to process incoming data using the operation (and present it afterwards).
  • Use #respond to process incoming data and present it using Rails’ respond_to.

Generics

Before the operation is invoked, the controller method process_params! is run. You can override that to normalize the incoming parameters.

You need to include the Controller module into the controller. This will import the form, present, run and respond methods.

class ApplicationController < ActionController::Base
  include Trailblazer::Operation::Controller
end

Run

Use #run to invoke the operation.

class CommentsController < ApplicationController
  def create
    run Comment::Create
  end
end

This runs the operation with params, sets @operation, @model and @form on the controller instance, and returns the operation instance.

Note that you can grab the operation and reassign it to another instance variable if you have multiple operation invocations.

class CommentsController < ApplicationController
  def create
    create_op = run Comment::Create # returns operation instance.
    @comment  = create_op.model
  end

The call stack in #run is as follows.

#run
  process_params!(params)
  result, op = Comment::Create.run(params)
  @operation = op
  @model     = op.model
  @form      = op.contract

First, you have the chance to normalize parameters. The controller’s params hash is then passed into the operation run. After that, operation and model are assigned to controller instance variables.

An optional block for #run is invoked only when the operation was valid.

class CommentsController < ApplicationController
  def create
    run Comment::Create do |op|
      flash[:notice] = "Success!" # only run for successful/valid operation.
    end
  end
end

Present

To setup an operation without running its #process method, use #present. This is often used if you only need the operation’s model for presentation.

class CommentsController < ApplicationController
  def show
    present Comment::Update
    # you have access to @operation and @model.
  end
end

This instantiates the operation with params, sets @operation and @model on the controller instance, and returns the operation instance.

Instead of running the operation, this will only instantiate the operation by passing in the controller’s params. In turn, this only runs the operation’s #setup (which embraces model finding logic).

The #present helper does not run #process and does not instantiate a contract (which will be faster than #form).

Note that you can grab the operation and reassign it to another instance variable if you have multiple operation invocations.

class CommentsController < ApplicationController
  def show
    update_op = present Comment::Update # returns operation instance.
    @comment  = update_op.model
  end

The call stack in #present is as follows.

#present
  op = Comment::Update.present(params)
  @operation = op
  @model     = op.model

Note that no @form is instantiated and assigned to the controller. If you need the form use the #form method.

Form

To render the operation’s form, use #form. This is identical to #present plus the contract is being instantiated.

class CommentsController < ApplicationController
  def new
    form Comment::Create
  end
end

This instantiates the operation with params and then sets @operation, @model and @form on the controller instance. After that, the form’s prepopulate! method is called and the form is ready for rendering.

The #form helper does not run #process.

It returns the form instance.

Note that you can grab the form and reassign it to another instance variable if you use multiple operations in your endpoint.

class CommentsController < ApplicationController
  def show
    @create_form = form Comment::Create # returns form instance.
    @post_form   = form Post::Create
  end

The call stack in #form is as follows.

#form(operation, options)
  op = Comment::Create.present(params)

  @operation = op
  @model     = op.model
  @form      = op.contract

  @form.prepopulate!(options)

Options for prepopulate!

Any options passed to #form are directly propagated to the form’s prepopulate! method, allowing you to use runtime data in prepopulators. Note that the original params object is available in this options hash, too.

form Comment::Create, color: "green"

This will result in the following hash being passed to prepopulate!.

class Comment::Create < Trailblazer::Operation
  contract do
    def prepopulate!(options)
      options[:color]  #=> "green"
      options[:params] #=> <ActionController::Params ..>

Respond

Rails-specific.

class CommentsController < ApplicationController
  respond_to :json

  def create
    respond Comment::Create
  end
end

This will do the same as #run, invoke the operation and then pass it to #respond_with.

op = Comment::Create.(params)
respond_with op

The operation needs to be prepared for the responder as the latter makes weird assumptions about the object being passed to respond_with. For example, the operation needs to respond to to_json in a JSON request. Read about Representer here.

If the operation class has Representer mixed in, the params hash will be slightly modified. As the operation’s model key, the request body document is passed into the operation.

params[:comment] = request.body
Comment::Create.(params)

By doing so the operation’s representer will automatically parse and deserialize the incoming document, bypassing Rails’ ParamsParser.

If you want the responder to compute URLs with namespaces, pass in the :namespace option.

respond Comment::Create, namespace: [:api]

This will result in a call respond_with :api, op.

Custom Params

If you want to manually hand in parameters to #run, #respond, #form or #present, use the params: option.

def create
  run Comment::Create, params: {comment: {body: "Always the same! Boring!"}}
end

Document Formats

Normally, Trailblazer will pass Rails’ params hash into any operation.

For operations that have Operation::Representer included, not a hash but the request body will be passed into the operation, keyed under the operation’s model_name.

Comment::Create.({comment: request.body.string})

This allows the operation’s representer to deserialize the document and populate the contract, bypassing Rails’ ParamsParser.

You can instruct Trailblazer not to do that and pass in the normal params hash if you don’t want that using the :is_document option.

def create
  run Comment::Create, is_document: false # will run Comment::Create(params)
end

Normalizing Params

Override #params! to return an arbitrary params object. This is called in #run, #respond, #present and #form before the operation is called.

class CommentsController < ApplicationController
private
  def params!(params)
    params.to_h # return arbitrary object.
  end
end

Override #process_params! to add or remove values to params before the operation is run. This is called in #run, #respond, #present and #form.

class CommentsController < ApplicationController
private
  def process_params!(params)
    params.merge!(current_user: current_user)
  end
end

Note that this is a mutual method where you’re changing the params object.