interaktor 0.3.0 → 0.5.0
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/.github/workflows/publish.yml +5 -3
- data/.github/workflows/tests.yml +1 -1
- data/.ruby-version +1 -1
- data/.standard.yml +1 -0
- data/Gemfile +2 -5
- data/README.md +25 -4
- data/bin/_guard-core +16 -0
- data/bin/guard +16 -0
- data/bin/rspec +16 -0
- data/bin/standardrb +16 -0
- data/interaktor.gemspec +3 -4
- data/lib/interaktor/callable.rb +63 -76
- data/lib/interaktor/error/invalid_method_for_state_error.rb +10 -0
- data/lib/interaktor/error/organizer_missing_passed_attribute_error.rb +1 -1
- data/lib/interaktor/error/organizer_success_attribute_missing_error.rb +1 -1
- data/lib/interaktor/error/unknown_attribute_error.rb +12 -1
- data/lib/interaktor/failure.rb +8 -8
- data/lib/interaktor/hooks.rb +68 -2
- data/lib/interaktor/interaction.rb +154 -0
- data/lib/interaktor/organizer.rb +9 -1
- data/lib/interaktor.rb +41 -53
- data/spec/integration_spec.rb +1874 -1874
- data/spec/interaktor/context_spec.rb +185 -185
- data/spec/interaktor/hooks_spec.rb +111 -13
- data/spec/interaktor/organizer_spec.rb +58 -90
- data/spec/support/helpers.rb +5 -7
- data/spec/support/lint.rb +33 -60
- metadata +19 -26
- data/.rubocop.yml +0 -245
- data/lib/interaktor/context.rb +0 -116
- data/lib/interaktor/error/disallowed_attribute_assignment_error.rb +0 -9
- data/lib/interaktor/error/option_error.rb +0 -18
- data/lib/interaktor/error/unknown_option_error.rb +0 -5
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
module Interaktor
|
|
2
|
+
class Interaction
|
|
3
|
+
attr_reader :input_args, :success_args, :failure_args
|
|
4
|
+
|
|
5
|
+
# @param interaktor [Interaktor]
|
|
6
|
+
# @param input [Hash, Interaction]
|
|
7
|
+
def initialize(interaktor, input)
|
|
8
|
+
@interaktor = interaktor
|
|
9
|
+
@executed = false
|
|
10
|
+
@failed = false
|
|
11
|
+
@rolled_back = false
|
|
12
|
+
|
|
13
|
+
@input_args = case input
|
|
14
|
+
when Hash
|
|
15
|
+
input.transform_keys(&:to_sym)
|
|
16
|
+
when Interaction
|
|
17
|
+
input
|
|
18
|
+
.input_args
|
|
19
|
+
.merge(input.success_args || {})
|
|
20
|
+
.slice(*(interaktor.class.input_attributes || []))
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "Invalid input type: #{input.class}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Whether the interaction is successful.
|
|
27
|
+
def success?
|
|
28
|
+
if !@executed
|
|
29
|
+
raise Interaktor::Error::InvalidMethodForStateError.new(
|
|
30
|
+
self,
|
|
31
|
+
"Cannot call `success?` before interaktor execution is complete"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
!failure?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether the interaction has failed.
|
|
39
|
+
def failure?
|
|
40
|
+
if !@executed
|
|
41
|
+
raise Interaktor::Error::InvalidMethodForStateError.new(
|
|
42
|
+
self,
|
|
43
|
+
"Cannot call `failure?` before interaktor execution is complete"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@failed
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param args [Hash]
|
|
51
|
+
#
|
|
52
|
+
# @raises [Interaktor::Failure]
|
|
53
|
+
def fail!(args = {})
|
|
54
|
+
if @executed
|
|
55
|
+
raise Interaktor::Error::InvalidMethodForStateError.new(
|
|
56
|
+
self,
|
|
57
|
+
"Cannot call `fail!` after interaktor execution is already complete"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@executed = true
|
|
62
|
+
@failed = true
|
|
63
|
+
@failure_args = args.transform_keys(&:to_sym)
|
|
64
|
+
raise Interaktor::Failure, self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param args [Hash]
|
|
68
|
+
def success!(args = {})
|
|
69
|
+
if @executed
|
|
70
|
+
raise Interaktor::Error::InvalidMethodForStateError.new(
|
|
71
|
+
self,
|
|
72
|
+
"Cannot call `success!` after interaktor execution is already complete"
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@executed = true
|
|
77
|
+
@success_args = args.transform_keys(&:to_sym)
|
|
78
|
+
early_return!
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def allowable_success_attributes
|
|
82
|
+
@interaktor.class.success_attributes
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def allowable_failure_attributes
|
|
86
|
+
@interaktor.class.failure_attributes
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Only allow access to arguments when appropriate. Input arguments should be
|
|
90
|
+
# accessible only during the interaction's execution, and after the
|
|
91
|
+
# execution is complete, either the success or failure arguments should be
|
|
92
|
+
# accessible, depending on the outcome.
|
|
93
|
+
def method_missing(method_name, *args, &block)
|
|
94
|
+
if !@executed && input_args.key?(method_name)
|
|
95
|
+
input_args[method_name]
|
|
96
|
+
elsif success? && allowable_success_attributes.include?(method_name)
|
|
97
|
+
success_args[method_name]
|
|
98
|
+
elsif failure? && allowable_failure_attributes.include?(method_name)
|
|
99
|
+
failure_args[method_name]
|
|
100
|
+
else
|
|
101
|
+
super
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
106
|
+
input_args.key?(method_name) ||
|
|
107
|
+
success_args&.key?(method_name) ||
|
|
108
|
+
failure_args&.key?(method_name) ||
|
|
109
|
+
super
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Roll back the interaction. Successful interactions may have this method
|
|
113
|
+
# called to roll back their state.
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean] true if rolled back successfully, false if already
|
|
116
|
+
# rolled back
|
|
117
|
+
def rollback!
|
|
118
|
+
return false if @rolled_back
|
|
119
|
+
|
|
120
|
+
_called.reverse_each(&:rollback)
|
|
121
|
+
@rolled_back = true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Track that an Interaktor has been called. The `#called!` method is used by
|
|
125
|
+
# the interaktor being invoked. After an interaktor is successfully called,
|
|
126
|
+
# the interaction is tracked for the purpose of potential future rollback.
|
|
127
|
+
#
|
|
128
|
+
# @param interaktor [Interaktor] an interaktor that has been successfully
|
|
129
|
+
# called
|
|
130
|
+
def called!(interaktor)
|
|
131
|
+
@executed = true
|
|
132
|
+
_called << interaktor
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# An array of successfully called Interaktor instances invoked against this
|
|
136
|
+
# interaction instance.
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<Interaktor>]
|
|
139
|
+
def _called
|
|
140
|
+
@called ||= []
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Trigger an early return throw.
|
|
144
|
+
def early_return!
|
|
145
|
+
@early_return = true
|
|
146
|
+
throw :early_return, self
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Whether or not the interaction has been returned from early.
|
|
150
|
+
def early_return?
|
|
151
|
+
(@early_return == true) || false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
data/lib/interaktor/organizer.rb
CHANGED
|
@@ -41,8 +41,16 @@ module Interaktor::Organizer
|
|
|
41
41
|
def call
|
|
42
42
|
check_attribute_flow_valid
|
|
43
43
|
|
|
44
|
+
latest_interaction = nil
|
|
45
|
+
|
|
44
46
|
self.class.organized.each do |interaktor|
|
|
45
|
-
catch(:early_return)
|
|
47
|
+
catch(:early_return) do
|
|
48
|
+
latest_interaction = interaktor.call!(latest_interaction || @interaction)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if latest_interaction
|
|
53
|
+
@interaction.instance_variable_set(:@success_args, latest_interaction.success_args)
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
|
data/lib/interaktor.rb
CHANGED
|
@@ -14,99 +14,87 @@ module Interaktor
|
|
|
14
14
|
extend ClassMethods
|
|
15
15
|
include Hooks
|
|
16
16
|
include Callable
|
|
17
|
+
|
|
18
|
+
interaction_class = Class.new(Interaktor::Interaction) do
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
base.const_set(:Interaction, interaction_class)
|
|
17
22
|
end
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
module ClassMethods
|
|
21
26
|
end
|
|
22
27
|
|
|
23
|
-
# @param
|
|
28
|
+
# @param args [Hash, Interaktor::Interaction] the context object as a hash
|
|
24
29
|
# with attributes or an already-built context
|
|
25
|
-
def initialize(
|
|
26
|
-
@
|
|
30
|
+
def initialize(args = {})
|
|
31
|
+
@interaction = self.class::Interaction.new(self, args)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def fail!(failure_attributes = {})
|
|
35
|
-
# Silently remove any attributes that are not included in the schema
|
|
36
|
-
allowed_keys = self.class.failure_schema.key_map.keys.map { |k| k.name.to_sym }
|
|
37
|
-
failure_attributes.select! { |k, _| allowed_keys.include?(k.to_sym) }
|
|
38
|
-
|
|
39
|
-
self.class.validate_failure_schema(failure_attributes)
|
|
34
|
+
# @param args [Hash{Symbol=>Object}]
|
|
35
|
+
def fail!(args = {})
|
|
36
|
+
if (disallowed_key = args.keys.find { |k| !self.class.failure_attributes.include?(k.to_sym) })
|
|
37
|
+
raise Interaktor::Error::UnknownAttributeError.new(self, disallowed_key)
|
|
38
|
+
end
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
self.class.validate_failure_schema(args)
|
|
41
|
+
@interaction.fail!(args)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# @return [void]
|
|
50
|
-
def success!(success_attributes = {})
|
|
51
|
-
# Silently remove any attributes that are not included in the schema
|
|
52
|
-
allowed_keys = self.class.success_schema.key_map.keys.map { |k| k.name.to_sym }
|
|
53
|
-
success_attributes.select! { |k, _| allowed_keys.include?(k.to_sym) }
|
|
54
|
-
|
|
55
|
-
self.class.validate_success_schema(success_attributes)
|
|
44
|
+
# @param args [Hash]
|
|
45
|
+
def success!(args = {})
|
|
46
|
+
if (disallowed_key = args.keys.find { |k| !self.class.success_attributes.include?(k.to_sym) })
|
|
47
|
+
raise Interaktor::Error::UnknownAttributeError.new(self, disallowed_key)
|
|
48
|
+
end
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
self.class.validate_success_schema(args)
|
|
51
|
+
@interaction.success!(args)
|
|
58
52
|
end
|
|
59
53
|
|
|
60
54
|
# Invoke an Interaktor instance without any hooks, tracking, or rollback. It
|
|
61
55
|
# is expected that the `#call` instance method is overwritten for each
|
|
62
56
|
# interaktor class.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def call; end
|
|
57
|
+
def call
|
|
58
|
+
end
|
|
66
59
|
|
|
67
60
|
# Reverse prior invocation of an Interaktor instance. Any interaktor class
|
|
68
61
|
# that requires undoing upon downstream failure is expected to overwrite the
|
|
69
62
|
# `#rollback` instance method.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# If the context is failed or any error is raised, the context is rolled
|
|
78
|
-
# back.
|
|
79
|
-
#
|
|
80
|
-
# @return [void]
|
|
63
|
+
def rollback
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Invoke an interaktor instance along with all defined hooks. The `run` method
|
|
67
|
+
# is used internally by the `call` class method. After successful invocation
|
|
68
|
+
# of the interaktor, the instance is tracked within the context. If the
|
|
69
|
+
# context is failed or any error is raised, the context is rolled back.
|
|
81
70
|
def run
|
|
82
71
|
run!
|
|
83
|
-
rescue Interaktor::Failure
|
|
72
|
+
rescue Interaktor::Failure
|
|
84
73
|
end
|
|
85
74
|
|
|
86
75
|
# Invoke an Interaktor instance along with all defined hooks, typically used
|
|
87
76
|
# internally by `.call!`. After successful invocation of the interaktor, the
|
|
88
|
-
# instance is tracked within the
|
|
89
|
-
# error is raised, the
|
|
90
|
-
# identically to `#run` with one notable exception - if the
|
|
91
|
-
# during the invocation of the interaktor, `Interaktor::Failure` is
|
|
77
|
+
# instance is tracked within the interaction. If the interaction is failed or
|
|
78
|
+
# any error is raised, the interaction is rolled back. This method behaves
|
|
79
|
+
# identically to `#run` with one notable exception - if the interaction is
|
|
80
|
+
# failed during the invocation of the interaktor, `Interaktor::Failure` is
|
|
81
|
+
# raised.
|
|
92
82
|
#
|
|
93
83
|
# @raises [Interaktor::Failure]
|
|
94
|
-
#
|
|
95
|
-
# @return [void]
|
|
96
84
|
def run!
|
|
97
85
|
with_hooks do
|
|
98
86
|
catch(:early_return) do
|
|
99
87
|
call
|
|
100
88
|
end
|
|
101
89
|
|
|
102
|
-
if
|
|
90
|
+
if self.class.required_success_attributes.any? && !@interaction.success_args
|
|
103
91
|
raise Interaktor::Error::MissingExplicitSuccessError.new(self, self.class.required_success_attributes)
|
|
104
92
|
end
|
|
105
93
|
|
|
106
|
-
@
|
|
94
|
+
@interaction.called!(self)
|
|
107
95
|
end
|
|
108
|
-
rescue
|
|
109
|
-
@
|
|
96
|
+
rescue
|
|
97
|
+
@interaction.rollback!
|
|
110
98
|
raise
|
|
111
99
|
end
|
|
112
100
|
end
|