active_interaction 4.0.5 → 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 +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