active_interaction 4.1.0 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +150 -1
  3. data/CONTRIBUTING.md +11 -3
  4. data/README.md +256 -215
  5. data/lib/active_interaction/array_input.rb +77 -0
  6. data/lib/active_interaction/base.rb +14 -98
  7. data/lib/active_interaction/concerns/active_recordable.rb +3 -3
  8. data/lib/active_interaction/concerns/missable.rb +2 -2
  9. data/lib/active_interaction/errors.rb +6 -88
  10. data/lib/active_interaction/exceptions.rb +47 -0
  11. data/lib/active_interaction/filter/column.rb +59 -0
  12. data/lib/active_interaction/filter/error.rb +40 -0
  13. data/lib/active_interaction/filter.rb +40 -52
  14. data/lib/active_interaction/filters/abstract_date_time_filter.rb +9 -6
  15. data/lib/active_interaction/filters/abstract_numeric_filter.rb +7 -3
  16. data/lib/active_interaction/filters/array_filter.rb +40 -6
  17. data/lib/active_interaction/filters/boolean_filter.rb +4 -3
  18. data/lib/active_interaction/filters/date_filter.rb +1 -1
  19. data/lib/active_interaction/filters/date_time_filter.rb +1 -1
  20. data/lib/active_interaction/filters/decimal_filter.rb +1 -1
  21. data/lib/active_interaction/filters/float_filter.rb +1 -1
  22. data/lib/active_interaction/filters/hash_filter.rb +23 -15
  23. data/lib/active_interaction/filters/integer_filter.rb +1 -1
  24. data/lib/active_interaction/filters/interface_filter.rb +12 -12
  25. data/lib/active_interaction/filters/object_filter.rb +9 -3
  26. data/lib/active_interaction/filters/record_filter.rb +21 -11
  27. data/lib/active_interaction/filters/string_filter.rb +1 -1
  28. data/lib/active_interaction/filters/symbol_filter.rb +1 -1
  29. data/lib/active_interaction/filters/time_filter.rb +4 -4
  30. data/lib/active_interaction/hash_input.rb +43 -0
  31. data/lib/active_interaction/input.rb +23 -0
  32. data/lib/active_interaction/inputs.rb +161 -46
  33. data/lib/active_interaction/locale/en.yml +0 -1
  34. data/lib/active_interaction/locale/fr.yml +0 -1
  35. data/lib/active_interaction/locale/it.yml +0 -1
  36. data/lib/active_interaction/locale/ja.yml +0 -1
  37. data/lib/active_interaction/locale/pt-BR.yml +0 -1
  38. data/lib/active_interaction/modules/validation.rb +6 -17
  39. data/lib/active_interaction/version.rb +1 -1
  40. data/lib/active_interaction.rb +41 -36
  41. data/spec/active_interaction/array_input_spec.rb +166 -0
  42. data/spec/active_interaction/base_spec.rb +34 -248
  43. data/spec/active_interaction/concerns/active_modelable_spec.rb +3 -3
  44. data/spec/active_interaction/concerns/active_recordable_spec.rb +7 -7
  45. data/spec/active_interaction/concerns/hashable_spec.rb +8 -8
  46. data/spec/active_interaction/concerns/missable_spec.rb +9 -9
  47. data/spec/active_interaction/concerns/runnable_spec.rb +34 -32
  48. data/spec/active_interaction/errors_spec.rb +60 -43
  49. data/spec/active_interaction/{filter_column_spec.rb → filter/column_spec.rb} +3 -10
  50. data/spec/active_interaction/filter_spec.rb +27 -6
  51. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +2 -2
  52. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +2 -2
  53. data/spec/active_interaction/filters/array_filter_spec.rb +109 -16
  54. data/spec/active_interaction/filters/boolean_filter_spec.rb +12 -11
  55. data/spec/active_interaction/filters/date_filter_spec.rb +32 -27
  56. data/spec/active_interaction/filters/date_time_filter_spec.rb +34 -29
  57. data/spec/active_interaction/filters/decimal_filter_spec.rb +20 -18
  58. data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
  59. data/spec/active_interaction/filters/float_filter_spec.rb +19 -17
  60. data/spec/active_interaction/filters/hash_filter_spec.rb +16 -18
  61. data/spec/active_interaction/filters/integer_filter_spec.rb +24 -22
  62. data/spec/active_interaction/filters/interface_filter_spec.rb +105 -82
  63. data/spec/active_interaction/filters/object_filter_spec.rb +52 -36
  64. data/spec/active_interaction/filters/record_filter_spec.rb +61 -39
  65. data/spec/active_interaction/filters/string_filter_spec.rb +7 -7
  66. data/spec/active_interaction/filters/symbol_filter_spec.rb +6 -6
  67. data/spec/active_interaction/filters/time_filter_spec.rb +57 -34
  68. data/spec/active_interaction/hash_input_spec.rb +58 -0
  69. data/spec/active_interaction/i18n_spec.rb +22 -17
  70. data/spec/active_interaction/inputs_spec.rb +170 -18
  71. data/spec/active_interaction/integration/array_interaction_spec.rb +3 -7
  72. data/spec/active_interaction/integration/record_integration_spec.rb +5 -0
  73. data/spec/active_interaction/modules/validation_spec.rb +8 -31
  74. data/spec/spec_helper.rb +9 -0
  75. data/spec/support/concerns.rb +2 -2
  76. data/spec/support/filters.rb +27 -51
  77. data/spec/support/interactions.rb +4 -4
  78. metadata +43 -44
  79. data/lib/active_interaction/filter_column.rb +0 -57
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteraction
4
+ # Represents a processed array input.
5
+ class ArrayInput < Input
6
+ # @private
7
+ def initialize(filter, value: nil, error: nil, index_errors: false, children: [])
8
+ super(filter, value: value, error: error)
9
+
10
+ @filter = filter
11
+ @index_errors = index_errors
12
+ @children = children
13
+ end
14
+
15
+ # @overload children
16
+ # Child inputs if a nested filter is used.
17
+ #
18
+ # @return [Array<Input, ArrayInput, HashInput>]
19
+ attr_reader :children
20
+
21
+ # Any errors that occurred during processing.
22
+ #
23
+ # @return [Filter::Error]
24
+ def errors
25
+ return @errors if defined?(@errors)
26
+
27
+ return @errors = super if @error
28
+
29
+ child_errors = get_errors_by_index(children)
30
+
31
+ return @errors = super if child_errors.empty?
32
+
33
+ @errors ||=
34
+ if @index_errors
35
+ child_errors.map do |(error, i)|
36
+ name = attach_child_name(:"#{@filter.name}[#{i}]", error)
37
+ Filter::Error.new(error.filter, error.type, name: name)
38
+ end.freeze
39
+ else
40
+ error, = child_errors.first
41
+ [Filter::Error.new(@filter, error.type)].freeze
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def get_errors_by_index(children)
48
+ children.flat_map.with_index do |child, i|
49
+ child.errors.map do |error|
50
+ [error, i]
51
+ end
52
+ end
53
+ end
54
+
55
+ def attach_child_name(name, error)
56
+ return name unless error.name.present?
57
+
58
+ if children_are_arrays?(children)
59
+ :"#{name}#{error.name.to_s.sub(/\A[^\[]*/, '')}"
60
+ elsif children_are_hashes?(children)
61
+ :"#{name}.#{error.name.to_s[1..]}"
62
+ end
63
+ end
64
+
65
+ def children_are_arrays?(children)
66
+ return @children_are_arrays if defined?(@children_are_arrays)
67
+
68
+ @children_are_arrays = children.first&.is_a?(ArrayInput)
69
+ end
70
+
71
+ def children_are_hashes?(children)
72
+ return @children_are_hashes if defined?(@children_are_hashes)
73
+
74
+ @children_are_hashes = children.first&.is_a?(HashInput)
75
+ end
76
+ end
77
+ end
@@ -30,7 +30,7 @@ module ActiveInteraction
30
30
  include ActiveRecordable
31
31
  include Runnable
32
32
 
33
- define_callbacks :type_check
33
+ define_callbacks :filter
34
34
 
35
35
  class << self
36
36
  include Hashable
@@ -43,7 +43,7 @@ module ActiveInteraction
43
43
  #
44
44
  # Runs validations and if there are no errors it will call {#execute}.
45
45
  #
46
- # @param (see ActiveInteraction::Inputs.process)
46
+ # @param input [Hash, ActionController::Parameters]
47
47
  #
48
48
  # @return [Base]
49
49
 
@@ -88,7 +88,8 @@ module ActiveInteraction
88
88
  # rubocop:enable Naming/MemoizedInstanceVariableName
89
89
  end
90
90
 
91
- # @private
91
+ private
92
+
92
93
  # rubocop:disable Style/MissingRespondToMissing
93
94
  def method_missing(*args, &block)
94
95
  super do |klass, names, options|
@@ -99,8 +100,6 @@ module ActiveInteraction
99
100
  end
100
101
  # rubocop:enable Style/MissingRespondToMissing
101
102
 
102
- private
103
-
104
103
  # @param klass [Class]
105
104
  # @param name [Symbol]
106
105
  # @param options [Hash]
@@ -162,7 +161,9 @@ module ActiveInteraction
162
161
  def initialize(inputs = {})
163
162
  @_interaction_raw_inputs = inputs
164
163
 
165
- populate_filters_and_inputs(Inputs.process(inputs))
164
+ @_interaction_inputs = Inputs.new(inputs, self) do |name, input|
165
+ public_send("#{name}=", input.value)
166
+ end
166
167
  end
167
168
 
168
169
  # @!method compose(other, inputs = {})
@@ -186,113 +187,28 @@ module ActiveInteraction
186
187
  # Returns the inputs provided to {.run} or {.run!} after being cast based
187
188
  # on the filters in the class.
188
189
  #
189
- # @return [Hash{Symbol => Object}] All inputs passed to {.run} or {.run!}.
190
+ # @return [Inputs] All expected inputs passed to {.run} or {.run!}.
190
191
  def inputs
191
192
  @_interaction_inputs
192
193
  end
193
194
 
194
- # Returns `true` if the given key was in the hash passed to {.run}.
195
- # Otherwise returns `false`. Use this to figure out if an input was given,
196
- # even if it was `nil`. Keys within nested hash filter can also be checked
197
- # by passing them in series. Arrays can be checked in the same manor as
198
- # hashes by passing an index.
199
- #
200
- # @example
201
- # class Example < ActiveInteraction::Base
202
- # integer :x, default: nil
203
- # def execute; given?(:x) end
204
- # end
205
- # Example.run!() # => false
206
- # Example.run!(x: nil) # => true
207
- # Example.run!(x: rand) # => true
208
- #
209
- # @example Nested checks
210
- # class Example < ActiveInteraction::Base
211
- # hash :x, default: {} do
212
- # integer :y, default: nil
213
- # end
214
- # array :a, default: [] do
215
- # integer
216
- # end
217
- # def execute; given?(:x, :y) || given?(:a, 2) end
218
- # end
219
- # Example.run!() # => false
220
- # Example.run!(x: nil) # => false
221
- # Example.run!(x: {}) # => false
222
- # Example.run!(x: { y: nil }) # => true
223
- # Example.run!(x: { y: rand }) # => true
224
- # Example.run!(a: [1, 2]) # => false
225
- # Example.run!(a: [1, 2, 3]) # => true
226
- #
227
- # @param input [#to_sym]
228
- #
229
- # @return [Boolean]
230
- #
231
- # @since 2.1.0
232
- # rubocop:disable all
233
- def given?(input, *rest)
234
- filter_level = self.class
235
- input_level = @_interaction_raw_inputs
236
-
237
- [input, *rest].each do |key_or_index|
238
- if key_or_index.is_a?(Symbol) || key_or_index.is_a?(String)
239
- key = key_or_index.to_sym
240
- key_to_s = key_or_index.to_s
241
- filter_level = filter_level.filters[key]
242
-
243
- break false if filter_level.nil? || input_level.nil?
244
- if filter_level.accepts_grouped_inputs?
245
- break false unless input_level.key?(key) || input_level.key?(key_to_s) || Inputs.keys_for_group?(input_level.keys, key)
246
- else
247
- break false unless input_level.key?(key) || input_level.key?(key_to_s)
248
- end
249
-
250
- input_level = input_level[key] || input_level[key_to_s]
251
- else
252
- index = key_or_index
253
- filter_level = filter_level.filters.first.last
254
-
255
- break false if filter_level.nil? || input_level.nil?
256
- break false unless index.between?(-input_level.size, input_level.size - 1)
257
-
258
- input_level = input_level[index]
259
- end
260
- end && true
195
+ # @private
196
+ def read_attribute_for_validation(attribute)
197
+ super(errors.local_attribute(attribute))
261
198
  end
262
- # rubocop:enable all
263
199
 
264
200
  protected
265
201
 
266
202
  def run_validations!
267
- type_check
203
+ filter
268
204
 
269
205
  super if errors.empty?
270
206
  end
271
207
 
272
208
  private
273
209
 
274
- def populate_filters_and_inputs(inputs)
275
- @_interaction_inputs = Inputs.new
276
-
277
- self.class.filters.each do |name, filter|
278
- value =
279
- begin
280
- filter.clean(inputs[name], self)
281
- rescue InvalidValueError, MissingValueError, NoDefaultError
282
- # #type_check will add errors if appropriate.
283
- # We'll get the original value for the error.
284
- inputs[name]
285
- end
286
-
287
- @_interaction_inputs[name] = value
288
- public_send("#{name}=", value)
289
- end
290
-
291
- @_interaction_inputs.freeze
292
- end
293
-
294
- def type_check
295
- run_callbacks(:type_check) do
210
+ def filter
211
+ run_callbacks(:filter) do
296
212
  Validation.validate(self, self.class.filters, inputs).each do |attr, type, kwargs = {}|
297
213
  errors.add(attr, type, **kwargs)
298
214
  end
@@ -17,15 +17,15 @@ module ActiveInteraction
17
17
  # end
18
18
  #
19
19
  # Interaction.new.column_for_attribute(:email)
20
- # # => #<ActiveInteraction::FilterColumn:0x007faebeb2a6c8 @type=:string>
20
+ # # => #<ActiveInteraction::Filter::Column:0x007faebeb2a6c8 @type=:string>
21
21
  #
22
22
  # Interaction.new.column_for_attribute(:not_a_filter)
23
23
  # # => nil
24
24
  #
25
- # @return [FilterColumn, nil]
25
+ # @return [Filter::Column, nil]
26
26
  def column_for_attribute(name)
27
27
  filter = self.class.filters[name]
28
- FilterColumn.intern(filter.database_column_type) if filter
28
+ Filter::Column.intern(filter.database_column_type) if filter
29
29
  end
30
30
 
31
31
  # Returns true if a filter of that name exists.
@@ -7,6 +7,8 @@ module ActiveInteraction
7
7
  module Missable
8
8
  extend ActiveSupport::Concern
9
9
 
10
+ private
11
+
10
12
  # @param slug [Symbol]
11
13
  #
12
14
  # @yield [klass, args, options]
@@ -26,8 +28,6 @@ module ActiveInteraction
26
28
  self
27
29
  end
28
30
 
29
- private
30
-
31
31
  # @param slug [Symbol]
32
32
  #
33
33
  # @return [Filter, nil]
@@ -1,93 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveInteraction
4
- # Top-level error class. All other errors subclass this.
5
- #
6
- # @return [Class]
7
- Error = Class.new(StandardError)
8
-
9
- # Raised if a constant name is invalid.
10
- #
11
- # @return [Class]
12
- InvalidNameError = Class.new(Error)
13
-
14
- # Raised if a converter is invalid.
15
- #
16
- # @return [Class]
17
- InvalidConverterError = Class.new(Error)
18
-
19
- # Raised if a default value is invalid.
20
- #
21
- # @return [Class]
22
- InvalidDefaultError = Class.new(Error)
23
-
24
- # Raised if a filter has an invalid definition.
25
- #
26
- # @return [Class]
27
- InvalidFilterError = Class.new(Error)
28
-
29
- # Raised if an interaction is invalid.
30
- #
31
- # @return [Class]
32
- class InvalidInteractionError < Error
33
- attr_accessor :interaction
34
- end
35
-
36
- # Raised if a user-supplied value is invalid.
37
- #
38
- # @return [Class]
39
- InvalidValueError = Class.new(Error)
40
-
41
- # Raised if a filter cannot be found.
42
- #
43
- # @return [Class]
44
- MissingFilterError = Class.new(Error)
45
-
46
- # Raised if no value is given.
47
- #
48
- # @return [Class]
49
- MissingValueError = Class.new(Error)
50
-
51
- # Raised if there is no default value.
52
- #
53
- # @return [Class]
54
- NoDefaultError = Class.new(Error)
55
-
56
- # Raised if a user-supplied value to a nested hash input is invalid.
57
- #
58
- # @return [Class]
59
- class InvalidNestedValueError < InvalidValueError
60
- # @return [Symbol]
61
- attr_reader :filter_name
62
-
63
- # @return [Object]
64
- attr_reader :input_value
65
-
66
- # @param filter_name [Symbol]
67
- # @param input_value [Object]
68
- def initialize(filter_name, input_value)
69
- super("#{filter_name}: #{input_value.inspect}")
70
-
71
- @filter_name = filter_name
72
- @input_value = input_value
73
- end
74
- end
75
-
76
- # Used by {Runnable} to signal a failure when composing.
77
- #
78
- # @private
79
- class Interrupt < Error
80
- attr_reader :errors
81
-
82
- # @param errors [Runnable]
83
- def initialize(errors)
84
- super()
85
-
86
- @errors = errors
87
- end
88
- end
89
- private_constant :Interrupt
90
-
91
4
  # An extension that provides the ability to merge other errors into itself.
92
5
  class Errors < ActiveModel::Errors
93
6
  attr_accessor :backtrace
@@ -103,10 +16,15 @@ module ActiveInteraction
103
16
  self
104
17
  end
105
18
 
19
+ # @private
20
+ def local_attribute(attribute)
21
+ attribute.to_s.sub(/\A([^.\[]*).*\z/, '\1').to_sym
22
+ end
23
+
106
24
  private
107
25
 
108
26
  def attribute?(attribute)
109
- @base.respond_to?(attribute)
27
+ @base.respond_to?(local_attribute(attribute))
110
28
  end
111
29
 
112
30
  def detailed_error?(detail)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteraction
4
+ # Top-level error class. All other errors subclass this.
5
+ Error = Class.new(StandardError)
6
+
7
+ # Raised if a constant name is invalid.
8
+ InvalidNameError = Class.new(Error)
9
+
10
+ # Raised if a converter is invalid.
11
+ InvalidConverterError = Class.new(Error)
12
+
13
+ # Raised if a default value is invalid.
14
+ InvalidDefaultError = Class.new(Error)
15
+
16
+ # Raised if a filter has an invalid definition.
17
+ InvalidFilterError = Class.new(Error)
18
+
19
+ # Raised if an interaction is invalid.
20
+ class InvalidInteractionError < Error
21
+ # The interaction where the error occured.
22
+ #
23
+ # @return [ActiveInteraction::Base]
24
+ attr_accessor :interaction
25
+ end
26
+
27
+ # Raised if a filter cannot be found.
28
+ MissingFilterError = Class.new(Error)
29
+
30
+ # Raised if there is no default value.
31
+ NoDefaultError = Class.new(Error)
32
+
33
+ # Used by {Runnable} to signal a failure when composing.
34
+ #
35
+ # @private
36
+ class Interrupt < Error
37
+ attr_reader :errors
38
+
39
+ # @param errors [Runnable]
40
+ def initialize(errors)
41
+ super()
42
+
43
+ @errors = errors
44
+ end
45
+ end
46
+ private_constant :Interrupt
47
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteraction
4
+ class Filter
5
+ # A minimal implementation of an `ActiveRecord::ConnectionAdapters::Column`.
6
+ class Column
7
+ # @return [nil]
8
+ attr_reader :limit
9
+
10
+ # @return [Symbol]
11
+ attr_reader :type
12
+
13
+ class << self
14
+ # Find or create the `Filter::Column` for a specific type.
15
+ #
16
+ # @param type [Symbol] A database column type.
17
+ #
18
+ # @example
19
+ # Filter::Column.intern(:string)
20
+ # # => #<ActiveInteraction::Filter::Column:0x007feeaa649c @type=:string>
21
+ #
22
+ # Filter::Column.intern(:string)
23
+ # # => #<ActiveInteraction::Filter::Column:0x007feeaa649c @type=:string>
24
+ #
25
+ # Filter::Column.intern(:boolean)
26
+ # # => #<ActiveInteraction::Filter::Column:0x007feeab8a08 @type=:boolean>
27
+ #
28
+ # @return [Filter::Column]
29
+ def intern(type)
30
+ @columns ||= {}
31
+ @columns[type] ||= new(type)
32
+ end
33
+
34
+ private :new
35
+ end
36
+
37
+ # @param type [type] The database column type.
38
+ #
39
+ # @private
40
+ def initialize(type)
41
+ @type = type
42
+ end
43
+
44
+ # Returns `true` if the column is either of type :integer or :float.
45
+ #
46
+ # @return [Boolean]
47
+ def number?
48
+ %i[integer float].include?(type)
49
+ end
50
+
51
+ # Returns `true` if the column is of type :string.
52
+ #
53
+ # @return [Boolean]
54
+ def text?
55
+ type == :string
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+
5
+ module ActiveInteraction
6
+ class Filter
7
+ # A validation error that occurs while processing the filter.
8
+ class Error
9
+ # @private
10
+ def initialize(filter, type, name: nil)
11
+ @filter = filter
12
+ @name = name || filter.name
13
+ @type = type
14
+
15
+ @options = {}
16
+ options[:type] = I18n.translate("#{Base.i18n_scope}.types.#{filter.class.slug}") if type == :invalid_type
17
+ end
18
+
19
+ # The filter the error occured on.
20
+ #
21
+ # @return [ActiveInteraction::Filter]
22
+ attr_reader :filter
23
+
24
+ # The name of the error.
25
+ #
26
+ # @return [Symbol]
27
+ attr_reader :name
28
+
29
+ # Options passed to the error for error message creation.
30
+ #
31
+ # @return [Hash]
32
+ attr_reader :options
33
+
34
+ # The type of error.
35
+ #
36
+ # @return [Symbol]
37
+ attr_reader :type
38
+ end
39
+ end
40
+ end
@@ -73,38 +73,24 @@ module ActiveInteraction
73
73
  instance_eval(&block) if block_given?
74
74
  end
75
75
 
76
- # Convert a value into the expected type. If no value is given, fall back
77
- # to the default value.
76
+ # Processes the input through the filter and returns a variety of data
77
+ # about the input.
78
78
  #
79
79
  # @example
80
- # ActiveInteraction::Filter.new(:example).clean(nil, nil)
81
- # # => ActiveInteraction::MissingValueError: example
82
- # @example
83
- # ActiveInteraction::Filter.new(:example).clean(0, nil)
84
- # # => ActiveInteraction::InvalidValueError: example: 0
85
- # @example
86
- # ActiveInteraction::Filter.new(:example, default: nil).clean(nil, nil)
80
+ # input = ActiveInteraction::Filter.new(:example, default: nil).process(nil, nil)
81
+ # input.value
87
82
  # # => nil
88
- # @example
89
- # ActiveInteraction::Filter.new(:example, default: 0).clean(nil, nil)
90
- # # => ActiveInteraction::InvalidDefaultError: example: 0
91
83
  #
92
84
  # @param value [Object]
93
85
  # @param context [Base, nil]
94
86
  #
95
- # @return [Object]
87
+ # @return [Input, ArrayInput, HashInput]
96
88
  #
97
- # @raise [MissingValueError] If the value is missing and there is no
98
- # default.
99
- # @raise [InvalidValueError] If the value is invalid.
100
89
  # @raise (see #default)
101
- def clean(value, context)
102
- value = cast(value, context)
103
- if value.nil?
104
- default(context)
105
- else
106
- value
107
- end
90
+ def process(value, context)
91
+ value, error = cast(value, context)
92
+
93
+ Input.new(self, value: value, error: error)
108
94
  end
109
95
 
110
96
  # Get the default value.
@@ -129,13 +115,18 @@ module ActiveInteraction
129
115
  raise NoDefaultError, name unless default?
130
116
 
131
117
  value = raw_default(context)
132
- raise InvalidValueError if value.is_a?(GroupedInput)
118
+ raise InvalidDefaultError, "#{name}: #{value.inspect}" if value.is_a?(GroupedInput)
133
119
 
134
- cast(value, context)
135
- rescue InvalidNestedValueError => e
136
- raise InvalidDefaultError, "#{name}: #{value.inspect} (#{e})"
137
- rescue InvalidValueError, MissingValueError
138
- raise InvalidDefaultError, "#{name}: #{value.inspect}"
120
+ if value.nil?
121
+ nil
122
+ else
123
+ default = process(value, context)
124
+ if default.errors.any? && default.errors.first.is_a?(Filter::Error)
125
+ raise InvalidDefaultError, "#{name}: #{value.inspect}"
126
+ end
127
+
128
+ default.value
129
+ end
139
130
  end
140
131
 
141
132
  # Get the description.
@@ -195,30 +186,26 @@ module ActiveInteraction
195
186
 
196
187
  private
197
188
 
198
- # rubocop:disable Metrics/MethodLength
199
- def cast(value, context, convert: true, reconstantize: true)
189
+ # rubocop:disable Metrics/PerceivedComplexity
190
+ def cast(value, context, convertize: true, reconstantize: true)
200
191
  if matches?(value)
201
- adjust_output(value, context)
202
- # we can't use `nil?` because BasicObject doesn't have it
203
- elsif value == nil # rubocop:disable Style/NilComparison
204
- raise MissingValueError, name unless default?
205
-
206
- nil
192
+ [adjust_output(value, context), nil]
193
+ elsif value == nil # rubocop:disable Style/NilComparison - BasicObject does not have `nil?`
194
+ default? ? [default(context), nil] : [value, Filter::Error.new(self, :missing)]
207
195
  elsif reconstantize
208
- send(__method__, value, context,
209
- convert: convert,
210
- reconstantize: false
211
- )
212
- elsif convert
213
- send(__method__, convert(value), context,
214
- convert: false,
215
- reconstantize: reconstantize
216
- )
196
+ send(__method__, value, context, convertize: convertize, reconstantize: false)
197
+ elsif convertize
198
+ value, error = convert(value)
199
+ if error
200
+ [value, error]
201
+ else
202
+ send(__method__, value, context, convertize: false, reconstantize: reconstantize)
203
+ end
217
204
  else
218
- raise InvalidValueError, "#{name}: #{describe(value)}"
205
+ [value, Filter::Error.new(self, :invalid_type)]
219
206
  end
220
207
  end
221
- # rubocop:enable Metrics/MethodLength
208
+ # rubocop:enable Metrics/PerceivedComplexity
222
209
 
223
210
  def matches?(_value)
224
211
  false
@@ -229,7 +216,7 @@ module ActiveInteraction
229
216
  end
230
217
 
231
218
  def convert(value)
232
- value
219
+ [value, nil]
233
220
  end
234
221
 
235
222
  def klass
@@ -246,9 +233,10 @@ module ActiveInteraction
246
233
  value = options.fetch(:default)
247
234
  return value unless value.is_a?(Proc)
248
235
 
249
- case value.arity
250
- when 1 then context.instance_exec(self, &value)
251
- else context.instance_exec(&value)
236
+ if value.arity == 1
237
+ context.instance_exec(self, &value)
238
+ else
239
+ context.instance_exec(&value)
252
240
  end
253
241
  end
254
242
  end