Skip to content

Class Methods

.expects and .exposes

Actions have a declarative interface, whereby you explicitly declare both inbound and outbound arguments. Specifically, variables you expect to receive are specified via expects, and variables you intend to expose are specified via exposes.

Both expects and exposes support the same core options:

OptionExample (same for exposes)Meaning
sensitiveexpects :password, sensitive: trueFilters the field's value when logging, reporting errors, or calling inspect
defaultexpects :foo, default: 123If foo isn't explicitly set, it'll default to this value
allow_nilexpects :foo, allow_nil: trueDon't fail if the value is nil
allow_blankexpects :foo, allow_blank: trueDon't fail if the value is blank
typeexpects :foo, type: StringCustom type validation -- fail unless name.is_a?(String)
anything elseexpects :foo, inclusion: { in: [:apple, :peach] }Any other arguments will be processed as ActiveModel validations (i.e. as if passed to validates :foo, <...> on an ActiveRecord model)

Validation details

WARNING

While we support complex interface validations, in practice you usually just want a type, if anything. Remember this is your validation about how the action is called, not pretty user-facing errors (there's a different pattern for that).

In addition to the standard ActiveModel validations, we also support two additional custom validators:

  • type: Foo - fails unless the provided value .is_a?(Foo)
    • Edge case: use type: :boolean to handle a boolean field (since ruby doesn't have a Boolean class to pass in directly)
    • Edge case: use type: :uuid to handle a confirming given string is a UUID (with or without - chars)
  • validate: [callable] - Support custom validations (fails if any string is returned OR if it raises an exception)
    • Example:
      ruby
      expects :foo, validate: ->(value) { "must be pretty big" unless value > 10 }

Details specific to .expects

expects also supports a preprocess option that, if set to a callable, will be executed before applying any validations. This can be useful for type coercion, e.g.:

ruby
expects :date, type: Date, preprocess: ->(d) { d.is_a?(Date) ? d : Date.parse(d) }

will succeed if given either an actual Date object or a string that Date.parse can convert into one. If the preprocess callable raises an exception, that'll be swallowed and the action failed.

Details specific to .exposes

Remember that you'll need a corresponding expose call for every variable you declare via exposes.

.messages

The messages declaration allows you to customize the error and success messages on the returned result.

Accepts error and/or success keys. Values can be a string (returned directly) or a callable (evaluated in the action's context, so can access instance methods). If error is provided with a callable that expects a positional argument, the exception that was raised will be passed in as that value.

ruby
messages success: "All good!", error: ->(e) { "Bad news: #{e.message}" }

error_for and rescues

While .messages sets the default error/success messages and is more commonly used, there are times when you want specific error messages for specific failure cases.

error_for and rescues both register a matcher (exception class, exception class name (string), or callable) and a message to use if the matcher succeeds. They act exactly the same, except if a matcher registered with rescues succeeds, the exception will not trigger the configured exception handlers (global or specific to this class).

ruby
messages error: "bad"

# Note this will NOT trigger Action.config.on_exception
rescues ActiveRecord::InvalidRecord => "Invalid params provided"

# These WILL trigger error handler (second demonstrates callable matcher AND message)
error_for ArgumentError, ->(e) { "Argument error: #{e.message}" }
error_for -> { name == "bad" }, -> { "was given bad name: #{name}" }

Callbacks

In addition to the global exception handler, a number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails.

ALPHA

  • The callbacks themselves are functional. Note the ordering between callbacks is not well defined (currently a side effect of the order they're defined).
    • Ordering may change at any time so while in alpha DO NOT MAKE ASSUMPTIONS ABOUT THE ORDER OF CALLBACK EXECUTION!

Callbacks vs Hooks

  • Hooks (before/after) are executed as part of the call -- exceptions or fail!s here will change a successful action call to a failure (i.e. result.ok? will be false)
  • Callbacks (defined below) are executed after the call -- exceptions or fail!s here will not change result.ok?

on_success

This is triggered after the Axn completes, if it was successful. Difference from after: if the given block raises an error, this WILL be reported to the global exception handler, but will NOT change ok? to false.

on_error

Triggered on ANY error (explicit fail! or uncaught exception). Optional filter argument works the same as on_exception (documented below).

on_failure

Triggered ONLY on explicit fail! (i.e. not by an uncaught exception). Optional filter argument works the same as on_exception (documented below).

on_exception

Much like the globally-configured on_exception hook, you can also specify exception handlers for a specific Axn class:

ruby
class Foo
  include Action

  on_exception do |exception| 
    # e.g. trigger a slack error
  end
end

Note that by default the on_exception block will be applied to any StandardError that is raised, but you can specify a matcher using the same logic as for error_for and rescues:

ruby
class Foo
  include Action

  on_exception NoMethodError do |exception| 
    # e.g. trigger a slack error
  end

  on_exception ->(e) { e.is_a?(ZeroDivisionError) } do
    # e.g. trigger a slack error
  end
end

If multiple on_exception handlers are provided, ALL that match the raised exception will be triggered in the order provided.

The global handler will be triggered after all class-specific handlers.