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,11 +1,14 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe ActiveInteraction::Inputs do
4
- subject(:inputs) { described_class.new }
4
+ subject(:inputs) { described_class.new(args, base_class.new) }
5
+
6
+ let(:args) { {} }
7
+ let(:base_class) { ActiveInteraction::Base }
5
8
 
6
9
  describe '.reserved?(name)' do
7
10
  it 'returns true for anything starting with "_interaction_"' do
8
- expect(described_class.reserved?('_interaction_')).to be_truthy
11
+ expect(described_class).to be_reserved('_interaction_')
9
12
  end
10
13
 
11
14
  it 'returns true for existing instance methods' do
@@ -13,21 +16,20 @@ describe ActiveInteraction::Inputs do
13
16
  (ActiveInteraction::Base.instance_methods - Object.instance_methods) +
14
17
  (ActiveInteraction::Base.private_instance_methods - Object.private_instance_methods)
15
18
  ).each do |method|
16
- expect(described_class.reserved?(method)).to be_truthy
19
+ expect(described_class).to be_reserved(method)
17
20
  end
18
21
  end
19
22
 
20
23
  it 'returns false for anything else' do
21
- expect(described_class.reserved?(SecureRandom.hex)).to be_falsey
24
+ expect(described_class).to_not be_reserved(SecureRandom.hex)
22
25
  end
23
26
  end
24
27
 
25
- describe '.process(inputs)' do
26
- let(:inputs) { {} }
27
- let(:result) { described_class.process(inputs) }
28
+ describe '#normalized' do
29
+ let(:result) { inputs.normalized }
28
30
 
29
31
  context 'with invalid inputs' do
30
- let(:inputs) { nil }
32
+ let(:args) { nil }
31
33
 
32
34
  it 'raises an error' do
33
35
  expect { result }.to raise_error ArgumentError
@@ -35,7 +37,7 @@ describe ActiveInteraction::Inputs do
35
37
  end
36
38
 
37
39
  context 'with non-hash inputs' do
38
- let(:inputs) { [%i[k v]] }
40
+ let(:args) { [%i[k v]] }
39
41
 
40
42
  it 'raises an error' do
41
43
  expect { result }.to raise_error ArgumentError
@@ -43,15 +45,7 @@ describe ActiveInteraction::Inputs do
43
45
  end
44
46
 
45
47
  context 'with ActionController::Parameters inputs' do
46
- let(:inputs) { ActionController::Parameters.new }
47
-
48
- it 'does not raise an error' do
49
- expect { result }.to_not raise_error
50
- end
51
- end
52
-
53
- context 'with Inputs inputs' do
54
- let(:inputs) { ActiveInteraction::Inputs.new }
48
+ let(:args) { ::ActionController::Parameters.new }
55
49
 
56
50
  it 'does not raise an error' do
57
51
  expect { result }.to_not raise_error
@@ -59,17 +53,17 @@ describe ActiveInteraction::Inputs do
59
53
  end
60
54
 
61
55
  context 'with simple inputs' do
62
- before { inputs[:key] = :value }
56
+ before { args[:key] = :value }
63
57
 
64
58
  it 'sends them straight through' do
65
- expect(result).to eql inputs
59
+ expect(result).to eql args
66
60
  end
67
61
  end
68
62
 
69
63
  context 'with groupable inputs' do
70
64
  context 'without a matching simple input' do
71
65
  before do
72
- inputs.merge!(
66
+ args.merge!(
73
67
  'key(1i)' => :value1,
74
68
  'key(2i)' => :value2
75
69
  )
@@ -87,7 +81,7 @@ describe ActiveInteraction::Inputs do
87
81
 
88
82
  context 'with a matching simple input' do
89
83
  before do
90
- inputs.merge!(
84
+ args.merge!(
91
85
  'key(1i)' => :value1,
92
86
  key: :value2
93
87
  )
@@ -104,11 +98,161 @@ describe ActiveInteraction::Inputs do
104
98
  end
105
99
 
106
100
  context 'with a reserved name' do
107
- before { inputs[:_interaction_key] = :value }
101
+ before { args[:_interaction_key] = :value }
108
102
 
109
103
  it 'skips the input' do
110
104
  expect(result).to_not have_key(:_interaction_key)
111
105
  end
112
106
  end
113
107
  end
108
+
109
+ describe '#given?' do
110
+ let(:base_class) do
111
+ Class.new(ActiveInteraction::Base) do
112
+ float :x,
113
+ default: nil
114
+
115
+ def execute; end
116
+ end
117
+ end
118
+
119
+ it 'is false when the input is not given' do
120
+ expect(inputs.given?(:x)).to be false
121
+ end
122
+
123
+ it 'is true when the input is nil' do
124
+ args[:x] = nil
125
+ expect(inputs.given?(:x)).to be true
126
+ end
127
+
128
+ it 'is true when the input is given' do
129
+ args[:x] = rand
130
+ expect(inputs.given?(:x)).to be true
131
+ end
132
+
133
+ it 'symbolizes its argument' do
134
+ args[:x] = rand
135
+ expect(inputs.given?('x')).to be true
136
+ end
137
+
138
+ it 'only tracks inputs with filters' do
139
+ args[:y] = rand
140
+ expect(inputs.given?(:y)).to be false
141
+ end
142
+
143
+ context 'nested hash values' do
144
+ let(:base_class) do
145
+ Class.new(ActiveInteraction::Base) do
146
+ hash :x, default: {} do
147
+ boolean :y,
148
+ default: true
149
+ end
150
+
151
+ def execute; end
152
+ end
153
+ end
154
+
155
+ it 'is true when the nested inputs symbols are given' do
156
+ described_class.class_exec do
157
+ def execute
158
+ given?(:x, :y)
159
+ end
160
+ end
161
+
162
+ args[:x] = { y: false }
163
+ expect(inputs.given?(:x, :y)).to be true
164
+ end
165
+
166
+ it 'is true when the nested inputs strings are given' do
167
+ args['x'] = { 'y' => false }
168
+ expect(inputs.given?(:x, :y)).to be true
169
+ end
170
+
171
+ it 'is false when the nested input is not given' do
172
+ args[:x] = {}
173
+ expect(inputs.given?(:x, :y)).to be false
174
+ end
175
+
176
+ it 'is false when the first input is not given' do
177
+ expect(inputs.given?(:x, :y)).to be false
178
+ end
179
+
180
+ it 'is false when the first input is nil' do
181
+ args[:x] = nil
182
+ expect(inputs.given?(:x, :y)).to be false
183
+ end
184
+
185
+ it 'returns false if you go too far' do
186
+ args[:x] = { y: true }
187
+ expect(inputs.given?(:x, :y, :z)).to be false
188
+ end
189
+ end
190
+
191
+ context 'nested array values' do
192
+ let(:base_class) do
193
+ Class.new(ActiveInteraction::Base) do
194
+ array :x do
195
+ hash do
196
+ boolean :y, default: true
197
+ end
198
+ end
199
+
200
+ def execute; end
201
+ end
202
+ end
203
+
204
+ context 'has a positive index' do
205
+ it 'returns true if found' do
206
+ args[:x] = [{ y: true }]
207
+ expect(inputs.given?(:x, 0, :y)).to be true
208
+ end
209
+
210
+ it 'returns false if not found' do
211
+ args[:x] = []
212
+ expect(inputs.given?(:x, 0, :y)).to be false
213
+ end
214
+ end
215
+
216
+ context 'has a negative index' do
217
+ it 'returns true if found' do
218
+ args[:x] = [{ y: true }]
219
+ expect(inputs.given?(:x, -1, :y)).to be true
220
+ end
221
+
222
+ it 'returns false if not found' do
223
+ args[:x] = []
224
+ expect(inputs.given?(:x, -1, :y)).to be false
225
+ end
226
+ end
227
+
228
+ it 'returns false if you go too far' do
229
+ args[:x] = [{}]
230
+ expect(inputs.given?(:x, 10, :y)).to be false
231
+ end
232
+ end
233
+
234
+ context 'multi-part date values' do
235
+ let(:base_class) do
236
+ Class.new(ActiveInteraction::Base) do
237
+ date :thing,
238
+ default: nil
239
+
240
+ def execute; end
241
+ end
242
+ end
243
+
244
+ it 'returns true when the input is given' do
245
+ args.merge!(
246
+ 'thing(1i)' => '2020',
247
+ 'thing(2i)' => '12',
248
+ 'thing(3i)' => '31'
249
+ )
250
+ expect(inputs.given?(:thing)).to be true
251
+ end
252
+
253
+ it 'returns false if not found' do
254
+ expect(inputs.given?(:thing)).to be false
255
+ end
256
+ end
257
+ end
114
258
  end
@@ -1,10 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'active_record'
3
- if defined?(JRUBY_VERSION)
4
- require 'activerecord-jdbcsqlite3-adapter'
5
- else
6
- require 'sqlite3'
7
- end
3
+ require 'sqlite3'
8
4
 
9
5
  ActiveRecord::Base.establish_connection(
10
6
  adapter: 'sqlite3',
@@ -36,8 +32,8 @@ end
36
32
  describe ArrayInteraction do
37
33
  include_context 'interactions'
38
34
  it_behaves_like 'an interaction', :array, -> { [] }
39
- it_behaves_like 'an interaction', :array, -> { Element.where('1 = 1') }
40
- it_behaves_like 'an interaction', :array, -> { List.create!.elements }
35
+ it_behaves_like 'an interaction', :array, -> { Element.where('1 = 1') }, ->(result) { result.to_a }
36
+ it_behaves_like 'an interaction', :array, -> { List.create!.elements }, ->(result) { result.to_a }
41
37
 
42
38
  context 'with inputs[:a]' do
43
39
  let(:a) { [[]] }
@@ -22,11 +22,11 @@ describe ActiveInteraction::Validation do
22
22
  end
23
23
  end
24
24
 
25
- context 'filter.cast returns a value' do
25
+ context 'filter returns no errors' do
26
26
  let(:inputs) { { name: 1 } }
27
27
 
28
28
  before do
29
- allow(filter).to receive(:cast).and_return(1)
29
+ allow(filter).to receive(:process).and_return(ActiveInteraction::Input.new(filter, value: 1))
30
30
  end
31
31
 
32
32
  it 'returns no errors' do
@@ -34,14 +34,15 @@ describe ActiveInteraction::Validation do
34
34
  end
35
35
  end
36
36
 
37
- context 'filter throws' do
37
+ context 'filter returns with errors' do
38
38
  before do
39
- allow(filter).to receive(:cast).and_raise(exception)
39
+ allow(filter).to receive(:process).and_return(ActiveInteraction::Input.new(filter, error: exception))
40
40
  end
41
41
 
42
- context 'InvalidValueError' do
43
- let(:exception) { ActiveInteraction::InvalidValueError }
44
- let(:filter) { ActiveInteraction::FloatFilter.new(:name, {}) }
42
+ context 'Filter::Error' do
43
+ let(:filter) { ActiveInteraction::ArrayFilter.new(:name, [1.0, 'a']) { float } }
44
+
45
+ let(:exception) { ActiveInteraction::Filter::Error.new(filter, :invalid_type) }
45
46
 
46
47
  it 'returns an :invalid_type error' do
47
48
  type = I18n.translate(
@@ -51,30 +52,6 @@ describe ActiveInteraction::Validation do
51
52
  expect(result).to eql [[filter.name, :invalid_type, { type: type }]]
52
53
  end
53
54
  end
54
-
55
- context 'MissingValueError' do
56
- let(:exception) { ActiveInteraction::MissingValueError }
57
-
58
- it 'returns a :missing error' do
59
- expect(result).to eql [[filter.name, :missing]]
60
- end
61
- end
62
-
63
- context 'InvalidNestedValueError' do
64
- let(:exception) do
65
- ActiveInteraction::InvalidNestedValueError.new(name, value)
66
- end
67
- let(:name) { SecureRandom.hex.to_sym }
68
- let(:value) { double }
69
-
70
- it 'returns an :invalid_nested error' do
71
- expect(result).to eql [[
72
- filter.name,
73
- :invalid_nested,
74
- { name: name.inspect, value: value.inspect }
75
- ]]
76
- end
77
- end
78
55
  end
79
56
  end
80
57
  end
data/spec/spec_helper.rb CHANGED
@@ -8,4 +8,12 @@ Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
8
8
  RSpec.configure do |config|
9
9
  config.run_all_when_everything_filtered = true
10
10
  config.filter_run_including :focus
11
+
12
+ config.before(:suite) do
13
+ if ::ActiveRecord.respond_to?(:index_nested_attribute_errors)
14
+ ::ActiveRecord.index_nested_attribute_errors = false
15
+ else
16
+ ::ActiveRecord::Base.index_nested_attribute_errors = false
17
+ end
18
+ end
11
19
  end
@@ -1,4 +1,6 @@
1
1
  shared_context 'concerns' do |concern|
2
+ subject(:instance) { klass.new }
3
+
2
4
  let(:klass) do
3
5
  Class.new do
4
6
  include concern
@@ -8,6 +10,4 @@ shared_context 'concerns' do |concern|
8
10
  end
9
11
  end
10
12
  end
11
-
12
- subject(:instance) { klass.new }
13
13
  end
@@ -1,10 +1,10 @@
1
1
  shared_context 'filters' do
2
+ subject(:filter) { described_class.new(name, options, &block) }
3
+
2
4
  let(:block) { nil }
3
5
  let(:name) { SecureRandom.hex.to_sym }
4
6
  let(:options) { {} }
5
7
 
6
- subject(:filter) { described_class.new(name, options, &block) }
7
-
8
8
  shared_context 'optional' do
9
9
  before do
10
10
  options[:default] = nil
@@ -45,71 +45,35 @@ shared_examples_for 'a filter' do
45
45
  end
46
46
  end
47
47
 
48
- describe '#cast' do
49
- let(:value) { nil }
50
- let(:result) { filter.send(:cast, value, nil) }
51
-
52
- context 'optional' do
53
- include_context 'optional'
54
-
55
- it 'returns nil' do
56
- expect(result).to be_nil
57
- end
58
- end
59
-
60
- context 'required' do
61
- include_context 'required'
62
-
63
- it 'raises an error' do
64
- expect { result }.to raise_error ActiveInteraction::MissingValueError
65
- end
66
-
67
- context 'with an invalid default' do
68
- let(:value) { Object.new }
69
-
70
- it 'raises an error' do
71
- expect { result }.to raise_error ActiveInteraction::InvalidValueError
72
- end
73
- end
74
- end
75
-
76
- # BasicObject is missing a lot of methods
77
- context 'with a BasicObject' do
78
- let(:value) { BasicObject.new }
79
-
80
- it 'raises an error' do
81
- expect { result }.to raise_error ActiveInteraction::InvalidValueError
82
- end
83
- end
84
- end
85
-
86
- describe '#clean' do
48
+ describe '#process' do
87
49
  let(:value) { nil }
88
50
 
89
51
  context 'optional' do
90
52
  include_context 'optional'
91
53
 
92
54
  it 'returns the default' do
93
- expect(filter.clean(value, nil)).to eql options[:default]
55
+ expect(filter.process(value, nil).value).to eql options[:default]
94
56
  end
95
57
  end
96
58
 
97
59
  context 'required' do
98
60
  include_context 'required'
99
61
 
100
- it 'raises an error' do
101
- expect do
102
- filter.clean(value, nil)
103
- end.to raise_error ActiveInteraction::MissingValueError
62
+ it 'indicates an error' do
63
+ error = filter.process(value, nil).errors.first
64
+
65
+ expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
66
+ expect(error.type).to be :missing
104
67
  end
105
68
 
106
69
  context 'with an invalid value' do
107
70
  let(:value) { Object.new }
108
71
 
109
- it 'raises an error' do
110
- expect do
111
- filter.clean(value, nil)
112
- end.to raise_error ActiveInteraction::InvalidValueError
72
+ it 'indicates an error' do
73
+ error = filter.process(value, nil).errors.first
74
+
75
+ expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
76
+ expect(error.type).to be :invalid_type
113
77
  end
114
78
  end
115
79
  end
@@ -121,10 +85,22 @@ shared_examples_for 'a filter' do
121
85
 
122
86
  it 'raises an error' do
123
87
  expect do
124
- filter.clean(value, nil)
88
+ filter.process(value, nil)
125
89
  end.to raise_error ActiveInteraction::InvalidDefaultError
126
90
  end
127
91
  end
92
+
93
+ # BasicObject is missing a lot of methods
94
+ context 'with a BasicObject' do
95
+ let(:value) { BasicObject.new }
96
+
97
+ it 'indicates an error' do
98
+ error = filter.process(value, nil).errors.first
99
+
100
+ expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
101
+ expect(error.type).to be :invalid_type
102
+ end
103
+ end
128
104
  end
129
105
 
130
106
  describe '#default' do
@@ -14,7 +14,7 @@ shared_context 'interactions' do
14
14
  let(:result) { outcome.result }
15
15
  end
16
16
 
17
- shared_examples_for 'an interaction' do |type, generator, filter_options = {}|
17
+ shared_examples_for 'an interaction' do |type, generator, adjust_output = nil, **filter_options|
18
18
  include_context 'interactions'
19
19
 
20
20
  let(:described_class) do
@@ -73,7 +73,7 @@ shared_examples_for 'an interaction' do |type, generator, filter_options = {}|
73
73
  end
74
74
 
75
75
  it 'returns the correct value for :required' do
76
- expect(result[:required]).to eql required
76
+ expect(result[:required]).to eql(adjust_output ? adjust_output.call(required) : required)
77
77
  end
78
78
 
79
79
  it 'returns nil for :optional' do
@@ -107,7 +107,7 @@ shared_examples_for 'an interaction' do |type, generator, filter_options = {}|
107
107
  before { inputs[:optional] = optional }
108
108
 
109
109
  it 'returns the correct value for :optional' do
110
- expect(result[:optional]).to eql optional
110
+ expect(result[:optional]).to eql(adjust_output ? adjust_output.call(optional) : optional)
111
111
  end
112
112
  end
113
113
 
@@ -117,7 +117,7 @@ shared_examples_for 'an interaction' do |type, generator, filter_options = {}|
117
117
  before { inputs[:default] = default }
118
118
 
119
119
  it 'returns the correct value for :default' do
120
- expect(result[:default]).to eql default
120
+ expect(result[:default]).to eql(adjust_output ? adjust_output.call(default) : default)
121
121
  end
122
122
  end
123
123
  end