active_interaction 4.1.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -1
  3. data/README.md +63 -28
  4. data/lib/active_interaction/array_input.rb +77 -0
  5. data/lib/active_interaction/base.rb +14 -98
  6. data/lib/active_interaction/concerns/active_recordable.rb +3 -3
  7. data/lib/active_interaction/concerns/missable.rb +2 -2
  8. data/lib/active_interaction/errors.rb +6 -88
  9. data/lib/active_interaction/exceptions.rb +47 -0
  10. data/lib/active_interaction/filter/column.rb +59 -0
  11. data/lib/active_interaction/filter/error.rb +40 -0
  12. data/lib/active_interaction/filter.rb +44 -53
  13. data/lib/active_interaction/filters/abstract_date_time_filter.rb +9 -6
  14. data/lib/active_interaction/filters/abstract_numeric_filter.rb +7 -3
  15. data/lib/active_interaction/filters/array_filter.rb +34 -6
  16. data/lib/active_interaction/filters/boolean_filter.rb +4 -3
  17. data/lib/active_interaction/filters/date_filter.rb +1 -1
  18. data/lib/active_interaction/filters/date_time_filter.rb +1 -1
  19. data/lib/active_interaction/filters/decimal_filter.rb +1 -1
  20. data/lib/active_interaction/filters/float_filter.rb +1 -1
  21. data/lib/active_interaction/filters/hash_filter.rb +23 -15
  22. data/lib/active_interaction/filters/integer_filter.rb +1 -1
  23. data/lib/active_interaction/filters/interface_filter.rb +12 -12
  24. data/lib/active_interaction/filters/object_filter.rb +9 -3
  25. data/lib/active_interaction/filters/record_filter.rb +21 -11
  26. data/lib/active_interaction/filters/string_filter.rb +1 -1
  27. data/lib/active_interaction/filters/symbol_filter.rb +1 -1
  28. data/lib/active_interaction/filters/time_filter.rb +4 -4
  29. data/lib/active_interaction/hash_input.rb +43 -0
  30. data/lib/active_interaction/input.rb +23 -0
  31. data/lib/active_interaction/inputs.rb +157 -46
  32. data/lib/active_interaction/locale/en.yml +0 -1
  33. data/lib/active_interaction/locale/fr.yml +0 -1
  34. data/lib/active_interaction/locale/it.yml +0 -1
  35. data/lib/active_interaction/locale/ja.yml +0 -1
  36. data/lib/active_interaction/locale/pt-BR.yml +0 -1
  37. data/lib/active_interaction/modules/validation.rb +6 -17
  38. data/lib/active_interaction/version.rb +1 -1
  39. data/lib/active_interaction.rb +43 -36
  40. data/spec/active_interaction/array_input_spec.rb +166 -0
  41. data/spec/active_interaction/base_spec.rb +15 -240
  42. data/spec/active_interaction/concerns/active_modelable_spec.rb +3 -3
  43. data/spec/active_interaction/concerns/active_recordable_spec.rb +7 -7
  44. data/spec/active_interaction/concerns/hashable_spec.rb +8 -8
  45. data/spec/active_interaction/concerns/missable_spec.rb +9 -9
  46. data/spec/active_interaction/concerns/runnable_spec.rb +34 -32
  47. data/spec/active_interaction/errors_spec.rb +60 -43
  48. data/spec/active_interaction/{filter_column_spec.rb → filter/column_spec.rb} +3 -10
  49. data/spec/active_interaction/filter_spec.rb +6 -6
  50. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +2 -2
  51. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +2 -2
  52. data/spec/active_interaction/filters/array_filter_spec.rb +99 -16
  53. data/spec/active_interaction/filters/boolean_filter_spec.rb +12 -11
  54. data/spec/active_interaction/filters/date_filter_spec.rb +32 -27
  55. data/spec/active_interaction/filters/date_time_filter_spec.rb +34 -29
  56. data/spec/active_interaction/filters/decimal_filter_spec.rb +20 -18
  57. data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
  58. data/spec/active_interaction/filters/float_filter_spec.rb +19 -17
  59. data/spec/active_interaction/filters/hash_filter_spec.rb +16 -18
  60. data/spec/active_interaction/filters/integer_filter_spec.rb +24 -22
  61. data/spec/active_interaction/filters/interface_filter_spec.rb +105 -82
  62. data/spec/active_interaction/filters/object_filter_spec.rb +52 -36
  63. data/spec/active_interaction/filters/record_filter_spec.rb +61 -39
  64. data/spec/active_interaction/filters/string_filter_spec.rb +7 -7
  65. data/spec/active_interaction/filters/symbol_filter_spec.rb +6 -6
  66. data/spec/active_interaction/filters/time_filter_spec.rb +57 -34
  67. data/spec/active_interaction/hash_input_spec.rb +58 -0
  68. data/spec/active_interaction/i18n_spec.rb +22 -17
  69. data/spec/active_interaction/inputs_spec.rb +167 -23
  70. data/spec/active_interaction/integration/array_interaction_spec.rb +3 -7
  71. data/spec/active_interaction/modules/validation_spec.rb +8 -31
  72. data/spec/spec_helper.rb +8 -0
  73. data/spec/support/concerns.rb +2 -2
  74. data/spec/support/filters.rb +27 -51
  75. data/spec/support/interactions.rb +4 -4
  76. metadata +40 -91
  77. data/lib/active_interaction/filter_column.rb +0 -57
@@ -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.
@@ -126,16 +112,24 @@ module ActiveInteraction
126
112
  # @raise [NoDefaultError] If the default is missing.
127
113
  # @raise [InvalidDefaultError] If the default is invalid.
128
114
  def default(context = nil)
115
+ return @default if defined?(@default)
116
+
129
117
  raise NoDefaultError, name unless default?
130
118
 
131
119
  value = raw_default(context)
132
- raise InvalidValueError if value.is_a?(GroupedInput)
133
-
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
+ raise InvalidDefaultError, "#{name}: #{value.inspect}" if value.is_a?(GroupedInput)
121
+
122
+ @default =
123
+ if value.nil?
124
+ nil
125
+ else
126
+ default = process(value, context)
127
+ if default.errors.any? && default.errors.first.is_a?(Filter::Error)
128
+ raise InvalidDefaultError, "#{name}: #{value.inspect}"
129
+ end
130
+
131
+ default.value
132
+ end
139
133
  end
140
134
 
141
135
  # Get the description.
@@ -195,30 +189,26 @@ module ActiveInteraction
195
189
 
196
190
  private
197
191
 
198
- # rubocop:disable Metrics/MethodLength
199
- def cast(value, context, convert: true, reconstantize: true)
192
+ # rubocop:disable Metrics/PerceivedComplexity
193
+ def cast(value, context, convertize: true, reconstantize: true)
200
194
  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
195
+ [adjust_output(value, context), nil]
196
+ elsif value == nil # rubocop:disable Style/NilComparison - BasicObject does not have `nil?`
197
+ default? ? [default(context), nil] : [value, Filter::Error.new(self, :missing)]
207
198
  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
- )
199
+ send(__method__, value, context, convertize: convertize, reconstantize: false)
200
+ elsif convertize
201
+ value, error = convert(value)
202
+ if error
203
+ [value, error]
204
+ else
205
+ send(__method__, value, context, convertize: false, reconstantize: reconstantize)
206
+ end
217
207
  else
218
- raise InvalidValueError, "#{name}: #{describe(value)}"
208
+ [value, Filter::Error.new(self, :invalid_type)]
219
209
  end
220
210
  end
221
- # rubocop:enable Metrics/MethodLength
211
+ # rubocop:enable Metrics/PerceivedComplexity
222
212
 
223
213
  def matches?(_value)
224
214
  false
@@ -229,7 +219,7 @@ module ActiveInteraction
229
219
  end
230
220
 
231
221
  def convert(value)
232
- value
222
+ [value, nil]
233
223
  end
234
224
 
235
225
  def klass
@@ -246,9 +236,10 @@ module ActiveInteraction
246
236
  value = options.fetch(:default)
247
237
  return value unless value.is_a?(Proc)
248
238
 
249
- case value.arity
250
- when 1 then context.instance_exec(self, &value)
251
- else context.instance_exec(&value)
239
+ if value.arity == 1
240
+ context.instance_exec(self, &value)
241
+ else
242
+ context.instance_exec(&value)
252
243
  end
253
244
  end
254
245
  end
@@ -31,14 +31,16 @@ module ActiveInteraction
31
31
  def convert(value)
32
32
  if value.respond_to?(:to_str)
33
33
  value = value.to_str
34
- value.blank? ? send(__method__, nil) : convert_string(value)
34
+ if value.blank?
35
+ send(__method__, nil)
36
+ else
37
+ [convert_string(value), nil]
38
+ end
35
39
  elsif value.is_a?(GroupedInput)
36
- convert_grouped_input(value)
40
+ [convert_grouped_input(value), nil]
37
41
  else
38
42
  super
39
43
  end
40
- rescue ArgumentError
41
- value
42
44
  rescue NoMethodError # BasicObject
43
45
  super
44
46
  end
@@ -47,9 +49,10 @@ module ActiveInteraction
47
49
  if format?
48
50
  klass.strptime(value, format)
49
51
  else
50
- klass.parse(value) ||
51
- (raise ArgumentError, "no time information in #{value.inspect}")
52
+ klass.parse(value) || value
52
53
  end
54
+ rescue ArgumentError
55
+ value
53
56
  end
54
57
 
55
58
  def convert_grouped_input(value)
@@ -21,12 +21,16 @@ module ActiveInteraction
21
21
 
22
22
  def convert(value)
23
23
  if value.is_a?(Numeric)
24
- safe_converter(value)
24
+ [safe_converter(value), nil]
25
25
  elsif value.respond_to?(:to_int)
26
- safe_converter(value.to_int)
26
+ [safe_converter(value.to_int), nil]
27
27
  elsif value.respond_to?(:to_str)
28
28
  value = value.to_str
29
- value.blank? ? send(__method__, nil) : safe_converter(value)
29
+ if value.blank?
30
+ send(__method__, nil)
31
+ else
32
+ [safe_converter(value), nil]
33
+ end
30
34
  else
31
35
  super
32
36
  end
@@ -8,6 +8,8 @@ module ActiveInteraction
8
8
  #
9
9
  # @!macro filter_method_params
10
10
  # @param block [Proc] filter method to apply to each element
11
+ # @option options [Boolean] :index_errors (ActiveRecord.index_nested_attribute_errors) returns errors with an
12
+ # index
11
13
  #
12
14
  # @example
13
15
  # array :ids
@@ -36,8 +38,37 @@ module ActiveInteraction
36
38
 
37
39
  register :array
38
40
 
41
+ def process(value, context)
42
+ input = super
43
+
44
+ return ArrayInput.new(self, value: input.value, error: input.errors.first) if input.errors.any?
45
+ return ArrayInput.new(self, value: default(context), error: input.errors.first) if input.value.nil?
46
+
47
+ value = input.value
48
+ error = nil
49
+ children = []
50
+
51
+ unless filters.empty?
52
+ value.each do |item|
53
+ children.push(filters[:'0'].process(item, context))
54
+ end
55
+ end
56
+
57
+ ArrayInput.new(self, value: value, error: error, children: children, index_errors: index_errors?)
58
+ end
59
+
39
60
  private
40
61
 
62
+ def index_errors?
63
+ default =
64
+ if ::ActiveRecord.respond_to?(:index_nested_attribute_errors)
65
+ ::ActiveRecord.index_nested_attribute_errors # Moved to here in Rails 7.0
66
+ else
67
+ ::ActiveRecord::Base.index_nested_attribute_errors
68
+ end
69
+ options.fetch(:index_errors, default)
70
+ end
71
+
41
72
  def klasses
42
73
  %w[
43
74
  ActiveRecord::Relation
@@ -55,16 +86,13 @@ module ActiveInteraction
55
86
  false
56
87
  end
57
88
 
58
- def adjust_output(value, context)
59
- return value if filters.empty?
60
-
61
- filter = filters.values.first
62
- value.map { |e| filter.clean(e, context) }
89
+ def adjust_output(value, _context)
90
+ value.to_a
63
91
  end
64
92
 
65
93
  def convert(value)
66
94
  if value.respond_to?(:to_ary)
67
- value.to_ary
95
+ [value.to_ary, nil]
68
96
  else
69
97
  super
70
98
  end
@@ -6,7 +6,8 @@ module ActiveInteraction
6
6
  # Creates accessors for the attributes and ensures that values passed to
7
7
  # the attributes are Booleans. The strings `"1"`, `"true"`, and `"on"`
8
8
  # (case-insensitive) are converted to `true` while the strings `"0"`,
9
- # `"false"`, and `"off"` are converted to `false`.
9
+ # `"false"`, and `"off"` are converted to `false`. Blank strings are
10
+ # treated as a `nil` value.
10
11
  #
11
12
  # @!macro filter_method_params
12
13
  #
@@ -38,9 +39,9 @@ module ActiveInteraction
38
39
 
39
40
  case value
40
41
  when /\A(?:0|false|off)\z/i
41
- false
42
+ [false, nil]
42
43
  when /\A(?:1|true|on)\z/i
43
- true
44
+ [true, nil]
44
45
  else
45
46
  super
46
47
  end
@@ -6,7 +6,7 @@ module ActiveInteraction
6
6
  # Creates accessors for the attributes and ensures that values passed to
7
7
  # the attributes are Dates. String values are processed using `parse`
8
8
  # unless the format option is given, in which case they will be
9
- # processed with `strptime`.
9
+ # processed with `strptime`. Blank strings are treated as a `nil` value.
10
10
  #
11
11
  # @!macro filter_method_params
12
12
  # @option options [String] :format parse strings using this format string
@@ -6,7 +6,7 @@ module ActiveInteraction
6
6
  # Creates accessors for the attributes and ensures that values passed to
7
7
  # the attributes are DateTimes. String values are processed using
8
8
  # `parse` unless the format option is given, in which case they will be
9
- # processed with `strptime`.
9
+ # processed with `strptime`. Blank strings are treated as a `nil` value.
10
10
  #
11
11
  # @!macro filter_method_params
12
12
  # @option options [String] :format parse strings using this format string
@@ -7,7 +7,7 @@ module ActiveInteraction
7
7
  # @!method self.decimal(*attributes, options = {})
8
8
  # Creates accessors for the attributes and ensures that values passed to
9
9
  # the attributes are BigDecimals. Numerics and String values are
10
- # converted into BigDecimals.
10
+ # converted into BigDecimals. Blank strings are treated as a `nil` value.
11
11
  #
12
12
  # @!macro filter_method_params
13
13
  #
@@ -5,7 +5,7 @@ module ActiveInteraction
5
5
  # @!method self.float(*attributes, options = {})
6
6
  # Creates accessors for the attributes and ensures that values passed to
7
7
  # the attributes are Floats. Integer and String values are converted
8
- # into Floats.
8
+ # into Floats. Blank strings are treated as a `nil` value.
9
9
  #
10
10
  # @!macro filter_method_params
11
11
  #
@@ -25,6 +25,26 @@ module ActiveInteraction
25
25
 
26
26
  register :hash
27
27
 
28
+ def process(value, context)
29
+ input = super
30
+
31
+ return HashInput.new(self, value: input.value, error: input.errors.first) if input.errors.first
32
+ return HashInput.new(self, value: default(context), error: input.errors.first) if input.value.nil?
33
+
34
+ value = strip? ? HashWithIndifferentAccess.new : input.value
35
+ error = nil
36
+ children = {}
37
+
38
+ filters.each do |name, filter|
39
+ filter.process(input.value[name], context).tap do |result|
40
+ value[name] = result.value
41
+ children[name.to_sym] = result
42
+ end
43
+ end
44
+
45
+ HashInput.new(self, value: value, error: error, children: children)
46
+ end
47
+
28
48
  private
29
49
 
30
50
  def matches?(value)
@@ -33,29 +53,17 @@ module ActiveInteraction
33
53
  false
34
54
  end
35
55
 
36
- def clean_value(hash, name, filter, value, context)
37
- hash[name] = filter.clean(value[name], context)
38
- rescue InvalidValueError, MissingValueError
39
- raise InvalidNestedValueError.new(name, value[name])
40
- end
41
-
42
56
  def strip?
43
57
  options.fetch(:strip, true)
44
58
  end
45
59
 
46
- def adjust_output(value, context)
47
- value = ActiveSupport::HashWithIndifferentAccess.new(value.to_hash)
48
-
49
- initial = strip? ? ActiveSupport::HashWithIndifferentAccess.new : value
50
-
51
- filters.each_with_object(initial) do |(name, filter), hash|
52
- clean_value(hash, name.to_s, filter, value, context)
53
- end
60
+ def adjust_output(value, _context)
61
+ ActiveSupport::HashWithIndifferentAccess.new(value)
54
62
  end
55
63
 
56
64
  def convert(value)
57
65
  if value.respond_to?(:to_hash)
58
- value.to_hash
66
+ [value.to_hash, nil]
59
67
  else
60
68
  super
61
69
  end
@@ -5,7 +5,7 @@ module ActiveInteraction
5
5
  # @!method self.integer(*attributes, options = {})
6
6
  # Creates accessors for the attributes and ensures that values passed to
7
7
  # the attributes are Integers. String values are converted into
8
- # Integers.
8
+ # Integers. Blank strings are treated as a `nil` value.
9
9
  #
10
10
  # @!macro filter_method_params
11
11
  # @option options [Integer] :base (10) The base used to convert strings