Class: Dry::Operation
- Inherits:
-
Object
- Object
- Dry::Operation
- Extended by:
- ClassContext
- Includes:
- Monads::Result::Mixin
- Defined in:
- lib/dry/operation.rb,
lib/dry/operation/errors.rb,
lib/dry/operation/version.rb,
lib/dry/operation/class_context.rb,
lib/dry/operation/extensions/rom.rb,
lib/dry/operation/extensions/sequel.rb,
lib/dry/operation/extensions/active_record.rb,
lib/dry/operation/class_context/prepend_manager.rb,
lib/dry/operation/class_context/steps_method_prepender.rb
Overview
DSL for chaining operations that can fail
Operation is a thin DSL wrapping dry-monads that allows you to chain operations by focusing on the happy path and short-circuiting on failure.
The canonical way of using it is to subclass Operation and define your flow in the ‘#call` method. Individual operations can be called with #step. They need to return either a success or a failure result. Successful results will be automatically unwrapped, while a failure will stop further execution of the method.
“‘ruby class MyOperation < Dry::Operation
def call(input)
attrs = step validate(input)
user = step persist(attrs)
step notify(user)
user
end
def validate(input)
# Dry::Monads::Result::Success or Dry::Monads::Result::Failure
end
def persist(attrs)
# Dry::Monads::Result::Success or Dry::Monads::Result::Failure
end
def notify(user)
# Dry::Monads::Result::Success or Dry::Monads::Result::Failure
end
end
include Dry::Monads
case MyOperation.new.call(input) in Success(user)
puts "User #{user.name} created"
in Failure[:invalid_input, validation_errors]
puts "Invalid input: #{validation_errors}"
in Failure(:database_error)
puts "Database error"
in Failure(:email_error)
puts "Email error"
end “‘
Under the hood, the ‘#call` method is decorated to allow skipping the rest of its execution when a failure is encountered. You can choose to use another method with ClassContext#operate_on (which also accepts a list of methods):
“‘ruby class MyOperation < Dry::Operation
operate_on :run # or operate_on :run, :call
def run(input)
attrs = step validate(input)
user = step persist(attrs)
step notify(user)
user
end
# ...
end “‘
As you can see, the aforementioned behavior allows you to write your flow in a linear fashion. Failures are mostly handled locally by each individual operation. However, you can also define a global failure handler by defining an ‘#on_failure` method. It will be called with the wrapped failure value and, in the case of accepting a second argument, the name of the method that defined the flow:
“‘ruby class MyOperation < Dry::Operation
def call(input)
attrs = step validate(input)
user = step persist(attrs)
step notify(user)
user
end
def on_failure(user) # or def on_failure(failure_value, method_name)
log_failure(user)
end
end “‘
You can opt out altogether of this behavior via ClassContext#skip_prepending. If so, you manually need to wrap your flow within the #steps method and manually handle global failures.
“‘ruby class MyOperation < Dry::Operation
skip_prepending
def call(input)
steps do
attrs = step validate(input)
user = step persist(attrs)
step notify(user)
user
end.tap do |result|
log_failure(result.failure) if result.failure?
end
end
# ...
end “‘
The behavior configured by ClassContext#operate_on and ClassContext#skip_prepending is inherited by subclasses.
Some extensions are available under the ‘Dry::Operation::Extensions` namespace, providing additional functionality that can be included in your operation classes.
Defined Under Namespace
Modules: ClassContext, Extensions Classes: Error, ExtensionError, FailureHookArityError, InvalidStepResultError, MethodsToPrependAlreadyDefinedError, MissingDependencyError, PrependConfigurationError
Constant Summary collapse
- VERSION =
"1.0.0"
Constants included from ClassContext
ClassContext::DEFAULT_METHODS_TO_PREPEND
Class Method Summary collapse
Instance Method Summary collapse
-
#intercepting_failure(handler = method(:throw_failure), &block) ⇒ Object
Invokes a callable in case of block’s failure.
-
#step(result) ⇒ Object
Unwraps a Monads::Result::Success.
-
#steps(&block) ⇒ Dry::Monads::Result::Success
Wraps block’s return value in a Monads::Result::Success.
-
#throw_failure(failure) ⇒ Object
Throws ‘:halt` with a failure.
Methods included from ClassContext
directly_inherited, indirectly_inherited, inherited, operate_on, skip_prepending
Class Method Details
.loader ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/dry/operation.rb', line 126 def self.loader @loader ||= Zeitwerk::Loader.new.tap do |loader| root = File. "..", __dir__ loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry/operation.rb") loader.tag = "dry-operation" loader.push_dir root loader.ignore( "#{root}/dry/operation/errors.rb", "#{root}/dry/operation/extensions/*.rb" ) loader.inflector.inflect("rom" => "ROM") end end |
Instance Method Details
#intercepting_failure(handler = method(:throw_failure), &block) ⇒ Object
184 185 186 187 188 189 190 191 192 193 |
# File 'lib/dry/operation.rb', line 184 def intercepting_failure(handler = method(:throw_failure), &block) output = catching_failure(&block) case output when Failure handler.(output) else output end end |
#step(result) ⇒ Object
Unwraps a Monads::Result::Success
Throws ‘:halt` with a Monads::Result::Failure on failure.
165 166 167 168 169 170 171 |
# File 'lib/dry/operation.rb', line 165 def step(result) if result.is_a?(Dry::Monads::Result) result.value_or { throw_failure(result) } else raise InvalidStepResultError.new(result: result) end end |
#steps(&block) ⇒ Dry::Monads::Result::Success
Wraps block’s return value in a Monads::Result::Success
Catches ‘:halt` and returns it
154 155 156 |
# File 'lib/dry/operation.rb', line 154 def steps(&block) catching_failure { Success(block.call) } end |
#throw_failure(failure) ⇒ Object
Throws ‘:halt` with a failure
198 199 200 |
# File 'lib/dry/operation.rb', line 198 def throw_failure(failure) throw FAILURE_TAG, failure end |