active_interaction 4.1.0 → 5.0.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.
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