Skip to content

How to build an Action

The core boilerplate is pretty minimal:

ruby
class Foo
  include Action

  def call
    # ... do some stuff here?
  end
end

Declare the interface

The first step is to determine what arguments you expect to be passed into call. These are declared via the expects keyword.

If you want to expose any results to the caller, declare that via the exposes keyword.

Both of these optionally accept type:, allow_blank:, and any other ActiveModel validation (see: reference).

ruby
class Foo
  include Action

  expects :name, type: String
  exposes :meaning_of_life

  def call
    # ... do some stuff here?
  end
end

Implement the action

Once the interface is defined, you're primarily focused on defining the call method.

To abort execution with a specific error message, call fail!.

If you declare that your action exposes anything, you need to actually expose it.

ruby
class Foo
  include Action

  expects :name, type: String
  exposes :meaning_of_life

  def call
    fail! "Douglas already knows the meaning" if name == "Doug"

    msg = "Hello #{name}, the meaning of life is 42"
    expose meaning_of_life: msg 
  end
end

See the reference doc for a few more handy helper methods (e.g. #log).

Customizing messages

The default error and success message strings ("Something went wrong" / "Action completed successfully", respectively) are technically safe to show users, but you'll often want to set them to something more useful.

There's a messages declaration for that -- you can set strings (most common) or a callable (note for the error case, if you give it a callable that expects a single argument, the exception that was raised will be passed in).

For instance, configuring the action like this:

ruby
class Foo
  include Action

  expects :name, type: String
  exposes :meaning_of_life

  messages success: -> { "Revealed the secret of life to #{name}" }, 
           error: ->(e) { "No secret of life for you: #{e.message}" }

  def call
    fail! "Douglas already knows the meaning" if name == "Doug"

    msg = "Hello #{name}, the meaning of life is 42"
    expose meaning_of_life: msg
  end
end

Would give us these outputs:

ruby
Foo.call.error # => "No secret of life for you: Name can't be blank"
Foo.call(name: "Doug").error # => "Douglas already knows the meaning"
Foo.call(name: "Adams").success # => "Revealed the secret of life to Adams"
Foo.call(name: "Adams").meaning_of_life # => "Hello Adams, the meaning of life is 42"

Lifecycle methods

In addition to #call, there are a few additional pieces to be aware of:

#rollback

If you define a #rollback method, it'll be called (before returning an Action::Result to the caller) whenever your action fails.

Hooks

before and after hooks are also supported. They can receive a block directly, or the symbol name of a local method.

Concrete example

Given this series of methods and hooks:

ruby
class Foo
  include Action

  before { log("before hook") } 
  after :log_after

  def call
    log("in call")
    raise "oh no something borked"
  end

  def rollback
    log("rolling back")
  end

  private

  def log_after
    log("after hook")
  end
end

Foo.call would fail (because of the raise), but along the way would end up logging:

text
before hook
in call
after hook
rolling back

Debugging

Remember you can enable debug logging to print log lines before and after each action is executed.