mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-04 11:03:50 +08:00
Currently when a model is not found, we raise an `ArgumentError` exception and that exception is stored in the resulting context object. However, since we’re also storing unexpected exceptions, this default exception can pollute the context object when we need to inspect it or act on it. This patch addresses that issue by raising a custom exception instead, and we then discard it from the context object.
180 lines
4.3 KiB
Ruby
180 lines
4.3 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, :result_key, to: :step
|
|
delegate :failure?, :success?, :error, :raised_exception?, to: :step_result, allow_nil: true
|
|
|
|
alias error? failure?
|
|
|
|
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}#{runtime} #{emoji}".rstrip
|
|
end
|
|
|
|
private
|
|
|
|
def runtime
|
|
return unless step_result&.__runtime__
|
|
" (#{(step_result.__runtime__ * 1000).round(4)} ms)"
|
|
end
|
|
|
|
def step_result
|
|
result[result_key]
|
|
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 error?
|
|
" <= 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 || "Model not found"
|
|
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}]#{runtime}#{unexpected_result_emoji}"
|
|
end
|
|
end
|
|
|
|
# @!visibility private
|
|
class Options < Step
|
|
end
|
|
|
|
# @!visibility private
|
|
class Try < Transaction
|
|
def error?
|
|
step_result.exception
|
|
end
|
|
|
|
def error
|
|
step_result.exception.full_message
|
|
end
|
|
end
|
|
|
|
# @!visibility private
|
|
class Lock < Transaction
|
|
def inspect
|
|
"#{" " * nesting_level}[#{inspect_type}] #{name}#{runtime} #{emoji}".rstrip
|
|
end
|
|
|
|
def error
|
|
"Lock '#{name}' was not acquired."
|
|
end
|
|
end
|
|
|
|
attr_reader :steps, :result
|
|
|
|
def initialize(result)
|
|
@steps = result.__steps__.map { Step.for(_1, result).steps }.flatten
|
|
@result = result
|
|
end
|
|
|
|
def inspect
|
|
output = <<~OUTPUT
|
|
Inspecting #{result.__service_class__} result object:
|
|
|
|
#{execution_flow}
|
|
OUTPUT
|
|
output += "\nWhy it failed:\n\n#{error}" if error.present?
|
|
output
|
|
end
|
|
|
|
# Example output:
|
|
# [1/4] [model] channel (0.02 ms) ✅
|
|
# [2/4] [params] default (0.1 ms) ✅
|
|
# [3/4] [policy] check_channel_permission ❌
|
|
# [4/4] [step] change_status
|
|
# @return [String] the steps of the result object with their state
|
|
def execution_flow
|
|
steps
|
|
.filter_map
|
|
.with_index do |step, index|
|
|
next if @encountered_error
|
|
@encountered_error = index + 1 if step.failure?
|
|
"[#{format("%#{steps.size.to_s.size}s", index + 1)}/#{steps.size}] #{step.inspect}"
|
|
end
|
|
.join("\n")
|
|
.then do |output|
|
|
skipped_steps = steps.size - @encountered_error.to_i
|
|
next output unless @encountered_error && skipped_steps.positive?
|
|
"#{output}\n\n(#{skipped_steps} more steps not shown as the execution flow was stopped before reaching them)"
|
|
end
|
|
end
|
|
|
|
# @return [String, nil] the first available error, if any.
|
|
def error
|
|
steps.detect(&:error?)&.error
|
|
end
|
|
end
|