{{ page.title }}

  • Last updated 05 May 2017
  • representable v3.0

Operation

  • Last updated 05 May 2017
  • activity v1.1.1.1.0

Overview

Operations have been a central element since Trailblazer 1.0. An operation models a top-level function of your application, such as creating a user or archiving a blog post. It’s the outer-most object that is directly invoked by the controller, embraces all business-specific logic and hides it from the latter.

Operations are often confused as god objects that do “everything”. However, operations are nothing but orchestrators. Where to implement the actual code and when to call it is up to the developer.

Since Trailblazer 2.1, operations are reduced to being a very thin, public API around an Activity with some pre-defined configuration such as the FastTrack-railway.

Deeply nested business logic is implemented using activities. For background-compatibility, you may still use an operation for the upper-most logic, but internally, it boils down to being an Activity::FastTrack.

  • Two public Operation.call signatures: “public call” and circuit interface. *

Invocation

An operation has two invocation styles. This is the only difference to an Activity.

Until TRB 2.1, the public call was the only way to invoke an operation. Its signature is simpler, but less powerful.


result = Memo::Create.(params: {text: "Enjoy an IPA"})

puts result.success?    #=> true

model = result[:model]
puts model.text         #=> "Enjoy an IPA"

The public call will return a [result object] that exposes the binary state (success? or failure?). All variables written to the context are accessable via the #[] reader.

Since operations are just normal activities under the hood, they also expose the [circuit interface]. This allows using all advanced features such as [taskWrap], [tracing] or nesting operations with the generic activity mechanics.


ctx = {params: {text: "Enjoy an IPA"}}
signal, (ctx, _) = Memo::Create.([ctx, {}], {})

puts signal #=> #<Trailblazer::Activity::Railway::End::Success semantic=:success>

Wiring

An operation is simply an Activity::FastTrack subclass and all [DSL implications are identical].


class Create < Trailblazer::Operation
  step :validate, fast_track: true
  fail :log_error
  step :create

  # ...
end

An operation always allows you the fast-track outputs and wiring.

For DSL options, refer to [Fast Track].

Result

An operation invoked with public call will return an Operation::Result object for your convenience. It’s nothing but a container exposing the binary state (or outcome) plus the ctx object that was passed around in the circuit.


class Create < Trailblazer::Operation
  step :validate, fast_track: true
  fail :log_error
  step :create

  def create(ctx, **)
    ctx[:model] = Memo.new
  end
  # ...
  # ...

The result exposes state and the context you wrote to.


result.success? #=> true
result[:model]  #=> #<Memo ..>

The operation ending on a “failure” end (End.failure, End.fail_fast) will result in result.failure? being true. All other outcomes will be interpreted as success.

Please note that the result object is not compatible with the circuit interface and only here for backward-compatibility, when invoking operations manually.

In compositions or workflows, operations will always be called using the circuit interface.

Macros

Trailblazer ships with a handful of step functions called macros. Those are implemented in trailblazer-macro and trailblazer-macro-contract.

Model

An operation can automatically find or create a model for you depending on the input, with the Model macro.


class Create < Trailblazer::Operation
  step Model( Song, :new )
  # ..
end

After this step, there is a fresh model instance under options[:model] that can be used in all following steps.


result = Create.(params: {})
result[:model] #=> #<struct Song id=nil, title=nil>

Internally, Model macro will simply invoke Song.new to populate :model.

You can also find models using :find_by. This is helpful for Update or Delete operations.


class Update < Trailblazer::Operation
  step Model( Song, :find_by )
  # ..
end

The Model macro will invoke the following code for you.


options[:model] = Song.find_by( params[:id] )

This will assign [:model] for you by invoking find_by.


result = Update.(params: { id: 1 })
result[:model] #=> #<struct Song id=1, title="nil">

If Song.find_by returns nil, this will deviate to the left track, skipping the rest of the operation.


result = Update.(params: {})
result[:model] #=> nil
result.success? #=> false

Note that you may also use :find. This is not recommended, though, since it raises an exception, which is not the preferred way of flow control in Trailblazer.

It’s possible to specify any finder method, which is helpful with ROMs such as Sequel.


class Show < Trailblazer::Operation
  step Model( Song, :[] )
  # ..
end

The provided method will be invoked and Trailblazer passes it the params[:id] value.


Song[ params[:id] ]

Given your database gem provides that finder, it will result in a successful query.


result = Show.(params: { id: 1 })
result[:model] #=> #<struct Song id=1, title="Roxanne">

Wrap

Steps can be wrapped by an embracing step. This is necessary when defining a set of steps to be contained in a database transaction or a database lock.


class Memo::Create < Trailblazer::Operation
  step :find_model
  step Wrap( MyTransaction ) {
    step :update
    step :rehash
  }
  step :notify
  fail :log_error
  # ...
end

The Wrap macro helps you to define the wrapping code (such as a Sequel.transaction call) and allows you to define the wrapped steps. (Because of the precedence works in Ruby, you need to use {...} instead of do...end.)


step Wrap( MyTransaction ) {
  step :update
  step :rehash
}

As always, you can have steps before and after Wrap in the pipe.

The proc passed to Wrap will be called when the step is executed, and receives block. block.call will execute the nested pipe.

You may have any amount of Wrap nesting.

For reusable wrappers, you can also use a Callable object.


class MyTransaction
  def self.call((ctx, flow_options), *, &block)
    result = Sequel.transaction { yield }

    signal = result ? Trailblazer::Operation::Railway.pass! : Trailblazer::Operation::Railway.fail!

    [ signal, [ctx, flow_options] ]
  end
end

This can then be passed to Wrap, making the flow extremely readable.

All nested steps will simply be executed as if they were on the “top-level” track, but within the wrapper code. Steps may deviate to the left track, and so on.

However, the last signal of the wrapped pipe is not simply passed on to the “outer” pipe. The return value of the actual Wrap block is crucial: If it returns falsey, the pipe will deviate to left after Wrap.


step Wrap ->(*, &block) { Sequel.transaction do block.call end; false } {

In the above example, regardless of Sequel.transaction’s return value, the outer pipe will deviate to the left track as the Wrap’s return value is always false.

TODO: document how you can wire

Rescue

While you can write your own begin/rescue/end mechanics using Wrap, Trailblazer offers you the Rescue macro to catch and handle exceptions that might occur while running the pipe.


class Memo::Create < Trailblazer::Operation
  step :find_model
  step Rescue( RuntimeError, handler: MyHandler ) {
    step :update
    step :rehash
  }
  step :notify
  fail :log_error
  # ...
end

Any exception raised during a step in the Rescue block will stop the nested pipe from being executed, and continue after the block on the left track.

You can specify what exceptions to catch and an optional handler that is called when an exception is encountered.


class MyHandler
  def self.call(exception, (ctx), *)
    ctx[:exception_class] = exception.class
  end
end

Alternatively, you can use a Callable object for :handler.

The Nested, Wrap and Rescue macros can also be nested, allowing an easily extendable business workflow with error handling along the way.

TODO: add example

Policy

The Policy macros Policy::Pundit, and Policy::Guard help to implement permission decider steps.

Pundit

The Policy::Pundit module allows using Pundit-compatible policy classes in an operation.

A Pundit policy has various rule methods and a special constructor that receives the current user and the current model.


class MyPolicy
  def initialize(user, model)
    @user, @model = user, model
  end

  def create?
    @user == Module && @model.id.nil?
  end

  def new?
    @user == Class
  end
end

In pundit policies, it is a convention to have access to those objects at runtime and build rules on top of those.

You can plug this policy into your pipe at any point. However, this must be inserted after the "model" skill is available.


class Create < Trailblazer::Operation
  step Model( Song, :new )
  step Policy::Pundit( MyPolicy, :create? )
  # ...
end

Note that you don’t have to create the model via the Model macro - you can use any logic you want. The Pundit macro will grab the model from ["model"], though.

This policy will only pass when the operation is invoked as follows.


Create.(current_user: User.find(1))

Any other call will cause a policy breach and stop the pipe from executing after the Policy::Pundit step.

Add your polices using the Policy::Pundit macro. It accepts the policy class name, and the rule method to call.


class Create < Trailblazer::Operation
  step Model( Song, :new )
  step Policy::Pundit( MyPolicy, :create? )
  # ...
end

The step will create the policy instance automatically for you and passes the "model" and the "current_user" skill into the policies constructor. Just make sure those dependencies are available before the step is executed.

If the policy returns falsey, it deviates to the left track.

After running the Pundit step, its result is readable from the Result object.


result = Create.(params: {}, current_user: Module)
result[:"result.policy.default"].success? #=> true
result[:"result.policy.default"][:policy] #=> #<MyPolicy ...>

Note that the actual policy instance is available via ["result.policy.#{name}"]["policy"] to be reinvoked with other rules (e.g. in the view layer).

You can add any number of Pundit policies to your pipe. Make sure to use name: to name them, though.


class Create < Trailblazer::Operation
  step Model( Song, :new )
  step Policy::Pundit( MyPolicy, :create?, name: "after_model" )
  # ...
end

The result will be stored in "result.policy.#{name}"


result = Create.(params: {}, current_user: Module)
result[:"result.policy.after_model"].success? #=> true

Override a configured policy using dependency injection.


Create.(params: {},
  current_user:            Module,
  :"policy.default.eval" => Trailblazer::Operation::Policy::Pundit.build(AnotherPolicy, :create?)
)

You can inject it using "policy.#{name}.eval". It can be any object responding to call.

Guard

A guard is a step that helps you evaluating a condition and writing the result. If the condition was evaluated as falsey, the pipe won’t be further processed and a policy breach is reported in Result["result.policy.default"].


class Create < Trailblazer::Operation
  step Policy::Guard(->(options, pass:, **) { pass })
  step :process

  def process(options, **)
    options[:x] = true
  end
end

The only way to make the above operation invoke the second step :process is as follows.


result = Create.({ pass: true })
result["x"] #=> true

Any other input will result in an abortion of the pipe after the guard.


result = Create.()
result["x"] #=> nil
result["result.policy.default"].success? #=> false

Learn more about → dependency injection to pass params and current user into the operation. TODO: fix link

The Policy::Guard macro helps you inserting your guard logic. If not defined, it will be evaluated where you insert it.


step :process

def process(options, **)
  options[:x] = true
end

The options object is passed into the guard and allows you to read and inspect data like params or current_user. Please use kw args.

As always, the guard can also be a Callable-marked object.


class MyGuard
  def call(options, pass:, **)
    pass
  end
end

Insert the object instance via the Policy::Guard macro.


class Create < Trailblazer::Operation
  step Policy::Guard( MyGuard.new )
  step :process

  # ...
end

As always, you may also use an instance method to implement a guard.


class Create < Trailblazer::Operation
  step Policy::Guard( :pass? )

  def pass?(options, pass:, **)
    pass
  end
  step :process
  # ...
end

The guard name defaults to default and can be set via name:. This allows having multiple guards.


class Create < Trailblazer::Operation
  step Policy::Guard( ->(options, current_user:, **) { current_user }, name: :user )
  # ...
end

The result will sit in result.policy.#{name}.


result = Create.(:current_user => true)
result[:"result.policy.user"].success? #=> true

Instead of using the configured guard, you can inject any callable object that returns a Result object. Do so by overriding the policy.#{name}.eval path when calling the operation.


Create.(
  {},
  :current_user        => Module,
  :"policy.default.eval" => Trailblazer::Operation::Policy::Guard.build(->(options, **) { false })
)

An easy way to let Trailblazer build a compatible object for you is using Guard.build.

This is helpful to override a certain policy for testing, or to invoke it with special rights, e.g. for an admin.

You may specify a position.


class Create < Trailblazer::Operation
  step :model!
  step Policy::Guard( :authorize! ),
    before: :model!
end

Resulting in the guard inserted before model!, even though it was added at a later point.


  Trailblazer::Developer.railway(Create, style: :rows) #=>
   # 0 ========================>operation.new
   # 1 ==================>policy.default.eval
   # 2 ===============================>model!

This is helpful if you maintain modules for operations with generic steps.

Contract

A contract is an abstraction to handle validation of arbitrary data or object state. It is a fully self-contained object that is orchestrated by the operation.

The actual validation can be implemented using Reform with ActiveModel::Validation or dry-validation, or a Dry::Schema directly without Reform.

The Contract macros helps you defining contracts and assists with instantiating and validating data with those contracts at runtime.

overview: reform

Most contracts are Reform objects that you can define and validate in the operation. Reform is a fantastic tool for deserializing and validating deeply nested hashes, and then, when valid, writing those to the database using your persistence layer such as ActiveRecord.


# app/concepts/song/contract/create.rb
module Song::Contract
  class Create < Reform::Form
    property :title
    property :length

    validates :title,  length: 2..33
    validates :length, numericality: true
  end
end

The contract then gets hooked into the operation.


class Song::Create < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build( constant: Song::Contract::Create )
  step Contract::Validate()
  step Contract::Persist()
end

As you can see, using contracts consists of five steps.

  1. Define the contract class (or multiple of them) for the operation.
  2. Plug the contract creation into the operation’s pipe using Contract::Build.
  3. Run the contract’s validation for the params using Contract::Validate.
  4. If successful, write the sane data to the model(s). This will usually happen in the Contract::Persist macro.
  5. After the operation has been run, interpret the result. For instance, a controller calling an operation will render a erroring form for invalid input.

You don’t have to use any of the TRB macros to deal with contracts, and do everything yourself. They are an abstraction that will save code and bugs, and introduce strong conventions. However, feel free to use your own code.

Here’s what the result would look like after running the Create operation with invalid data.


result = Song::Create.(params: { title: "A" })
result.success? #=> false
result[:"contract.default"].errors.messages
#=> {:title=>["is too short (minimum is 2 characters)"], :length=>["is not a number"]}

Definition

Trailblazer offers a few different ways to define contract classes and use them in an operation.

The preferred way of defining contracts is to use a separate file and class, such as the example below.


# app/concepts/song/contract/create.rb
module Song::Contract
  class Create < Reform::Form
    property :title
    property :length

    validates :title,  length: 2..33
    validates :length, numericality: true
  end
end

This is called explicit contract.

The contract file could be located just anywhere, but it’s clever to follow the Trailblazer conventions.

Using the contract happens via Contract::Build, and the :constant option.


class Song::Create < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build( constant: Song::Contract::Create )
  step Contract::Validate()
  step Contract::Persist()
end

Since both operations and contracts grow during development, the completely encapsulated approach of the explicit contract is what we recommend.

Contracts can also be defined in the operation itself.


# app/concepts/song/create.rb
class Create < Trailblazer::Operation
  class MyContract < Reform::Form
    property :title
    property :length

    validates :title,  presence: true
    validates :length, numericality: true
  end

  step Model( Song, :new )
  step Contract::Build(constant: MyContract)
  step Contract::Validate()
  step Contract::Persist( method: :sync )
end

Defining the contract happens via the contract block. This is called an inline contract. Note that you need to extend the class with the Contract::DSL module. You don’t have to specify anything in the Build macro.

While this is nice for a quick example, this usually ends up quite convoluted and we advise you to use the explicit style.

Build

The Contract::Build macro helps you to instantiate the contract. It is both helpful for a complete workflow, or to create the contract, only, without validating it, e.g. when presenting the form.


class Song::New < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build( constant: Song::Contract::Create )
end

This macro will grab the model from options["model"] and pass it into the contract’s constructor. The contract is then saved in options["contract.default"].


result = Song::New.(params: {})
result["model"] #=> #<struct Song title=nil, length=nil>
result["contract.default"]
#=> #<Song::Contract::Create model=#<struct Song title=nil, length=nil>>

The Build macro accepts the :name option to change the name from default.

Validate

The Contract::Validate macro is responsible for validating the incoming params against its contract. That means you have to use Contract::Build beforehand, or create the contract yourself. The macro will then grab the params and throw then into the contract’s validate (or call) method.


class Song::ValidateOnly < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build( constant: Song::Contract::Create )
  step Contract::Validate()
end

Depending on the outcome of the validation, it either stays on the right track, or deviates to left, skipping the remaining steps.


result = Song::ValidateOnly.(params: {}) # empty params
result.success? #=> false

Note that Validate really only validates the contract, nothing is written to the model, yet. You need to push data to the model manually, e.g. with Contract::Persist.


result = Song::ValidateOnly.(params: { title: "Rising Force", length: 13 })

result.success? #=> true
result[:model] #=> #<struct Song title=nil, length=nil>
result[:"contract.default"].title #=> "Rising Force"

Validate will use options["params"] as the input. You can change the nesting with the :key option.

Internally, this macro will simply call Form#validate on the Reform object.

Note that Reform comes with sophisticated deserialization semantics for nested forms, it might be worth reading a bit about Reform to fully understand what you can do in the Validate step.

Key

Per default, Contract::Validate will use options["params"] as the data to be validated. Use the key: option if you want to validate a nested hash from the original params structure.


class Song::Create < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build( constant: Song::Contract::Create )
  step Contract::Validate( key: "song" )
  step Contract::Persist( )
end

This automatically extracts the nested "song" hash.


result = Song::Create.(params: { "song" => { title: "Rising Force", length: 13 } })
result.success? #=> true

If that key isn’t present in the params hash, the operation fails before the actual validation.


result = Song::Create.(params: { title: "Rising Force", length: 13 })
result.success? #=> false

Note that string vs. symbol do matter here since the operation will simply do a hash lookup using the key you provided.

Persist

To push validated data from the contract to the model(s), use Persist. Like Validate, this requires a contract to be set up beforehand.


class Song::Create < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build( constant: Song::Contract::Create )
  step Contract::Validate()
  step Contract::Persist()
end

After the step, the contract’s attribute values are written to the model, and the contract will call save on the model.


result = Song::Create.(params: { title: "Rising Force", length: 13 })
result.success? #=> true
result["model"] #=> #<Song title="Rising Force", length=13>

You can also configure the Persist step to call sync instead of Reform’s save.


step Persist( method: :sync )

This will only write the contract’s data to the model without calling save on it.

Name

Explicit naming for the contract is possible, too.


class Song::Create < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build(    name: "form", constant: Song::Contract::Create )
  step Contract::Validate( name: "form" )
  step Contract::Persist(  name: "form" )
end

You have to use the name: option to tell each step what contract to use. The contract and its result will now use your name instead of default.


result = Song::Create.(params: { title: "A" })
result[:"contract.form"].errors.messages #=> {:title=>["is too short (minimum is 2 ch...

Use this if your operation has multiple contracts.

Dry-Schema

It is possible to use a Dry::Schema directly as a contract. This is great for stateless, formal validations, e.g. to make sure the params have the right format.


require "reform/form/dry"
class Create < Trailblazer::Operation
  # contract to verify params formally.
  class MyContract < Reform::Form
    feature Reform::Form::Dry
    property :id
    property :title

    validation name: :default do
      required(:id).filled
    end

    validation name: :extra, if: :default do
      required(:title).filled(min_size?: 2)
    end
  end

  step Model( Song, :new )                      # create the op's main model.
  step Contract::Build( constant: MyContract )  # create the Reform contract.
  step Contract::Validate()                     # validate the Reform contract.
  step Contract::Persist( method: :sync)        # persist the contract's data via the model.
end

Schema validations don’t need a model and hence you don’t have to instantiate them.

Dry’s schemas can even be executed before the operation gets instantiated, if you want that. This is called a guard schema and great for a quick formal check. If that fails, the operation won’t be instantiated which will save time massively.

TODO: DOCUMENT

Use schemas for formal, linear validations. Use Reform forms when there’s a more complex deserialization with nesting and object state happening.

As always, you can also use an explicit schema.

TODO: document

Manual Extraction

You can plug your own complex logic to extract params for validation into the pipe.


class Create < Trailblazer::Operation
  class MyContract < Reform::Form
    property :title
  end

  def type
    "evergreen" # this is how you could do polymorphic lookups.
  end

  step Model( Song, :new )
  step Contract::Build(constant: MyContract)
  step :extract_params!
  step Contract::Validate( skip_extract: true )
  step Contract::Persist( method: :sync )

  def extract_params!(options, **)
    options[:"contract.default.params"] = options[:params][type]
  end
end

Note that you have to set the self["params.validate"] field in your own step, and - obviously - this has to happen before the actual validation.

Keep in mind that & will deviate to the left track if your extract_params! logic returns falsey.

Dependency Injection

In fact, the operation doesn’t need any reference to a contract class at all.


class Create < Trailblazer::Operation
  step Model( Song, :new )
  step Contract::Build()
  step Contract::Validate()
  step Contract::Persist( method: :sync )
end

The contract can be injected when calling the operation.

A prerequisite for that is that the contract class is defined.


class MyContract < Reform::Form
  property :title
  validates :title, length: 2..33
end

When calling, you now have to provide the default contract class as a dependency.


Create.(
  params: { title: "Anthony's Song" },
  :"contract.default.class" => MyContract
)

This will work with any name if you follow the naming conventions.

Manual Build

To manually build the contract instance, e.g. to inject the current user, use builder:.


class Create < Trailblazer::Operation

  class MyContract < Reform::Form
    property :title
    property :current_user, virtual: true

    validate :current_user?
    validates :title, presence: true

    def current_user?
      return true if defined?(current_user)
      false
    end
  end

  step Model( Song, :new )
  step Contract::Build( constant: MyContract, builder: :default_contract! )
  step Contract::Validate()
  step Contract::Persist( method: :sync )

  def default_contract!(options, constant:, model:, **)
    constant.new(model, current_user: options [:current_user])
  end
end

Note how the contract’s class and the appropriate model are offered as kw arguments. You’re free to ignore these options and use your own assets.

As always, you may also use a proc.

Result Object

The operation will store the validation result for every contract in its own result object.

The path is result.contract.#{name}.


result = Create.(params: { length: "A" })

result[:"result.contract.default"].success?        #=> false
result[:"result.contract.default"].errors          #=> Errors object
result[:"result.contract.default"].errors.messages #=> {:length=>["is not a number"]}

Each result object responds to success?, failure?, and errors, which is an Errors object. TODO: design/document Errors. WE ARE CURRENTLY WORKING ON A UNIFIED API FOR ERRORS (FOR DRY AND REFORM).

Nested

The Nested macro works identical to Subprocess when you pass an activity.


class Create < Trailblazer::Operation
  step :create
  step Nested(Validate)
  step :save
  # ...
end

It will print a deprecation and use Subprocess internally, automatically wiring the nested’s outputs (Validate) to the known outer tracks.

Anyhow, Nested allows for dynamically deciding the nested activity at run-time using an :instance_method, or any callable object.


class Create < Trailblazer::Operation
  step :create
  step Nested(:compute_nested)
  step :save

  def compute_nested(ctx, params:, **)
    params.is_a?(Hash) ? Validate : JsonValidate
  end
  # ...
end

The dynamic nested decider method or callable has a task interface and must return the activity to nest. Nested will automatically connect the failure and the success end, only.

We’re currently investigating the best strategy for Nested’s auto-wiring. Please hit us up if you need additions or have groundbreaking ideas.

You may use the wiring DSL with Output and friends to connect outputs from the nested activity.

Signals

A signal is the object that is returned from a task. It can be any kind of object, but per convention, we derive signals from Trailblazer::Activity::Signal. When using the wiring API with step and friends, your tasks will automatically get wrapped so the returned boolean gets translated into a signal.

You can bypass this by returning a signal directly.

{{ “signal-validate” tsnippet }}

Historically, the signal name for taking the success track is Right whereas the signal for the error track is Left. Instead of using the signal constants directly (which some users, for whatever reason, prefer), you may use signal helpers. The following snippet is identical to the one above.

{{ “signalhelper-validate” tsnippet }}

Available signal helpers per default are Railway.pass!, Railway.fail!, Railway.pass_fast! and Railway.fail_fast!.

{% callout %} Note that those signals must have outputs that are connected to the next task, otherwise you will get a IllegalOutputSignalError exception. The PRO editor or tracing can help understanding.

Also, keep in mind that the more signals you use, the harder it will be to understand. This is why the operation enforces the :fast_track option when you want to use pass_fast! and fail_fast! - so both the developer reading your operation and the framework itself know about the implications upfront. {% endcallout %}

Hello hello early birds

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ac ligula convallis, mollis velit eu, porta odio. Proin nibh ipsum, bibendum eu auctor volutpat, consectetur vitae erat. Duis condimentum dapibus hendrerit.