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:
Option | Example (same for exposes ) | Meaning |
---|---|---|
sensitive | expects :password, sensitive: true | Filters the field's value when logging, reporting errors, or calling inspect |
default | expects :foo, default: 123 | If foo isn't explicitly set, it'll default to this value |
allow_nil | expects :foo, allow_nil: true | Don't fail if the value is nil |
allow_blank | expects :foo, allow_blank: true | Don't fail if the value is blank |
type | expects :foo, type: String | Custom type validation -- fail unless name.is_a?(String) |
anything else | expects :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)
- Edge case: use
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 }
- Example:
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.:
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.
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).
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 thecall
-- exceptions orfail!
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 orfail!
s here will not changeresult.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:
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
:
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.