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.
@@ -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 required_attributes
16
- @required_attributes ||= []
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 NOT required to be passed in when
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 optional_attributes
24
- @optional_attributes ||= []
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
- # A list of optional attributes and their default values.
54
+ # The complete list of input attributes.
28
55
  #
29
56
  # @return [Array<Symbol>]
30
- def optional_defaults
31
- @optional_defaults ||= {}
57
+ def input_attributes
58
+ required_input_attributes + optional_input_attributes
32
59
  end
33
60
 
34
- # A list of attributes which could be passed when calling the interaktor.
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 [Array<Symbol>]
37
- def input_attributes
38
- required_attributes + optional_attributes
65
+ # @return [Dry::Schema::Params]
66
+ def input_schema
67
+ @input_schema || Dry::Schema.Params
39
68
  end
40
69
 
41
- # The list of attributes which are required to be passed in when calling
42
- # `#fail!` from within the interaktor.
70
+ # @param context [Hash]
43
71
  #
44
- # @return [Array<Symbol>]
45
- def failure_attributes
46
- @failure_attributes ||= []
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
- # The list of attributes which are required to be passed in when calling
50
- # `#fail!` from within the interaktor.
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 success_attributes
54
- @success_attributes ||= []
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
- # A DSL method for documenting required interaktor attributes.
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 [void]
63
- def required(*attributes, **options)
64
- required_attributes.concat attributes
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
- attributes.each do |attribute|
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
- # Define setter
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
- raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
76
- end
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
- # A DSL method for documenting optional interaktor attributes.
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
- # @param attributes [Symbol, Array<Symbol>] the list of attribute names
82
- # @param options [Hash]
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 optional(*attributes, **options)
86
- optional_attributes.concat attributes
173
+ def validate_failure_schema(context)
174
+ return unless failure_schema
87
175
 
88
- attributes.each do |attribute|
89
- # Define getter
90
- define_method(attribute) { @context.send(attribute) }
176
+ result = failure_schema.call(context)
91
177
 
92
- # Define setter
93
- define_method("#{attribute}=".to_sym) do |value|
94
- unless @context.to_h.key?(attribute)
95
- raise Interaktor::Error::DisallowedAttributeAssignmentError.new(self.class.to_s, [attribute])
96
- end
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
- @context.send("#{attribute}=".to_sym, value)
99
- end
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
- # Handle options
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
- raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
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
- # A DSL method for documenting required interaktor failure attributes.
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
- # @param attributes [Symbol, Array<Symbol>] the list of attribute names
112
- # @param options [Hash]
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 [void]
115
- def failure(*attributes, **options)
116
- failure_attributes.concat attributes
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
- attributes.each do |attribute|
119
- # Handle options
120
- raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
121
- end
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
- # A DSL method for documenting required interaktor success attributes.
241
+ # The complete list of success attributes.
125
242
  #
126
- # @param attributes [Symbol, Array<Symbol>] the list of attribute names
127
- # @param options [Hash]
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 success(*attributes, **options)
131
- success_attributes.concat attributes
260
+ def validate_success_schema(context)
261
+ return unless success_schema
262
+
263
+ result = success_schema.call(context)
132
264
 
133
- attributes.each do |attribute|
134
- # Handle options
135
- raise Interaktor::Error::UnknownOptionError.new(self.class.to_s, options) if options.any?
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
- new(context).tap(&run_method).instance_variable_get(:@context)
187
- end
188
-
189
- # Check the provided context against the attributes defined with the DSL
190
- # methods, and determine if there are any attributes which are required and
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
- allowed_attrs = required_attributes + optional_attributes
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
- # Given the list of optional default attribute values defined by the class,
208
- # assign those default values to the context if they were omitted.
209
- #
210
- # @param context [Interaktor::Context]
211
- #
212
- # @return [void]
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
@@ -10,7 +10,9 @@ class Interaktor::Error::AttributeError < Interaktor::Error::Base
10
10
  @attributes = attributes
11
11
  end
12
12
 
13
+ # @return [String]
14
+ # @abstract
13
15
  def message
14
- raise NotImplementedError
16
+ raise NoMethodError
15
17
  end
16
18
  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,5 @@
1
+ class Interaktor::Error::MissingExplicitSuccessError < Interaktor::Error::AttributeError
2
+ def message
3
+ "#{interaktor} interaktor execution finished successfully but requires one or more success parameters to have been provided: #{attributes.join(", ")}"
4
+ end
5
+ 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
@@ -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
  #
@@ -39,16 +39,42 @@ module Interaktor::Organizer
39
39
  #
40
40
  # @return [void]
41
41
  def call
42
- self.class.organized.each do |interaktor|
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
- # Make sure we have all required attributes
36
- missing_attrs = self.class
37
- .failure_attributes
38
- .reject { |failure_attr| failure_attributes.key?(failure_attr) }
39
- raise Interaktor::Error::MissingAttributeError.new(self.class.to_s, missing_attrs) if missing_attrs.any?
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
- # Make sure we have all required attributes
52
- missing_attrs = self.class
53
- .success_attributes
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
- # Make sure we haven't provided any unknown attributes
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.success_attributes.any?
108
- raise Interaktor::Error::MissingAttributeError.new(self, self.class.success_attributes)
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)