Class: Dry::Operation

Inherits:
Object
  • Object
show all
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

Methods included from ClassContext

directly_inherited, indirectly_inherited, inherited, operate_on, skip_prepending

Class Method Details

.loaderObject



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.expand_path "..", __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

Invokes a callable in case of block’s failure

This method is useful when you want to perform some side-effect when a failure is encountered. It’s meant to be used within the #steps block commonly wrapping a sub-set of #step calls.

Parameters:

  • handler (#call) (defaults to: method(:throw_failure))

    a callable that will be called with the encountered failure. By default, it throws ‘FAILURE_TAG` with the failure.

Yield Returns:

  • (Object)

Returns:

  • (Object)

    the block’s return value when it’s not a failure or the handler’s return value when the block returns a failure



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.

Parameters:

  • result (Dry::Monads::Result)

Returns:

  • (Object)

    wrapped value

See Also:



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

Yield Returns:

  • (Object)

Returns:

  • (Dry::Monads::Result::Success)

See Also:



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

Parameters:

  • failure (Dry::Monads::Result::Failure)


198
199
200
# File 'lib/dry/operation.rb', line 198

def throw_failure(failure)
  throw FAILURE_TAG, failure
end