mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-04 20:35:52 +08:00
This patch adds a new step to services named `try`.
It’s useful to rescue exceptions that some steps could raise. That way,
if an exception is caught, the service will stop its execution and can
be inspected like with any other steps.
Just wrap the steps that can raise with a `try` block:
```ruby
try do
step :step_that_can_raise
step :another_step_that_can_raise
end
```
By default, `try` will catch any exception inheriting from
`StandardError`, but we can specify what exceptions to catch:
```ruby
try(ArgumentError, RuntimeError) do
step :will_raise
end
```
An outcome matcher has been added: `on_exceptions`. By default it will
be executed for any exception caught by the `try` step.
Here also, we can specify what exceptions to catch:
```ruby
on_exceptions(ArgumentError, RuntimeError) do |exception|
…
end
```
Finally, an RSpec matcher has been added:
```ruby
it { is_expected.to fail_with_exception }
# or
it { is_expected.to fail_with_exception(ArgumentError) }
```
150 lines
3.4 KiB
Ruby
150 lines
3.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# = Service::StepsInspector
|
|
#
|
|
# This class takes a {Service::Base::Context} object and inspects it.
|
|
# It will output a list of steps and what is their known state.
|
|
class Service::StepsInspector
|
|
# @!visibility private
|
|
class Step
|
|
attr_reader :step, :result, :nesting_level
|
|
|
|
delegate :name, to: :step
|
|
delegate :failure?, :success?, :error, :raised_exception?, to: :step_result, allow_nil: true
|
|
|
|
def self.for(step, result, nesting_level: 0)
|
|
class_name =
|
|
"#{module_parent_name}::#{step.class.name.split("::").last.sub(/^(\w+)Step$/, "\\1")}"
|
|
class_name.constantize.new(step, result, nesting_level: nesting_level)
|
|
end
|
|
|
|
def initialize(step, result, nesting_level: 0)
|
|
@step = step
|
|
@result = result
|
|
@nesting_level = nesting_level
|
|
end
|
|
|
|
def type
|
|
self.class.name.split("::").last.downcase
|
|
end
|
|
alias inspect_type type
|
|
|
|
def emoji
|
|
"#{result_emoji}#{unexpected_result_emoji}"
|
|
end
|
|
|
|
def steps
|
|
[self]
|
|
end
|
|
|
|
def inspect
|
|
"#{" " * nesting_level}[#{inspect_type}] '#{name}' #{emoji}".rstrip
|
|
end
|
|
|
|
private
|
|
|
|
def step_result
|
|
result["result.#{type}.#{name}"]
|
|
end
|
|
|
|
def result_emoji
|
|
return "💥" if raised_exception?
|
|
return "❌" if failure?
|
|
return "✅" if success?
|
|
""
|
|
end
|
|
|
|
def unexpected_result_emoji
|
|
" ⚠️#{unexpected_result_text}" if step_result.try(:[], "spec.unexpected_result")
|
|
end
|
|
|
|
def unexpected_result_text
|
|
return " <= expected to return true but got false instead" if failure?
|
|
" <= expected to return false but got true instead"
|
|
end
|
|
end
|
|
|
|
# @!visibility private
|
|
class Model < Step
|
|
def error
|
|
return result[name].errors.inspect if step_result.invalid
|
|
step_result.exception.full_message
|
|
end
|
|
end
|
|
|
|
# @!visibility private
|
|
class Contract < Step
|
|
def error
|
|
"#{step_result.errors.inspect}\n\nProvided parameters: #{step_result.parameters.pretty_inspect}"
|
|
end
|
|
|
|
def inspect_type
|
|
"params"
|
|
end
|
|
end
|
|
|
|
# @!visibility private
|
|
class Policy < Step
|
|
def error
|
|
step_result.reason
|
|
end
|
|
end
|
|
|
|
# @!visibility private
|
|
class Transaction < Step
|
|
def steps
|
|
[self, *step.steps.map { Step.for(_1, result, nesting_level: nesting_level + 1).steps }]
|
|
end
|
|
|
|
def inspect
|
|
"#{" " * nesting_level}[#{inspect_type}]"
|
|
end
|
|
|
|
def step_result
|
|
nil
|
|
end
|
|
end
|
|
|
|
# @!visibility private
|
|
class Options < Step
|
|
end
|
|
|
|
# @!visibility private
|
|
class Try < Transaction
|
|
def error
|
|
step_result.exception.inspect
|
|
end
|
|
|
|
def step_result
|
|
result["result.#{type}.#{name}"]
|
|
end
|
|
end
|
|
|
|
attr_reader :steps, :result
|
|
|
|
def initialize(result)
|
|
@steps = result.__steps__.map { Step.for(_1, result).steps }.flatten
|
|
@result = result
|
|
end
|
|
|
|
# Inspect the provided result object.
|
|
# Example output:
|
|
# [1/4] [model] 'channel' ✅
|
|
# [2/4] [params] 'default' ✅
|
|
# [3/4] [policy] 'check_channel_permission' ❌
|
|
# [4/4] [step] 'change_status'
|
|
# @return [String] the steps of the result object with their state
|
|
def inspect
|
|
steps
|
|
.map
|
|
.with_index do |step, index|
|
|
"[#{format("%#{steps.size.to_s.size}s", index + 1)}/#{steps.size}] #{step.inspect}"
|
|
end
|
|
.join("\n")
|
|
end
|
|
|
|
# @return [String, nil] the first available error, if any.
|
|
def error
|
|
steps.detect(&:failure?)&.error
|
|
end
|
|
end
|