axn 0.1.0.pre.alpha.2.5.3.1 → 0.1.0.pre.alpha.2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +25 -1
- data/README.md +2 -11
- data/docs/reference/action-result.md +2 -0
- data/docs/reference/class.md +12 -4
- data/docs/reference/configuration.md +53 -20
- data/docs/reference/instance.md +2 -2
- data/docs/strategies/index.md +1 -1
- data/docs/usage/setup.md +1 -1
- data/docs/usage/writing.md +9 -9
- data/lib/action/attachable/steps.rb +16 -1
- data/lib/action/attachable.rb +3 -3
- data/lib/action/{core/configuration.rb → configuration.rb} +3 -4
- data/lib/action/context.rb +38 -0
- data/lib/action/core/automatic_logging.rb +93 -0
- data/lib/action/core/context/facade.rb +69 -0
- data/lib/action/core/context/facade_inspector.rb +63 -0
- data/lib/action/core/context/internal.rb +32 -0
- data/lib/action/core/contract.rb +167 -211
- data/lib/action/core/contract_for_subfields.rb +84 -82
- data/lib/action/core/contract_validation.rb +62 -0
- data/lib/action/core/flow/callbacks.rb +54 -0
- data/lib/action/core/flow/exception_execution.rb +79 -0
- data/lib/action/core/flow/messages.rb +61 -0
- data/lib/action/core/flow.rb +19 -0
- data/lib/action/core/hoist_errors.rb +42 -40
- data/lib/action/core/hooks.rb +123 -0
- data/lib/action/core/logging.rb +22 -20
- data/lib/action/core/timing.rb +40 -0
- data/lib/action/core/tracing.rb +17 -0
- data/lib/action/core/use_strategy.rb +19 -17
- data/lib/action/core/validation/fields.rb +2 -0
- data/lib/action/core.rb +100 -0
- data/lib/action/enqueueable/via_sidekiq.rb +2 -2
- data/lib/action/enqueueable.rb +1 -1
- data/lib/action/{core/exceptions.rb → exceptions.rb} +1 -19
- data/lib/action/result.rb +95 -0
- data/lib/axn/factory.rb +27 -9
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +10 -47
- metadata +19 -21
- data/lib/action/core/context_facade.rb +0 -209
- data/lib/action/core/handle_exceptions.rb +0 -163
- data/lib/action/core/top_level_around_hook.rb +0 -108
data/lib/action/core.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/context"
|
4
|
+
|
5
|
+
require "action/strategies"
|
6
|
+
require "action/core/hooks"
|
7
|
+
require "action/core/logging"
|
8
|
+
require "action/core/hoist_errors"
|
9
|
+
require "action/core/flow"
|
10
|
+
require "action/core/automatic_logging"
|
11
|
+
require "action/core/use_strategy"
|
12
|
+
require "action/core/tracing"
|
13
|
+
|
14
|
+
# CONSIDER: make class names match file paths?
|
15
|
+
require "action/core/validation/validators/model_validator"
|
16
|
+
require "action/core/validation/validators/type_validator"
|
17
|
+
require "action/core/validation/validators/validate_validator"
|
18
|
+
|
19
|
+
require "action/core/contract_validation"
|
20
|
+
require "action/core/contract"
|
21
|
+
require "action/core/contract_for_subfields"
|
22
|
+
require "action/core/timing"
|
23
|
+
|
24
|
+
module Action
|
25
|
+
module Core
|
26
|
+
def self.included(base)
|
27
|
+
base.class_eval do
|
28
|
+
extend ClassMethods
|
29
|
+
include Core::Hooks
|
30
|
+
include Core::Logging
|
31
|
+
include Core::AutomaticLogging
|
32
|
+
include Core::Tracing
|
33
|
+
include Core::Timing
|
34
|
+
|
35
|
+
include Core::Flow
|
36
|
+
|
37
|
+
include Core::ContractValidation
|
38
|
+
include Core::Contract
|
39
|
+
include Core::ContractForSubfields
|
40
|
+
|
41
|
+
include Core::HoistErrors
|
42
|
+
include Core::UseStrategy
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def call(**)
|
48
|
+
new(**).tap(&:_run).result
|
49
|
+
end
|
50
|
+
|
51
|
+
def call!(**)
|
52
|
+
result = call(**)
|
53
|
+
return result if result.ok?
|
54
|
+
|
55
|
+
raise result.exception || Action::Failure.new(result.error)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(**)
|
60
|
+
@__context = Action::Context.new(**)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Main entry point for action execution
|
64
|
+
def _run
|
65
|
+
_with_tracing do
|
66
|
+
_with_logging do
|
67
|
+
_with_timing do
|
68
|
+
_with_exception_handling do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
|
69
|
+
_with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
|
70
|
+
_with_hooks do # User hooks -- any failures here *should* fail the Action::Result
|
71
|
+
call
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
ensure
|
79
|
+
_emit_metrics
|
80
|
+
end
|
81
|
+
|
82
|
+
# User-defined action logic - override this method in your action classes
|
83
|
+
def call; end
|
84
|
+
|
85
|
+
delegate :fail!, to: :@__context
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def _emit_metrics
|
90
|
+
return unless Action.config.emit_metrics
|
91
|
+
|
92
|
+
Action.config.emit_metrics.call(
|
93
|
+
self.class.name || "AnonymousClass",
|
94
|
+
result,
|
95
|
+
)
|
96
|
+
rescue StandardError => e
|
97
|
+
Axn::Util.piping_error("running metrics hook", action: self, exception: e)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/action/enqueueable.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Action
|
4
|
-
# Raised internally when fail! is called
|
4
|
+
# Raised internally when fail! is called
|
5
5
|
class Failure < StandardError
|
6
6
|
DEFAULT_MESSAGE = "Execution was halted"
|
7
7
|
|
@@ -17,24 +17,6 @@ module Action
|
|
17
17
|
def inspect = "#<#{self.class.name} '#{message}'>"
|
18
18
|
end
|
19
19
|
|
20
|
-
class StepsRequiredForInheritanceSupportError < StandardError
|
21
|
-
def message
|
22
|
-
<<~MSG
|
23
|
-
** Inheritance support requires the following steps: **
|
24
|
-
|
25
|
-
Add this to your Gemfile:
|
26
|
-
gem "interactor", github: "kaspermeyer/interactor", branch: "fix-hook-inheritance"
|
27
|
-
|
28
|
-
Explanation:
|
29
|
-
Unfortunately the upstream interactor gem does not support inheritance of hooks, which is required for this feature.
|
30
|
-
This branch is a temporary fork that adds support for inheritance of hooks, but published gems cannot specify a branch dependency.
|
31
|
-
In the future we may inline the upstream Interactor gem entirely and remove this necessity, but while we're in alpha we're continuing
|
32
|
-
to use the upstream gem for stability (and there has been recent activity on the project, so they *may* be adding additional functionality
|
33
|
-
soon).
|
34
|
-
MSG
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
20
|
class ContractViolation < StandardError
|
39
21
|
class ReservedAttributeError < ContractViolation
|
40
22
|
def initialize(name)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/context/facade"
|
4
|
+
require "action/core/context/facade_inspector"
|
5
|
+
|
6
|
+
module Action
|
7
|
+
# Outbound / External ContextFacade
|
8
|
+
class Result < ContextFacade
|
9
|
+
# For ease of mocking return results in tests
|
10
|
+
class << self
|
11
|
+
def ok(msg = nil, **exposures)
|
12
|
+
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
13
|
+
|
14
|
+
Axn::Factory.build(exposes:, messages: { success: msg }) do
|
15
|
+
exposures.each do |key, value|
|
16
|
+
expose(key, value)
|
17
|
+
end
|
18
|
+
end.call
|
19
|
+
end
|
20
|
+
|
21
|
+
def error(msg = nil, **exposures, &block)
|
22
|
+
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
23
|
+
rescues = [-> { true }, msg]
|
24
|
+
|
25
|
+
Axn::Factory.build(exposes:, rescues:) do
|
26
|
+
exposures.each do |key, value|
|
27
|
+
expose(key, value)
|
28
|
+
end
|
29
|
+
block.call if block_given?
|
30
|
+
fail!
|
31
|
+
end.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Poke some holes for necessary internal control methods
|
36
|
+
delegate :each_pair, to: :context
|
37
|
+
|
38
|
+
# External interface
|
39
|
+
delegate :ok?, :exception, to: :context
|
40
|
+
|
41
|
+
def error
|
42
|
+
return if ok?
|
43
|
+
|
44
|
+
[@context.error_prefix, determine_error_message].compact.join(" ").squeeze(" ")
|
45
|
+
end
|
46
|
+
|
47
|
+
def success
|
48
|
+
return unless ok?
|
49
|
+
|
50
|
+
stringified(action._success_msg).presence || "Action completed successfully"
|
51
|
+
end
|
52
|
+
|
53
|
+
def ok = success
|
54
|
+
|
55
|
+
def message = error || success
|
56
|
+
|
57
|
+
# Outcome constants for action execution results
|
58
|
+
OUTCOMES = [
|
59
|
+
OUTCOME_SUCCESS = :success,
|
60
|
+
OUTCOME_FAILURE = :failure,
|
61
|
+
OUTCOME_EXCEPTION = :exception,
|
62
|
+
].freeze
|
63
|
+
|
64
|
+
def outcome
|
65
|
+
return OUTCOME_EXCEPTION if exception
|
66
|
+
return OUTCOME_FAILURE if @context.failed?
|
67
|
+
|
68
|
+
OUTCOME_SUCCESS
|
69
|
+
end
|
70
|
+
|
71
|
+
# Elapsed time in milliseconds
|
72
|
+
def elapsed_time
|
73
|
+
@context.elapsed_time
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def context_data_source = @context.exposed_data
|
79
|
+
|
80
|
+
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
81
|
+
if @context.__combined_data.key?(method_name.to_sym)
|
82
|
+
msg = <<~MSG
|
83
|
+
Method ##{method_name} is not available on Action::Result!
|
84
|
+
|
85
|
+
#{action_name} may be missing a line like:
|
86
|
+
exposes :#{method_name}
|
87
|
+
MSG
|
88
|
+
|
89
|
+
raise Action::ContractViolation::MethodNotAllowed, msg
|
90
|
+
end
|
91
|
+
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/axn/factory.rb
CHANGED
@@ -22,8 +22,15 @@ module Axn
|
|
22
22
|
after: nil,
|
23
23
|
around: nil,
|
24
24
|
|
25
|
-
#
|
26
|
-
|
25
|
+
# Callbacks
|
26
|
+
on_success: nil,
|
27
|
+
on_failure: nil,
|
28
|
+
on_error: nil,
|
29
|
+
on_exception: nil,
|
30
|
+
|
31
|
+
# Strategies
|
32
|
+
use: [],
|
33
|
+
|
27
34
|
&block
|
28
35
|
)
|
29
36
|
args = block.parameters.each_with_object(_hash_with_default_array) { |(type, field), hash| hash[type] << field }
|
@@ -84,13 +91,24 @@ module Axn
|
|
84
91
|
axn.after(after) if after.present?
|
85
92
|
axn.around(around) if around.present?
|
86
93
|
|
87
|
-
#
|
88
|
-
if
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
+
# Callbacks
|
95
|
+
axn.on_success(&on_success) if on_success.present?
|
96
|
+
axn.on_failure(&on_failure) if on_failure.present?
|
97
|
+
axn.on_error(&on_error) if on_error.present?
|
98
|
+
axn.on_exception(&on_exception) if on_exception.present?
|
99
|
+
|
100
|
+
# Strategies
|
101
|
+
Array(use).each do |strategy|
|
102
|
+
if strategy.is_a?(Array)
|
103
|
+
strategy_name, *config_args = strategy
|
104
|
+
if config_args.last.is_a?(Hash)
|
105
|
+
*other_args, config = config_args
|
106
|
+
axn.use(strategy_name, *other_args, **config)
|
107
|
+
else
|
108
|
+
axn.use(strategy_name, *config_args)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
axn.use(strategy)
|
94
112
|
end
|
95
113
|
end
|
96
114
|
|
data/lib/axn/version.rb
CHANGED
data/lib/axn.rb
CHANGED
@@ -1,31 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Axn; end
|
4
|
-
require_relative "axn/version"
|
5
|
-
require_relative "axn/util"
|
6
|
-
|
7
|
-
require "interactor"
|
8
3
|
require "active_support"
|
9
4
|
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
module Axn; end
|
6
|
+
require "axn/version"
|
7
|
+
require "axn/util"
|
8
|
+
require "axn/factory"
|
13
9
|
|
14
|
-
|
15
|
-
|
16
|
-
require_relative "action/core/configuration"
|
17
|
-
require_relative "action/core/top_level_around_hook"
|
18
|
-
require_relative "action/core/contract"
|
19
|
-
require_relative "action/core/contract_for_subfields"
|
20
|
-
require_relative "action/core/handle_exceptions"
|
21
|
-
require_relative "action/core/hoist_errors"
|
22
|
-
require_relative "action/core/use_strategy"
|
10
|
+
require "action/configuration"
|
11
|
+
require "action/exceptions"
|
23
12
|
|
24
|
-
|
13
|
+
require "action/core"
|
25
14
|
|
26
|
-
|
27
|
-
|
28
|
-
require_relative "action/strategies"
|
15
|
+
require "action/attachable"
|
16
|
+
require "action/enqueueable"
|
29
17
|
|
30
18
|
def Axn(callable, **) # rubocop:disable Naming/MethodName
|
31
19
|
return callable if callable.is_a?(Class) && callable < Action
|
@@ -36,22 +24,7 @@ end
|
|
36
24
|
module Action
|
37
25
|
def self.included(base)
|
38
26
|
base.class_eval do
|
39
|
-
include
|
40
|
-
|
41
|
-
# Include first so other modules can assume `log` is available
|
42
|
-
include Logging
|
43
|
-
|
44
|
-
# NOTE: include before any others that set hooks (like contract validation), so we
|
45
|
-
# can include those hook executions in any traces set from this hook.
|
46
|
-
include TopLevelAroundHook
|
47
|
-
|
48
|
-
include HandleExceptions
|
49
|
-
include Contract
|
50
|
-
include ContractForSubfields
|
51
|
-
|
52
|
-
include HoistErrors
|
53
|
-
|
54
|
-
include UseStrategy
|
27
|
+
include Core
|
55
28
|
|
56
29
|
# --- Extensions ---
|
57
30
|
include Attachable
|
@@ -59,16 +32,6 @@ module Action
|
|
59
32
|
|
60
33
|
# Allow additional automatic includes to be configured
|
61
34
|
Array(Action.config.additional_includes).each { |mod| include mod }
|
62
|
-
|
63
|
-
# ----
|
64
|
-
|
65
|
-
# ALPHA: Everything below here is to support inheritance
|
66
|
-
|
67
|
-
base.define_singleton_method(:inherited) do |base_klass|
|
68
|
-
return super(base_klass) if Interactor::Hooks::ClassMethods.private_method_defined?(:ancestor_hooks)
|
69
|
-
|
70
|
-
raise StepsRequiredForInheritanceSupportError
|
71
|
-
end
|
72
35
|
end
|
73
36
|
end
|
74
37
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: axn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.0.pre.alpha.2.
|
4
|
+
version: 0.1.0.pre.alpha.2.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kali Donovan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -38,20 +38,6 @@ dependencies:
|
|
38
38
|
- - ">"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '7.0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: interactor
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - '='
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 3.1.2
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - '='
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 3.1.2
|
55
41
|
description: Pattern for writing callable service objects with contract validation
|
56
42
|
and error swallowing
|
57
43
|
email:
|
@@ -90,16 +76,26 @@ files:
|
|
90
76
|
- lib/action/attachable/base.rb
|
91
77
|
- lib/action/attachable/steps.rb
|
92
78
|
- lib/action/attachable/subactions.rb
|
93
|
-
- lib/action/
|
94
|
-
- lib/action/
|
79
|
+
- lib/action/configuration.rb
|
80
|
+
- lib/action/context.rb
|
81
|
+
- lib/action/core.rb
|
82
|
+
- lib/action/core/automatic_logging.rb
|
83
|
+
- lib/action/core/context/facade.rb
|
84
|
+
- lib/action/core/context/facade_inspector.rb
|
85
|
+
- lib/action/core/context/internal.rb
|
95
86
|
- lib/action/core/contract.rb
|
96
87
|
- lib/action/core/contract_for_subfields.rb
|
88
|
+
- lib/action/core/contract_validation.rb
|
97
89
|
- lib/action/core/event_handlers.rb
|
98
|
-
- lib/action/core/
|
99
|
-
- lib/action/core/
|
90
|
+
- lib/action/core/flow.rb
|
91
|
+
- lib/action/core/flow/callbacks.rb
|
92
|
+
- lib/action/core/flow/exception_execution.rb
|
93
|
+
- lib/action/core/flow/messages.rb
|
100
94
|
- lib/action/core/hoist_errors.rb
|
95
|
+
- lib/action/core/hooks.rb
|
101
96
|
- lib/action/core/logging.rb
|
102
|
-
- lib/action/core/
|
97
|
+
- lib/action/core/timing.rb
|
98
|
+
- lib/action/core/tracing.rb
|
103
99
|
- lib/action/core/use_strategy.rb
|
104
100
|
- lib/action/core/validation/fields.rb
|
105
101
|
- lib/action/core/validation/subfields.rb
|
@@ -108,6 +104,8 @@ files:
|
|
108
104
|
- lib/action/core/validation/validators/validate_validator.rb
|
109
105
|
- lib/action/enqueueable.rb
|
110
106
|
- lib/action/enqueueable/via_sidekiq.rb
|
107
|
+
- lib/action/exceptions.rb
|
108
|
+
- lib/action/result.rb
|
111
109
|
- lib/action/strategies.rb
|
112
110
|
- lib/action/strategies/transaction.rb
|
113
111
|
- lib/axn.rb
|
@@ -1,209 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_support/parameter_filter"
|
4
|
-
|
5
|
-
module Action
|
6
|
-
class ContextFacade
|
7
|
-
def initialize(action:, context:, declared_fields:, implicitly_allowed_fields: nil)
|
8
|
-
if self.class.name == "Action::ContextFacade" # rubocop:disable Style/ClassEqualityComparison
|
9
|
-
raise "Action::ContextFacade is an abstract class and should not be instantiated directly"
|
10
|
-
end
|
11
|
-
|
12
|
-
@context = context
|
13
|
-
@action = action
|
14
|
-
@declared_fields = declared_fields
|
15
|
-
|
16
|
-
(@declared_fields + Array(implicitly_allowed_fields)).each do |field|
|
17
|
-
singleton_class.define_method(field) { @context.public_send(field) }
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
attr_reader :declared_fields
|
22
|
-
|
23
|
-
def inspect = Inspector.new(facade: self, action:, context:).call
|
24
|
-
|
25
|
-
def fail!(...)
|
26
|
-
raise Action::ContractViolation::MethodNotAllowed, "Call fail! directly rather than on the context"
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
attr_reader :action, :context
|
32
|
-
|
33
|
-
def exposure_method_name = raise NotImplementedError
|
34
|
-
|
35
|
-
# Add nice error message for missing methods
|
36
|
-
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
37
|
-
if context.respond_to?(method_name)
|
38
|
-
msg = <<~MSG
|
39
|
-
Method ##{method_name} is not available on #{self.class.name}!
|
40
|
-
|
41
|
-
#{@action.class.name || "The action"} may be missing a line like:
|
42
|
-
#{exposure_method_name} :#{method_name}
|
43
|
-
MSG
|
44
|
-
|
45
|
-
raise Action::ContractViolation::MethodNotAllowed, msg
|
46
|
-
end
|
47
|
-
|
48
|
-
super
|
49
|
-
end
|
50
|
-
|
51
|
-
def determine_error_message(only_default: false)
|
52
|
-
return @context.error_from_user if @context.error_from_user.present?
|
53
|
-
|
54
|
-
# We need an exception for interceptors, and also in case the messages.error callable expects an argument
|
55
|
-
exception = @context.exception || Action::Failure.new
|
56
|
-
|
57
|
-
msg = action._error_msg
|
58
|
-
|
59
|
-
unless only_default
|
60
|
-
interceptor = action.class._error_interceptor_for(exception:, action:)
|
61
|
-
msg = interceptor.message if interceptor
|
62
|
-
end
|
63
|
-
|
64
|
-
stringified(msg, exception:).presence || "Something went wrong"
|
65
|
-
end
|
66
|
-
|
67
|
-
# Allow for callable OR string messages
|
68
|
-
def stringified(msg, exception: nil)
|
69
|
-
return msg.presence unless msg.respond_to?(:call)
|
70
|
-
|
71
|
-
# The error message callable can take the exception as an argument
|
72
|
-
if exception && msg.arity == 1
|
73
|
-
action.instance_exec(exception, &msg)
|
74
|
-
else
|
75
|
-
action.instance_exec(&msg)
|
76
|
-
end
|
77
|
-
rescue StandardError => e
|
78
|
-
Axn::Util.piping_error("determining message callable", action:, exception: e)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# Inbound / Internal ContextFacade
|
83
|
-
class InternalContext < ContextFacade
|
84
|
-
# So can be referenced from within e.g. rescues callables
|
85
|
-
def default_error
|
86
|
-
[@context.error_prefix, determine_error_message(only_default: true)].compact.join(" ").squeeze(" ")
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
def exposure_method_name = :expects
|
92
|
-
end
|
93
|
-
|
94
|
-
# Outbound / External ContextFacade
|
95
|
-
class Result < ContextFacade
|
96
|
-
# For ease of mocking return results in tests
|
97
|
-
class << self
|
98
|
-
def ok(msg = nil, **exposures)
|
99
|
-
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
100
|
-
|
101
|
-
Axn::Factory.build(exposes:, messages: { success: msg }) do
|
102
|
-
exposures.each do |key, value|
|
103
|
-
expose(key, value)
|
104
|
-
end
|
105
|
-
end.call
|
106
|
-
end
|
107
|
-
|
108
|
-
def error(msg = nil, **exposures, &block)
|
109
|
-
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
110
|
-
rescues = [-> { true }, msg]
|
111
|
-
|
112
|
-
Axn::Factory.build(exposes:, rescues:) do
|
113
|
-
exposures.each do |key, value|
|
114
|
-
expose(key, value)
|
115
|
-
end
|
116
|
-
block.call if block_given?
|
117
|
-
fail!
|
118
|
-
end.call
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# Poke some holes for necessary internal control methods
|
123
|
-
delegate :called!, :rollback!, :each_pair, to: :context
|
124
|
-
|
125
|
-
# External interface
|
126
|
-
delegate :success?, :exception, to: :context
|
127
|
-
def ok? = success?
|
128
|
-
|
129
|
-
def error
|
130
|
-
return if ok?
|
131
|
-
|
132
|
-
[@context.error_prefix, determine_error_message].compact.join(" ").squeeze(" ")
|
133
|
-
end
|
134
|
-
|
135
|
-
def success
|
136
|
-
return unless ok?
|
137
|
-
|
138
|
-
stringified(action._success_msg).presence || "Action completed successfully"
|
139
|
-
end
|
140
|
-
|
141
|
-
def ok = success
|
142
|
-
|
143
|
-
def message = error || success
|
144
|
-
|
145
|
-
private
|
146
|
-
|
147
|
-
def exposure_method_name = :exposes
|
148
|
-
end
|
149
|
-
|
150
|
-
class Inspector
|
151
|
-
def initialize(action:, facade:, context:)
|
152
|
-
@action = action
|
153
|
-
@facade = facade
|
154
|
-
@context = context
|
155
|
-
end
|
156
|
-
|
157
|
-
def call
|
158
|
-
str = [status, visible_fields].compact_blank.join(" ")
|
159
|
-
|
160
|
-
"#<#{class_name} #{str}>"
|
161
|
-
end
|
162
|
-
|
163
|
-
private
|
164
|
-
|
165
|
-
attr_reader :action, :facade, :context
|
166
|
-
|
167
|
-
def status
|
168
|
-
return unless facade.is_a?(Action::Result)
|
169
|
-
|
170
|
-
return "[OK]" if context.success?
|
171
|
-
unless context.exception
|
172
|
-
return context.error_from_user.present? ? "[failed with '#{context.error_from_user}']" : "[failed]"
|
173
|
-
end
|
174
|
-
|
175
|
-
%([failed with #{context.exception.class.name}: '#{context.exception.message}'])
|
176
|
-
end
|
177
|
-
|
178
|
-
def visible_fields
|
179
|
-
declared_fields.map do |field|
|
180
|
-
value = facade.public_send(field)
|
181
|
-
|
182
|
-
"#{field}: #{format_for_inspect(field, value)}"
|
183
|
-
end.join(", ")
|
184
|
-
end
|
185
|
-
|
186
|
-
def class_name = facade.class.name
|
187
|
-
def declared_fields = facade.send(:declared_fields)
|
188
|
-
|
189
|
-
def format_for_inspect(field, value)
|
190
|
-
return value.inspect if value.nil?
|
191
|
-
|
192
|
-
# Initially based on https://github.com/rails/rails/blob/800976975253be2912d09a80757ee70a2bb1e984/activerecord/lib/active_record/attribute_methods.rb#L527
|
193
|
-
inspected_value = if value.is_a?(String) && value.length > 50
|
194
|
-
"#{value[0, 50]}...".inspect
|
195
|
-
elsif value.is_a?(Date) || value.is_a?(Time)
|
196
|
-
%("#{value.to_fs(:inspect)}")
|
197
|
-
elsif defined?(::ActiveRecord::Relation) && value.instance_of?(::ActiveRecord::Relation)
|
198
|
-
# Avoid hydrating full AR relation (i.e. avoid loading records just to report an error)
|
199
|
-
"#{value.name}::ActiveRecord_Relation"
|
200
|
-
else
|
201
|
-
value.inspect
|
202
|
-
end
|
203
|
-
|
204
|
-
inspection_filter.filter_param(field, inspected_value)
|
205
|
-
end
|
206
|
-
|
207
|
-
def inspection_filter = action.send(:inspection_filter)
|
208
|
-
end
|
209
|
-
end
|