active_interaction 4.0.5 → 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 +149 -6
  3. data/README.md +67 -32
  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 +36 -10
  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 -24
  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 +45 -95
  77. data/lib/active_interaction/filter_column.rb +0 -57
@@ -1,24 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
4
+
3
5
  module ActiveInteraction
4
6
  # Holds inputs passed to the interaction.
5
- class Inputs < DelegateClass(Hash)
6
- class << self
7
- # matches inputs like "key(1i)"
8
- GROUPED_INPUT_PATTERN = /
9
- \A
10
- (?<key>.+) # extracts "key"
11
- \((?<index>\d+)i\) # extracts "1"
12
- \z
13
- /x.freeze
14
- private_constant :GROUPED_INPUT_PATTERN
7
+ class Inputs
8
+ include Enumerable
9
+ extend Forwardable
15
10
 
16
- # @private
17
- def keys_for_group?(keys, group_key)
18
- search_key = /\A#{group_key}\(\d+i\)\z/
19
- keys.any? { |key| search_key.match?(key) }
20
- end
11
+ # matches inputs like "key(1i)"
12
+ GROUPED_INPUT_PATTERN = /
13
+ \A
14
+ (?<key>.+) # extracts "key"
15
+ \((?<index>\d+)i\) # extracts "1"
16
+ \z
17
+ /x.freeze
18
+ private_constant :GROUPED_INPUT_PATTERN
21
19
 
20
+ class << self
22
21
  # Checking `syscall` is the result of what appears to be a bug in Ruby.
23
22
  # https://bugs.ruby-lang.org/issues/15597
24
23
  # @private
@@ -34,47 +33,159 @@ module ActiveInteraction
34
33
  !Object.private_method_defined?(name)
35
34
  )
36
35
  end
36
+ end
37
37
 
38
- # @param inputs [Hash, ActionController::Parameters, ActiveInteraction::Inputs] Attribute values to set.
39
- #
40
- # @private
41
- def process(inputs)
42
- normalize_inputs!(inputs)
43
- .stringify_keys
44
- .sort
45
- .each_with_object({}) do |(k, v), h|
46
- next if reserved?(k)
47
-
48
- if (group = GROUPED_INPUT_PATTERN.match(k))
49
- assign_to_grouped_input!(h, group[:key], group[:index], v)
50
- else
51
- h[k.to_sym] = v
52
- end
53
- end
38
+ # @private
39
+ def initialize(raw_inputs, base)
40
+ @base = base
41
+ @normalized_inputs = normalize(raw_inputs)
42
+ @inputs = base.class.filters.each_with_object({}) do |(name, filter), inputs|
43
+ inputs[name] = filter.process(@normalized_inputs[name], base)
44
+
45
+ yield name, inputs[name] if block_given?
54
46
  end
47
+ end
55
48
 
56
- private
49
+ # @private
50
+ def normalized
51
+ @normalized_inputs
52
+ end
57
53
 
58
- def normalize_inputs!(inputs)
59
- return inputs if inputs.is_a?(Hash) || inputs.is_a?(Inputs)
54
+ # Turn the inputs into a Hash.
55
+ #
56
+ # @return [Hash]
57
+ def to_h
58
+ @to_h ||= @inputs.transform_values(&:value).freeze
59
+ end
60
60
 
61
- parameters = 'ActionController::Parameters'
62
- klass = parameters.safe_constantize
63
- return inputs.to_unsafe_h if klass && inputs.is_a?(klass)
61
+ def_delegators :to_h,
62
+ :[],
63
+ :dig,
64
+ :each,
65
+ :each_key,
66
+ :each_pair,
67
+ :each_value,
68
+ :empty?,
69
+ :except,
70
+ :fetch,
71
+ :fetch_values,
72
+ :filter,
73
+ :flatten,
74
+ :has_key?,
75
+ :has_value?,
76
+ :include?,
77
+ :inspect,
78
+ :key,
79
+ :key?,
80
+ :keys,
81
+ :length,
82
+ :member?,
83
+ :merge,
84
+ :rassoc,
85
+ :reject,
86
+ :select,
87
+ :size,
88
+ :slice,
89
+ :store,
90
+ :to_a,
91
+ :to_s,
92
+ :value?,
93
+ :values,
94
+ :values_at
64
95
 
65
- raise ArgumentError, "inputs must be a hash or #{parameters}"
66
- end
96
+ # Returns `true` if the given key was in the hash passed to {.run}.
97
+ # Otherwise returns `false`. Use this to figure out if an input was given,
98
+ # even if it was `nil`. Keys within nested hash filter can also be checked
99
+ # by passing them in series. Arrays can be checked in the same manor as
100
+ # hashes by passing an index.
101
+ #
102
+ # @example
103
+ # class Example < ActiveInteraction::Base
104
+ # integer :x, default: nil
105
+ # def execute; given?(:x) end
106
+ # end
107
+ # Example.run!() # => false
108
+ # Example.run!(x: nil) # => true
109
+ # Example.run!(x: rand) # => true
110
+ #
111
+ # @example Nested checks
112
+ # class Example < ActiveInteraction::Base
113
+ # hash :x, default: {} do
114
+ # integer :y, default: nil
115
+ # end
116
+ # array :a, default: [] do
117
+ # integer
118
+ # end
119
+ # def execute; given?(:x, :y) || given?(:a, 2) end
120
+ # end
121
+ # Example.run!() # => false
122
+ # Example.run!(x: nil) # => false
123
+ # Example.run!(x: {}) # => false
124
+ # Example.run!(x: { y: nil }) # => true
125
+ # Example.run!(x: { y: rand }) # => true
126
+ # Example.run!(a: [1, 2]) # => false
127
+ # Example.run!(a: [1, 2, 3]) # => true
128
+ #
129
+ # @param input [#to_sym]
130
+ #
131
+ # @return [Boolean]
132
+ #
133
+ # rubocop:disable all
134
+ def given?(input, *rest)
135
+ filter_level = @base.class
136
+ input_level = @normalized_inputs
67
137
 
68
- def assign_to_grouped_input!(inputs, key, index, value)
69
- key = key.to_sym
138
+ [input, *rest].each do |key_or_index|
139
+ if key_or_index.is_a?(Symbol) || key_or_index.is_a?(String)
140
+ key = key_or_index.to_sym
141
+ key_to_s = key_or_index.to_s
142
+ filter_level = filter_level.filters[key]
70
143
 
71
- inputs[key] = GroupedInput.new unless inputs[key].is_a?(GroupedInput)
72
- inputs[key][index] = value
73
- end
144
+ break false if filter_level.nil? || input_level.nil?
145
+ break false unless input_level.key?(key) || input_level.key?(key_to_s)
146
+
147
+ input_level = input_level[key] || input_level[key_to_s]
148
+ else
149
+ index = key_or_index
150
+ filter_level = filter_level.filters.first.last
151
+
152
+ break false if filter_level.nil? || input_level.nil?
153
+ break false unless index.between?(-input_level.size, input_level.size - 1)
154
+
155
+ input_level = input_level[index]
156
+ end
157
+ end && true
74
158
  end
159
+ # rubocop:enable all
160
+
161
+ private
162
+
163
+ def normalize(inputs)
164
+ convert(inputs)
165
+ .sort
166
+ .each_with_object({}) do |(k, v), h|
167
+ next if self.class.reserved?(k)
168
+
169
+ if (group = GROUPED_INPUT_PATTERN.match(k))
170
+ assign_to_grouped_input!(h, group[:key], group[:index], v)
171
+ else
172
+ h[k.to_sym] = v
173
+ end
174
+ end
175
+ end
176
+
177
+ def convert(inputs)
178
+ return inputs.stringify_keys if inputs.is_a?(Hash)
179
+ return inputs.to_unsafe_h.stringify_keys if inputs.is_a?(ActionController::Parameters)
180
+
181
+ raise ArgumentError, 'inputs must be a hash or ActionController::Parameters'
182
+ end
183
+
184
+ def assign_to_grouped_input!(inputs, key, index, value)
185
+ key = key.to_sym
75
186
 
76
- def initialize(inputs = {})
77
- super(inputs)
187
+ inputs[key] = GroupedInput.new unless inputs[key].is_a?(GroupedInput)
188
+ inputs[key][index] = value
78
189
  end
79
190
  end
80
191
  end
@@ -3,7 +3,6 @@ en:
3
3
  errors:
4
4
  messages:
5
5
  invalid: is invalid
6
- invalid_nested: has an invalid nested value (%{name} => %{value})
7
6
  invalid_type: is not a valid %{type}
8
7
  missing: is required
9
8
  types:
@@ -3,7 +3,6 @@ fr:
3
3
  errors:
4
4
  messages:
5
5
  invalid: est invalide
6
- invalid_nested: contient une valeur imbriquée invalide (%{name} => %{value})
7
6
  invalid_type: a un type invalide (%{type})
8
7
  missing: est obligatoire
9
8
  types:
@@ -3,7 +3,6 @@ it:
3
3
  errors:
4
4
  messages:
5
5
  invalid: non è valido
6
- invalid_nested: ha un valore nidificato non valido (%{name} => %{value})
7
6
  invalid_type: non è di tipo %{type}
8
7
  missing: è obbligatorio
9
8
  types:
@@ -3,7 +3,6 @@ ja:
3
3
  errors:
4
4
  messages:
5
5
  invalid: は無効です
6
- invalid_nested: は無効なネストされた値です (%{name} => %{value})
7
6
  invalid_type: は無効な %{type} です
8
7
  missing: が必要です
9
8
  types:
@@ -3,7 +3,6 @@ pt-BR:
3
3
  errors:
4
4
  messages:
5
5
  invalid: é inválido
6
- invalid_nested: possui um valor aninhado inválido (%{name} => %{value})
7
6
  invalid_type: não é um(a) %{type} válido(a)
8
7
  missing: é obrigatório
9
8
  types:
@@ -8,26 +8,15 @@ module ActiveInteraction
8
8
  class << self
9
9
  # @param context [Base]
10
10
  # @param filters [Hash{Symbol => Filter}]
11
- # @param inputs [Hash{Symbol => Object}]
11
+ # @param inputs [Inputs]
12
12
  def validate(context, filters, inputs)
13
13
  filters.each_with_object([]) do |(name, filter), errors|
14
- filter.clean(inputs[name], context)
15
- rescue NoDefaultError
16
- nil
17
- rescue InvalidNestedValueError => e
18
- errors << [filter.name, :invalid_nested, { name: e.filter_name.inspect, value: e.input_value.inspect }]
19
- rescue InvalidValueError
20
- errors << [filter.name, :invalid_type, { type: type(filter) }]
21
- rescue MissingValueError
22
- errors << [filter.name, :missing]
23
- end
24
- end
14
+ input = filter.process(inputs[name], context)
25
15
 
26
- private
27
-
28
- # @param filter [Filter]
29
- def type(filter)
30
- I18n.translate("#{Base.i18n_scope}.types.#{filter.class.slug}")
16
+ input.errors.each do |error|
17
+ errors << [error.name, error.type, error.options]
18
+ end
19
+ end
31
20
  end
32
21
  end
33
22
  end
@@ -4,5 +4,5 @@ module ActiveInteraction
4
4
  # The version number.
5
5
  #
6
6
  # @return [Gem::Version]
7
- VERSION = Gem::Version.new('4.0.5')
7
+ VERSION = Gem::Version.new('5.0.0')
8
8
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_model'
4
- require 'active_support/hash_with_indifferent_access'
4
+ require 'active_record'
5
+ require 'active_support/core_ext/hash/indifferent_access'
6
+ require 'action_controller'
5
7
 
6
8
  # Manage application specific business logic.
7
9
  #
@@ -10,41 +12,46 @@ require 'active_support/hash_with_indifferent_access'
10
12
  module ActiveInteraction
11
13
  end
12
14
 
13
- require 'active_interaction/version'
14
- require 'active_interaction/errors'
15
-
16
- require 'active_interaction/concerns/active_modelable'
17
- require 'active_interaction/concerns/active_recordable'
18
- require 'active_interaction/concerns/hashable'
19
- require 'active_interaction/concerns/missable'
20
- require 'active_interaction/concerns/runnable'
21
-
22
- require 'active_interaction/grouped_input'
23
- require 'active_interaction/inputs'
24
-
25
- require 'active_interaction/modules/validation'
26
-
27
- require 'active_interaction/filter_column'
28
- require 'active_interaction/filter'
29
- require 'active_interaction/filters/interface_filter'
30
- require 'active_interaction/filters/abstract_date_time_filter'
31
- require 'active_interaction/filters/abstract_numeric_filter'
32
- require 'active_interaction/filters/array_filter'
33
- require 'active_interaction/filters/boolean_filter'
34
- require 'active_interaction/filters/date_filter'
35
- require 'active_interaction/filters/date_time_filter'
36
- require 'active_interaction/filters/decimal_filter'
37
- require 'active_interaction/filters/file_filter'
38
- require 'active_interaction/filters/float_filter'
39
- require 'active_interaction/filters/hash_filter'
40
- require 'active_interaction/filters/integer_filter'
41
- require 'active_interaction/filters/object_filter'
42
- require 'active_interaction/filters/record_filter'
43
- require 'active_interaction/filters/string_filter'
44
- require 'active_interaction/filters/symbol_filter'
45
- require 'active_interaction/filters/time_filter'
46
-
47
- require 'active_interaction/base'
15
+ require_relative 'active_interaction/version'
16
+ require_relative 'active_interaction/exceptions'
17
+ require_relative 'active_interaction/errors'
18
+
19
+ require_relative 'active_interaction/concerns/active_modelable'
20
+ require_relative 'active_interaction/concerns/active_recordable'
21
+ require_relative 'active_interaction/concerns/hashable'
22
+ require_relative 'active_interaction/concerns/missable'
23
+ require_relative 'active_interaction/concerns/runnable'
24
+
25
+ require_relative 'active_interaction/grouped_input'
26
+ require_relative 'active_interaction/input'
27
+ require_relative 'active_interaction/array_input'
28
+ require_relative 'active_interaction/hash_input'
29
+ require_relative 'active_interaction/inputs'
30
+
31
+ require_relative 'active_interaction/modules/validation'
32
+
33
+ require_relative 'active_interaction/filter/column'
34
+ require_relative 'active_interaction/filter/error'
35
+ require_relative 'active_interaction/filter'
36
+ require_relative 'active_interaction/filters/interface_filter'
37
+ require_relative 'active_interaction/filters/abstract_date_time_filter'
38
+ require_relative 'active_interaction/filters/abstract_numeric_filter'
39
+ require_relative 'active_interaction/filters/array_filter'
40
+ require_relative 'active_interaction/filters/boolean_filter'
41
+ require_relative 'active_interaction/filters/date_filter'
42
+ require_relative 'active_interaction/filters/date_time_filter'
43
+ require_relative 'active_interaction/filters/decimal_filter'
44
+ require_relative 'active_interaction/filters/file_filter'
45
+ require_relative 'active_interaction/filters/float_filter'
46
+ require_relative 'active_interaction/filters/hash_filter'
47
+ require_relative 'active_interaction/filters/integer_filter'
48
+ require_relative 'active_interaction/filters/object_filter'
49
+ require_relative 'active_interaction/filters/record_filter'
50
+ require_relative 'active_interaction/filters/string_filter'
51
+ require_relative 'active_interaction/filters/symbol_filter'
52
+ require_relative 'active_interaction/filters/time_filter'
53
+
54
+ require_relative 'active_interaction/base'
48
55
 
49
56
  I18n.load_path.unshift(
50
57
  *Dir.glob(
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveInteraction::ArrayInput do
4
+ subject(:input) do
5
+ described_class.new(filter,
6
+ value: value,
7
+ error: error,
8
+ index_errors: index_errors,
9
+ children: children
10
+ )
11
+ end
12
+
13
+ let(:filter) do
14
+ ActiveInteraction::ArrayFilter.new(:a, &block)
15
+ end
16
+ let(:block) { proc { integer } }
17
+ let(:value) { nil }
18
+ let(:error) { nil }
19
+ let(:index_errors) { false }
20
+ let(:children) { [] }
21
+
22
+ describe '#errors' do
23
+ context 'with no errors' do
24
+ it 'returns an empty array' do
25
+ expect(input.errors).to be_empty
26
+ end
27
+ end
28
+
29
+ context 'with an error on the array' do
30
+ let(:error) { ActiveInteraction::Filter::Error.new(filter, :invalid_type) }
31
+
32
+ it 'returns one error in the array' do
33
+ expect(input.errors.size).to be 1
34
+
35
+ error = input.errors.first
36
+ expect(error.name).to be filter.name
37
+ expect(error.type).to be :invalid_type
38
+ end
39
+ end
40
+
41
+ context 'with children with errors' do
42
+ let(:child_filter) { ActiveInteraction::IntegerFilter.new(:'') }
43
+ let(:child_error) { ActiveInteraction::Filter::Error.new(child_filter, :invalid_type) }
44
+ let(:child1) { ActiveInteraction::Input.new(child_filter, value: 'a', error: child_error) }
45
+ let(:child2) { ActiveInteraction::Input.new(child_filter, value: 'b', error: child_error) }
46
+ let(:children) { [child1, child2] }
47
+
48
+ context 'with an error on the array' do
49
+ let(:error) { ActiveInteraction::Filter::Error.new(filter, :invalid_type) }
50
+
51
+ it 'returns one error in the array' do
52
+ expect(input.errors.size).to be 1
53
+
54
+ error = input.errors.first
55
+ expect(error.name).to be filter.name
56
+ expect(error.type).to be :invalid_type
57
+ end
58
+ end
59
+
60
+ context 'without :index_errors' do
61
+ it 'promotes the first child error and returns it in the array' do
62
+ expect(input.errors.size).to be 1
63
+
64
+ error = input.errors.first
65
+ expect(error.name).to be filter.name
66
+ expect(error.type).to be :invalid_type
67
+ end
68
+ end
69
+
70
+ context 'with :index_errors' do
71
+ let(:index_errors) { true }
72
+
73
+ it 'lists all child errors in the array' do
74
+ expect(input.errors.size).to be 2
75
+
76
+ input.errors.each_with_index do |error, i|
77
+ expect(error.name).to be :"#{filter.name}[#{i}]"
78
+ expect(error.type).to be :invalid_type
79
+ end
80
+ end
81
+
82
+ context 'with a nested array' do
83
+ let(:invalid_value) { Object.new }
84
+ let(:array_child) do
85
+ filter = ActiveInteraction::IntegerFilter.new(:'')
86
+ ActiveInteraction::Input.new(filter,
87
+ value: invalid_value,
88
+ error: ActiveInteraction::Filter::Error.new(filter, :invalid_type)
89
+ )
90
+ end
91
+ let(:child_filter) { ActiveInteraction::ArrayFilter.new(:'') { integer } }
92
+ let(:child) do
93
+ described_class.new(child_filter,
94
+ value: [invalid_value],
95
+ index_errors: index_errors,
96
+ children: [array_child]
97
+ )
98
+ end
99
+ let(:children) { [child] }
100
+
101
+ it 'returns the error' do
102
+ expect(input.errors.size).to be 1
103
+
104
+ error = input.errors.first
105
+ expect(error.name).to be :"#{filter.name}[0][0]"
106
+ expect(error.type).to be :invalid_type
107
+ end
108
+ end
109
+
110
+ context 'with a nested hash' do
111
+ let(:hash_child_a) do
112
+ filter = ActiveInteraction::IntegerFilter.new(:a)
113
+ ActiveInteraction::Input.new(filter,
114
+ value: nil,
115
+ error: ActiveInteraction::Filter::Error.new(filter, :missing)
116
+ )
117
+ end
118
+ let(:hash_child_b) do
119
+ filter = ActiveInteraction::IntegerFilter.new(:b)
120
+ ActiveInteraction::Input.new(filter,
121
+ value: nil,
122
+ error: ActiveInteraction::Filter::Error.new(filter, :missing)
123
+ )
124
+ end
125
+ let(:child_filter) { ActiveInteraction::HashFilter.new(:'') { integer :a, :b } }
126
+ let(:child) do
127
+ ActiveInteraction::HashInput.new(child_filter, value: {}, children: { a: hash_child_a, b: hash_child_b })
128
+ end
129
+ let(:children) { [child] }
130
+
131
+ it 'list all child errors' do
132
+ expect(input.errors.size).to be 2
133
+
134
+ error = input.errors.first
135
+ expect(error.name).to be :"#{filter.name}[0].a"
136
+ expect(error.type).to be :missing
137
+
138
+ error = input.errors.last
139
+ expect(error.name).to be :"#{filter.name}[0].b"
140
+ expect(error.type).to be :missing
141
+ end
142
+
143
+ context 'with an invalid hash' do
144
+ let(:child_filter) { ActiveInteraction::HashFilter.new(:'') }
145
+ let(:child) do
146
+ ActiveInteraction::HashInput.new(child_filter,
147
+ value: Object.new,
148
+ error: ActiveInteraction::Filter::Error.new(child_filter, :invalid_type),
149
+ children: {}
150
+ )
151
+ end
152
+ let(:children) { [child] }
153
+
154
+ it 'list all child errors' do
155
+ expect(input.errors.size).to be 1
156
+
157
+ error = input.errors.first
158
+ expect(error.name).to be :"#{filter.name}[0]"
159
+ expect(error.type).to be :invalid_type
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end