interaktor 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)