interaktor 0.2.0 → 0.4.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 -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)
|