interaktor 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yml +5 -4
- data/.github/workflows/tests.yml +5 -5
- data/.gitignore +3 -0
- data/.ruby-version +1 -1
- data/Gemfile +1 -1
- data/Gemfile.ci +6 -0
- data/README.md +65 -29
- data/interaktor.gemspec +3 -2
- data/lib/interaktor/callable.rb +235 -108
- data/lib/interaktor/error/attribute_error.rb +3 -1
- data/lib/interaktor/error/attribute_schema_validation_error.rb +54 -0
- data/lib/interaktor/error/missing_explicit_success_error.rb +5 -0
- data/lib/interaktor/error/organizer_missing_passed_attribute_error.rb +21 -0
- data/lib/interaktor/error/organizer_success_attribute_missing_error.rb +20 -0
- data/lib/interaktor/hooks.rb +16 -0
- data/lib/interaktor/organizer.rb +33 -7
- data/lib/interaktor.rb +11 -16
- data/spec/integration_spec.rb +142 -71
- data/spec/{interactor → interaktor}/context_spec.rb +1 -1
- data/spec/{interactor → interaktor}/hooks_spec.rb +100 -2
- data/spec/interaktor/organizer_spec.rb +249 -0
- data/spec/interaktor_spec.rb +2 -2
- data/spec/spec_helper.rb +20 -0
- data/spec/support/helpers.rb +14 -0
- data/spec/support/lint.rb +403 -166
- metadata +31 -17
- data/.travis.yml +0 -14
- data/lib/interaktor/error/disallowed_attribute_assignment_error.rb +0 -9
- data/lib/interaktor/error/missing_attribute_error.rb +0 -5
- data/lib/interaktor/error/option_error.rb +0 -16
- data/lib/interaktor/error/unknown_attribute_error.rb +0 -5
- data/lib/interaktor/error/unknown_option_error.rb +0 -5
- data/spec/interactor/organizer_spec.rb +0 -128
data/lib/interaktor/callable.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
require "dry-schema"
|
2
|
+
|
3
|
+
Dry::Schema.load_extensions(:info)
|
4
|
+
|
1
5
|
module Interaktor::Callable
|
2
6
|
# When the module is included in a class, add the relevant class methods to
|
3
7
|
# that class.
|
@@ -8,131 +12,277 @@ module Interaktor::Callable
|
|
8
12
|
end
|
9
13
|
|
10
14
|
module ClassMethods
|
15
|
+
####################
|
16
|
+
# INPUT ATTRIBUTES #
|
17
|
+
####################
|
18
|
+
|
11
19
|
# The list of attributes which are required to be passed in when calling
|
12
20
|
# the interaktor.
|
13
21
|
#
|
14
22
|
# @return [Array<Symbol>]
|
15
|
-
def
|
16
|
-
@
|
23
|
+
def required_input_attributes
|
24
|
+
@required_input_attributes ||= input_schema.info[:keys]
|
25
|
+
.select { |_, info| info[:required] }
|
26
|
+
.keys
|
17
27
|
end
|
18
28
|
|
19
|
-
# The list of attributes which are
|
29
|
+
# The list of attributes which are not required to be passed in when
|
20
30
|
# calling the interaktor.
|
21
31
|
#
|
22
32
|
# @return [Array<Symbol>]
|
23
|
-
def
|
24
|
-
|
33
|
+
def optional_input_attributes
|
34
|
+
# Adding an optional attribute with NO predicates with Dry::Schema is
|
35
|
+
# sort of a "nothing statement" - the schema can sort of ignore it. The
|
36
|
+
# problem is that the optional-with-no-predicate key is not included in
|
37
|
+
# the #info results, so we need to find an list of keys elsewhere, find
|
38
|
+
# the ones that are listed there but not in the #info results, and find
|
39
|
+
# the difference. The result are the keys that are omitted from the #info
|
40
|
+
# result because they are optional and have no predicates.
|
41
|
+
#
|
42
|
+
# See https://github.com/dry-rb/dry-schema/issues/347
|
43
|
+
@optional_input_attributes ||= begin
|
44
|
+
attributes_in_info = input_schema.info[:keys].keys
|
45
|
+
all_attributes = input_schema.key_map.keys.map(&:id)
|
46
|
+
optional_attributes_by_exclusion = all_attributes - attributes_in_info
|
47
|
+
|
48
|
+
explicitly_optional_attributes = input_schema.info[:keys].reject { |_, info| info[:required] }.keys
|
49
|
+
|
50
|
+
explicitly_optional_attributes + optional_attributes_by_exclusion
|
51
|
+
end
|
25
52
|
end
|
26
53
|
|
27
|
-
#
|
54
|
+
# The complete list of input attributes.
|
28
55
|
#
|
29
56
|
# @return [Array<Symbol>]
|
30
|
-
def
|
31
|
-
|
57
|
+
def input_attributes
|
58
|
+
required_input_attributes + optional_input_attributes
|
32
59
|
end
|
33
60
|
|
34
|
-
#
|
61
|
+
# Get the input attribute schema. Fall back to an empty schema with a
|
62
|
+
# configuration that will deny ALL provided attributes - not defining an
|
63
|
+
# input schema should mean the interaktor has no input attributes.
|
35
64
|
#
|
36
|
-
# @return [
|
37
|
-
def
|
38
|
-
|
65
|
+
# @return [Dry::Schema::Params]
|
66
|
+
def input_schema
|
67
|
+
@input_schema || Dry::Schema.Params
|
39
68
|
end
|
40
69
|
|
41
|
-
#
|
42
|
-
# `#fail!` from within the interaktor.
|
70
|
+
# @param context [Hash]
|
43
71
|
#
|
44
|
-
# @return [
|
45
|
-
def
|
46
|
-
|
72
|
+
# @return [void]
|
73
|
+
def validate_input_schema(context)
|
74
|
+
return unless input_schema
|
75
|
+
|
76
|
+
result = input_schema.call(context)
|
77
|
+
|
78
|
+
if result.errors.any?
|
79
|
+
raise Interaktor::Error::AttributeSchemaValidationError.new(
|
80
|
+
self,
|
81
|
+
result.errors.to_h,
|
82
|
+
)
|
83
|
+
end
|
47
84
|
end
|
48
85
|
|
49
|
-
#
|
50
|
-
#
|
86
|
+
# @param schema [Dry::Schema::Params, nil] a predefined schema object
|
87
|
+
# @yield a new Dry::Schema::Params definition block
|
88
|
+
def input(schema = nil, &block)
|
89
|
+
raise "No schema or schema definition block provided to interaktor input." if schema.nil? && !block
|
90
|
+
|
91
|
+
raise "Provided both a schema and a schema definition block for interaktor input." if schema && block
|
92
|
+
|
93
|
+
if schema
|
94
|
+
raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
|
95
|
+
|
96
|
+
@input_schema = schema
|
97
|
+
elsif block
|
98
|
+
@input_schema = Dry::Schema.Params { instance_eval(&block) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# define the getters and setters for the input attributes
|
102
|
+
@input_schema.key_map.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
103
|
+
attribute_name = key.id
|
104
|
+
|
105
|
+
# Define getter
|
106
|
+
define_method(attribute_name) { @context.send(attribute_name) }
|
107
|
+
|
108
|
+
# Define setter
|
109
|
+
define_method("#{attribute_name}=".to_sym) do |value|
|
110
|
+
@context.send("#{attribute_name}=".to_sym, value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
######################
|
116
|
+
# FAILURE ATTRIBUTES #
|
117
|
+
######################
|
118
|
+
|
119
|
+
# The list of attributes which are required to be provided when failing the
|
120
|
+
# interaktor.
|
51
121
|
#
|
52
122
|
# @return [Array<Symbol>]
|
53
|
-
def
|
54
|
-
@
|
123
|
+
def required_failure_attributes
|
124
|
+
@required_failure_attributes ||= failure_schema.info[:keys]
|
125
|
+
.select { |_, info| info[:required] }
|
126
|
+
.keys
|
55
127
|
end
|
56
128
|
|
57
|
-
#
|
58
|
-
#
|
59
|
-
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
60
|
-
# @param options [Hash]
|
129
|
+
# The list of attributes which are not required to be provided when failing
|
130
|
+
# the interaktor.
|
61
131
|
#
|
62
|
-
# @return [
|
63
|
-
def
|
64
|
-
|
132
|
+
# @return [Array<Symbol>]
|
133
|
+
def optional_failure_attributes
|
134
|
+
# Adding an optional attribute with NO predicates with Dry::Schema is
|
135
|
+
# sort of a "nothing statement" - the schema can sort of ignore it. The
|
136
|
+
# problem is that the optional-with-no-predicate key is not included in
|
137
|
+
# the #info results, so we need to find an list of keys elsewhere, find
|
138
|
+
# the ones that are listed there but not in the #info results, and find
|
139
|
+
# the difference. The result are the keys that are omitted from the #info
|
140
|
+
# result because they are optional and have no predicates.
|
141
|
+
#
|
142
|
+
# See https://github.com/dry-rb/dry-schema/issues/347
|
143
|
+
@optional_failure_attributes ||= begin
|
144
|
+
attributes_in_info = failure_schema.info[:keys].keys
|
145
|
+
all_attributes = failure_schema.key_map.keys.map(&:id)
|
146
|
+
optional_attributes_by_exclusion = all_attributes - attributes_in_info
|
65
147
|
|
66
|
-
|
67
|
-
# Define getter
|
68
|
-
define_method(attribute) { @context.send(attribute) }
|
148
|
+
explicitly_optional_attributes = failure_schema.info[:keys].reject { |_, info| info[:required] }.keys
|
69
149
|
|
70
|
-
|
71
|
-
define_method("#{attribute}=".to_sym) do |value|
|
72
|
-
@context.send("#{attribute}=".to_sym, value)
|
150
|
+
explicitly_optional_attributes + optional_attributes_by_exclusion
|
73
151
|
end
|
152
|
+
end
|
74
153
|
|
75
|
-
|
76
|
-
|
154
|
+
# The complete list of failure attributes.
|
155
|
+
#
|
156
|
+
# @return [Array<Symbol>]
|
157
|
+
def failure_attributes
|
158
|
+
required_failure_attributes + optional_failure_attributes
|
77
159
|
end
|
78
160
|
|
79
|
-
#
|
161
|
+
# Get the failure attribute schema. Fall back to an empty schema with a
|
162
|
+
# configuration that will deny ALL provided attributes - not defining an
|
163
|
+
# failure schema should mean the interaktor has no failure attributes.
|
80
164
|
#
|
81
|
-
# @
|
82
|
-
|
165
|
+
# @return [Dry::Schema::Params]
|
166
|
+
def failure_schema
|
167
|
+
@failure_schema || Dry::Schema.Params
|
168
|
+
end
|
169
|
+
|
170
|
+
# @param context [Hash]
|
83
171
|
#
|
84
172
|
# @return [void]
|
85
|
-
def
|
86
|
-
|
173
|
+
def validate_failure_schema(context)
|
174
|
+
return unless failure_schema
|
87
175
|
|
88
|
-
|
89
|
-
# Define getter
|
90
|
-
define_method(attribute) { @context.send(attribute) }
|
176
|
+
result = failure_schema.call(context)
|
91
177
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
178
|
+
if result.errors.any?
|
179
|
+
raise Interaktor::Error::AttributeSchemaValidationError.new(
|
180
|
+
self,
|
181
|
+
result.errors.to_h,
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
97
185
|
|
98
|
-
|
99
|
-
|
186
|
+
# @param schema [Dry::Schema::Params, nil] a predefined schema object
|
187
|
+
# @yield a new Dry::Schema::Params definition block
|
188
|
+
def failure(schema = nil, &block)
|
189
|
+
raise "No schema or schema definition block provided to interaktor failure method." if schema.nil? && !block
|
100
190
|
|
101
|
-
|
102
|
-
optional_defaults[attribute] = options[:default] if options[:default]
|
103
|
-
options.delete(:default)
|
191
|
+
raise "Provided both a schema and a schema definition block for interaktor failure method." if schema && block
|
104
192
|
|
105
|
-
|
193
|
+
if schema
|
194
|
+
raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
|
195
|
+
|
196
|
+
@failure_schema = schema
|
197
|
+
elsif block
|
198
|
+
@failure_schema = Dry::Schema.Params { instance_eval(&block) }
|
106
199
|
end
|
107
200
|
end
|
108
201
|
|
109
|
-
|
202
|
+
######################
|
203
|
+
# SUCCESS ATTRIBUTES #
|
204
|
+
######################
|
205
|
+
|
206
|
+
# The list of attributes which are required to be provided when the
|
207
|
+
# interaktor succeeds.
|
110
208
|
#
|
111
|
-
# @
|
112
|
-
|
209
|
+
# @return [Array<Symbol>]
|
210
|
+
def required_success_attributes
|
211
|
+
@required_success_attributes ||= success_schema.info[:keys]
|
212
|
+
.select { |_, info| info[:required] }
|
213
|
+
.keys
|
214
|
+
end
|
215
|
+
|
216
|
+
# The list of attributes which are not required to be provided when failing
|
217
|
+
# the interaktor.
|
113
218
|
#
|
114
|
-
# @return [
|
115
|
-
def
|
116
|
-
|
219
|
+
# @return [Array<Symbol>]
|
220
|
+
def optional_success_attributes
|
221
|
+
# Adding an optional attribute with NO predicates with Dry::Schema is
|
222
|
+
# sort of a "nothing statement" - the schema can sort of ignore it. The
|
223
|
+
# problem is that the optional-with-no-predicate key is not included in
|
224
|
+
# the #info results, so we need to find an list of keys elsewhere, find
|
225
|
+
# the ones that are listed there but not in the #info results, and find
|
226
|
+
# the difference. The result are the keys that are omitted from the #info
|
227
|
+
# result because they are optional and have no predicates.
|
228
|
+
#
|
229
|
+
# See https://github.com/dry-rb/dry-schema/issues/347
|
230
|
+
@optional_success_attributes ||= begin
|
231
|
+
attributes_in_info = success_schema.info[:keys].keys
|
232
|
+
all_attributes = success_schema.key_map.keys.map(&:id)
|
233
|
+
optional_attributes_by_exclusion = all_attributes - attributes_in_info
|
117
234
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
235
|
+
explicitly_optional_attributes = success_schema.info[:keys].reject { |_, info| info[:required] }.keys
|
236
|
+
|
237
|
+
explicitly_optional_attributes + optional_attributes_by_exclusion
|
238
|
+
end
|
122
239
|
end
|
123
240
|
|
124
|
-
#
|
241
|
+
# The complete list of success attributes.
|
125
242
|
#
|
126
|
-
# @
|
127
|
-
|
243
|
+
# @return [Array<Symbol>]
|
244
|
+
def success_attributes
|
245
|
+
required_success_attributes + optional_success_attributes
|
246
|
+
end
|
247
|
+
|
248
|
+
# Get the success attribute schema. Fall back to an empty schema with a
|
249
|
+
# configuration that will deny ALL provided attributes - not defining an
|
250
|
+
# success schema should mean the interaktor has no success attributes.
|
251
|
+
#
|
252
|
+
# @return [Dry::Schema::Params]
|
253
|
+
def success_schema
|
254
|
+
@success_schema || Dry::Schema.Params
|
255
|
+
end
|
256
|
+
|
257
|
+
# @param context [Hash]
|
128
258
|
#
|
129
259
|
# @return [void]
|
130
|
-
def
|
131
|
-
|
260
|
+
def validate_success_schema(context)
|
261
|
+
return unless success_schema
|
262
|
+
|
263
|
+
result = success_schema.call(context)
|
132
264
|
|
133
|
-
|
134
|
-
|
135
|
-
|
265
|
+
if result.errors.any?
|
266
|
+
raise Interaktor::Error::AttributeSchemaValidationError.new(
|
267
|
+
self,
|
268
|
+
result.errors.to_h,
|
269
|
+
)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# @param schema [Dry::Schema::Params, nil] a predefined schema object
|
274
|
+
# @yield a new Dry::Schema::Params definition block
|
275
|
+
def success(schema = nil, &block)
|
276
|
+
raise "No schema or schema definition block provided to interaktor success method." if schema.nil? && !block
|
277
|
+
|
278
|
+
raise "Provided both a schema and a schema definition block for interaktor success method." if schema && block
|
279
|
+
|
280
|
+
if schema
|
281
|
+
raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
|
282
|
+
|
283
|
+
@success_schema = schema
|
284
|
+
elsif block
|
285
|
+
@success_schema = Dry::Schema.Params { instance_eval(&block) }
|
136
286
|
end
|
137
287
|
end
|
138
288
|
|
@@ -174,45 +324,22 @@ module Interaktor::Callable
|
|
174
324
|
#
|
175
325
|
# @return [Interaktor::Context] the context, following interaktor execution
|
176
326
|
def execute(context, raise_exception)
|
177
|
-
unless context.is_a?(Hash) || context.is_a?(Interaktor::Context)
|
178
|
-
raise ArgumentError, "Expected a hash argument when calling the interaktor, got a #{context.class} instead."
|
179
|
-
end
|
180
|
-
|
181
|
-
apply_default_optional_attributes(context)
|
182
|
-
verify_attribute_presence(context)
|
183
|
-
|
184
327
|
run_method = raise_exception ? :run! : :run
|
185
328
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
# have not been provided, or if there are any attributes which have been
|
192
|
-
# provided but are not listed as either required or optional.
|
193
|
-
#
|
194
|
-
# @param context [Interaktor::Context] the context to check
|
195
|
-
#
|
196
|
-
# @return [void]
|
197
|
-
def verify_attribute_presence(context)
|
198
|
-
# TODO: Add "allow_nil?" option to required attributes
|
199
|
-
missing_attrs = required_attributes.reject { |required_attr| context.to_h.key?(required_attr) }
|
200
|
-
raise Interaktor::Error::MissingAttributeError.new(self, missing_attrs) if missing_attrs.any?
|
329
|
+
case context
|
330
|
+
when Hash
|
331
|
+
# Silently remove any attributes that are not included in the schema
|
332
|
+
allowed_keys = input_schema.key_map.keys.map { |k| k.name.to_sym }
|
333
|
+
context.select! { |k, _| allowed_keys.include?(k.to_sym) }
|
201
334
|
|
202
|
-
|
203
|
-
extra_attrs = context.to_h.keys.reject { |attr| allowed_attrs.include?(attr) }
|
204
|
-
raise Interaktor::Error::UnknownAttributeError.new(self, extra_attrs) if extra_attrs.any?
|
205
|
-
end
|
335
|
+
validate_input_schema(context)
|
206
336
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
def apply_default_optional_attributes(context)
|
214
|
-
optional_defaults.each do |attribute, default|
|
215
|
-
context[attribute] ||= default
|
337
|
+
new(context).tap(&run_method).instance_variable_get(:@context)
|
338
|
+
when Interaktor::Context
|
339
|
+
new(context).tap(&run_method).instance_variable_get(:@context)
|
340
|
+
else
|
341
|
+
raise ArgumentError,
|
342
|
+
"Expected a hash argument when calling the interaktor, got a #{context.class} instead."
|
216
343
|
end
|
217
344
|
end
|
218
345
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Interaktor::Error::AttributeSchemaValidationError < Interaktor::Error::Base
|
2
|
+
# @return [Hash{Symbol=>Array<String>}]
|
3
|
+
attr_reader :validation_errors
|
4
|
+
|
5
|
+
# @param interaktor [Class]
|
6
|
+
# @param validation_errors [Hash{Symbol=>Array<String>}]
|
7
|
+
def initialize(interaktor, validation_errors)
|
8
|
+
super(interaktor)
|
9
|
+
|
10
|
+
@validation_errors = validation_errors
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [String]
|
14
|
+
# @abstract
|
15
|
+
def message
|
16
|
+
"Interaktor attribute schema failed validation:\n #{error_list}"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
def error_list
|
23
|
+
result = ""
|
24
|
+
|
25
|
+
validation_errors.each do |attribute, errors|
|
26
|
+
result << error_entry(attribute, errors)
|
27
|
+
end
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_entry(key, value, depth = 0)
|
33
|
+
result = " " * depth * 2
|
34
|
+
|
35
|
+
case value
|
36
|
+
when Hash
|
37
|
+
result << "#{key}:\n"
|
38
|
+
value.each do |sub_key, sub_value|
|
39
|
+
result << " "
|
40
|
+
result << error_entry(sub_key, sub_value, depth + 1)
|
41
|
+
end
|
42
|
+
when Array
|
43
|
+
result << "#{key}:\n"
|
44
|
+
value.each do |error_message|
|
45
|
+
result << " "
|
46
|
+
result << error_entry(nil, error_message, depth + 1)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
result << "- #{value}\n"
|
50
|
+
end
|
51
|
+
|
52
|
+
result
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Interaktor::Error::OrganizerMissingPassedAttributeError < Interaktor::Error::AttributeError
|
2
|
+
# @return [Symbol]
|
3
|
+
attr_reader :attribute
|
4
|
+
|
5
|
+
# @param next_interaktor [Class]
|
6
|
+
# @param attribute [Symbol]
|
7
|
+
def initialize(interaktor, attribute)
|
8
|
+
super(interaktor, [attribute])
|
9
|
+
|
10
|
+
@attribute = attribute
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
<<~MESSAGE.strip.tr("\n", "")
|
15
|
+
An organized #{interaktor} interaktor requires a '#{attribute}' input
|
16
|
+
attribute, but none of the interaktors that come before it in the
|
17
|
+
organizer list it as a success attribute, and the organizer does not list
|
18
|
+
it as a required attribute.
|
19
|
+
MESSAGE
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Interaktor::Error::OrganizerSuccessAttributeMissingError < Interaktor::Error::AttributeError
|
2
|
+
# @return [Symbol]
|
3
|
+
attr_reader :attribute
|
4
|
+
|
5
|
+
# @param interaktor [Class]
|
6
|
+
# @param attribute [Symbol]
|
7
|
+
def initialize(interaktor, attribute)
|
8
|
+
super(interaktor, [attribute])
|
9
|
+
|
10
|
+
@attribute = attribute
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
<<~MESSAGE.strip.tr("\n", "")
|
15
|
+
A #{interaktor} organizer requires a '#{attribute}' success attribute,
|
16
|
+
but none of the success attributes provided by any of the organized
|
17
|
+
interaktors list it.
|
18
|
+
MESSAGE
|
19
|
+
end
|
20
|
+
end
|
data/lib/interaktor/hooks.rb
CHANGED
@@ -126,6 +126,11 @@ module Interaktor::Hooks
|
|
126
126
|
hooks.each { |hook| after_hooks.unshift(hook) }
|
127
127
|
end
|
128
128
|
|
129
|
+
def ensure_hook(*hooks, &block)
|
130
|
+
hooks << block if block
|
131
|
+
hooks.each { |hook| ensure_hooks.push(hook) }
|
132
|
+
end
|
133
|
+
|
129
134
|
# Internal: An Array of declared hooks to run around Interaktor
|
130
135
|
# invocation. The hooks appear in the order in which they will be run.
|
131
136
|
#
|
@@ -182,6 +187,10 @@ module Interaktor::Hooks
|
|
182
187
|
def after_hooks
|
183
188
|
@after_hooks ||= []
|
184
189
|
end
|
190
|
+
|
191
|
+
def ensure_hooks
|
192
|
+
@ensure_hooks ||= []
|
193
|
+
end
|
185
194
|
end
|
186
195
|
|
187
196
|
private
|
@@ -212,6 +221,8 @@ module Interaktor::Hooks
|
|
212
221
|
yield
|
213
222
|
run_after_hooks
|
214
223
|
end
|
224
|
+
ensure
|
225
|
+
run_ensure_hooks
|
215
226
|
end
|
216
227
|
|
217
228
|
# Internal: Run around hooks.
|
@@ -237,6 +248,11 @@ module Interaktor::Hooks
|
|
237
248
|
run_hooks(self.class.after_hooks)
|
238
249
|
end
|
239
250
|
|
251
|
+
|
252
|
+
def run_ensure_hooks
|
253
|
+
run_hooks(self.class.ensure_hooks)
|
254
|
+
end
|
255
|
+
|
240
256
|
# Internal: Run a colection of hooks. The "run_hooks" method is the common
|
241
257
|
# interface by which collections of either before or after hooks are run.
|
242
258
|
#
|
data/lib/interaktor/organizer.rb
CHANGED
@@ -39,16 +39,42 @@ module Interaktor::Organizer
|
|
39
39
|
#
|
40
40
|
# @return [void]
|
41
41
|
def call
|
42
|
-
|
43
|
-
# Take the context that is being passed to each interaktor and remove
|
44
|
-
# any attributes from it that are not required by the interactor.
|
45
|
-
@context.to_h
|
46
|
-
.keys
|
47
|
-
.reject { |attr| interaktor.input_attributes.include?(attr) }
|
48
|
-
.each { |attr| @context.delete_field(attr) }
|
42
|
+
check_attribute_flow_valid
|
49
43
|
|
44
|
+
self.class.organized.each do |interaktor|
|
50
45
|
catch(:early_return) { interaktor.call!(@context) }
|
51
46
|
end
|
52
47
|
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# @return [void]
|
52
|
+
def check_attribute_flow_valid
|
53
|
+
interaktors = self.class.organized
|
54
|
+
|
55
|
+
# @type [Array<Symbol>]
|
56
|
+
success_attributes_so_far = []
|
57
|
+
|
58
|
+
success_attributes_so_far += self.class.required_input_attributes
|
59
|
+
|
60
|
+
# @param interaktor [Class]
|
61
|
+
interaktors.each do |interaktor|
|
62
|
+
interaktor.required_input_attributes.each do |required_attr|
|
63
|
+
unless success_attributes_so_far.include?(required_attr)
|
64
|
+
raise Interaktor::Error::OrganizerMissingPassedAttributeError.new(interaktor, required_attr)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
success_attributes_so_far += interaktor.success_attributes
|
69
|
+
|
70
|
+
next unless interaktor == interaktors.last
|
71
|
+
|
72
|
+
self.class.success_attributes.each do |success_attr|
|
73
|
+
unless success_attributes_so_far.include?(success_attr)
|
74
|
+
raise Interaktor::Error::OrganizerSuccessAttributeMissingError.new(interaktor, success_attr)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
53
79
|
end
|
54
80
|
end
|
data/lib/interaktor.rb
CHANGED
@@ -32,11 +32,11 @@ module Interaktor
|
|
32
32
|
#
|
33
33
|
# @return [void]
|
34
34
|
def fail!(failure_attributes = {})
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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)
|
40
40
|
|
41
41
|
@context.fail!(failure_attributes)
|
42
42
|
end
|
@@ -48,16 +48,11 @@ module Interaktor
|
|
48
48
|
#
|
49
49
|
# @return [void]
|
50
50
|
def success!(success_attributes = {})
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
-
.reject { |success_attr| success_attributes.key?(success_attr) }
|
55
|
-
raise Interaktor::Error::MissingAttributeError.new(self.class.to_s, missing_attrs) if missing_attrs.any?
|
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) }
|
56
54
|
|
57
|
-
|
58
|
-
unknown_attrs = success_attributes.keys
|
59
|
-
.reject { |success_attr| self.class.success_attributes.include?(success_attr) }
|
60
|
-
raise Interaktor::Error::UnknownAttributeError.new(self.class.to_s, unknown_attrs) if unknown_attrs.any?
|
55
|
+
self.class.validate_success_schema(success_attributes)
|
61
56
|
|
62
57
|
@context.success!(success_attributes)
|
63
58
|
end
|
@@ -104,8 +99,8 @@ module Interaktor
|
|
104
99
|
call
|
105
100
|
end
|
106
101
|
|
107
|
-
if !@context.early_return? && self.class.
|
108
|
-
raise Interaktor::Error::
|
102
|
+
if !@context.early_return? && self.class.required_success_attributes.any?
|
103
|
+
raise Interaktor::Error::MissingExplicitSuccessError.new(self, self.class.required_success_attributes)
|
109
104
|
end
|
110
105
|
|
111
106
|
@context.called!(self)
|