active_interaction 1.1.7 → 1.2.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -1
  3. data/README.md +3 -2
  4. data/lib/active_interaction.rb +7 -1
  5. data/lib/active_interaction/base.rb +61 -13
  6. data/lib/active_interaction/concerns/runnable.rb +2 -18
  7. data/lib/active_interaction/concerns/transactable.rb +72 -0
  8. data/lib/active_interaction/errors.rb +7 -0
  9. data/lib/active_interaction/filter.rb +23 -2
  10. data/lib/active_interaction/filter_column.rb +59 -0
  11. data/lib/active_interaction/filters/abstract_date_time_filter.rb +14 -0
  12. data/lib/active_interaction/filters/abstract_filter.rb +4 -7
  13. data/lib/active_interaction/filters/abstract_numeric_filter.rb +4 -0
  14. data/lib/active_interaction/filters/boolean_filter.rb +4 -0
  15. data/lib/active_interaction/filters/date_time_filter.rb +3 -0
  16. data/lib/active_interaction/filters/decimal_filter.rb +54 -0
  17. data/lib/active_interaction/filters/file_filter.rb +4 -0
  18. data/lib/active_interaction/filters/time_filter.rb +4 -0
  19. data/lib/active_interaction/grouped_input.rb +24 -0
  20. data/lib/active_interaction/locale/en.yml +1 -0
  21. data/lib/active_interaction/modules/input_processor.rb +40 -0
  22. data/lib/active_interaction/version.rb +1 -1
  23. data/spec/active_interaction/base_spec.rb +90 -29
  24. data/spec/active_interaction/concerns/runnable_spec.rb +0 -26
  25. data/spec/active_interaction/concerns/transactable_spec.rb +114 -0
  26. data/spec/active_interaction/filter_column_spec.rb +96 -0
  27. data/spec/active_interaction/filter_spec.rb +15 -11
  28. data/spec/active_interaction/filters/array_filter_spec.rb +13 -5
  29. data/spec/active_interaction/filters/boolean_filter_spec.rb +6 -0
  30. data/spec/active_interaction/filters/date_filter_spec.rb +76 -5
  31. data/spec/active_interaction/filters/date_time_filter_spec.rb +87 -5
  32. data/spec/active_interaction/filters/decimal_filter_spec.rb +70 -0
  33. data/spec/active_interaction/filters/file_filter_spec.rb +11 -3
  34. data/spec/active_interaction/filters/float_filter_spec.rb +12 -4
  35. data/spec/active_interaction/filters/hash_filter_spec.rb +16 -8
  36. data/spec/active_interaction/filters/integer_filter_spec.rb +12 -4
  37. data/spec/active_interaction/filters/model_filter_spec.rb +12 -5
  38. data/spec/active_interaction/filters/string_filter_spec.rb +11 -3
  39. data/spec/active_interaction/filters/symbol_filter_spec.rb +10 -2
  40. data/spec/active_interaction/filters/time_filter_spec.rb +87 -5
  41. data/spec/active_interaction/grouped_input_spec.rb +19 -0
  42. data/spec/active_interaction/modules/input_processor_spec.rb +75 -0
  43. data/spec/support/filters.rb +6 -0
  44. metadata +16 -1
@@ -8,14 +8,11 @@ module ActiveInteraction
8
8
  #
9
9
  # @private
10
10
  class AbstractFilter < Filter
11
- # @return [Class]
12
- attr_reader :klass
13
- private :klass
14
-
15
- def initialize(*)
16
- super
11
+ private
17
12
 
18
- @klass = self.class.slug.to_s.camelize.constantize
13
+ # @return [Class]
14
+ def klass
15
+ @klass ||= self.class.slug.to_s.camelize.constantize
19
16
  end
20
17
  end
21
18
  end
@@ -21,6 +21,10 @@ module ActiveInteraction
21
21
  end
22
22
  end
23
23
 
24
+ def database_column_type
25
+ self.class.slug
26
+ end
27
+
24
28
  private
25
29
 
26
30
  def convert(value)
@@ -26,5 +26,9 @@ module ActiveInteraction
26
26
  super
27
27
  end
28
28
  end
29
+
30
+ def database_column_type
31
+ self.class.slug
32
+ end
29
33
  end
30
34
  end
@@ -19,5 +19,8 @@ module ActiveInteraction
19
19
 
20
20
  # @private
21
21
  class DateTimeFilter < AbstractDateTimeFilter
22
+ def database_column_type
23
+ :datetime
24
+ end
22
25
  end
23
26
  end
@@ -0,0 +1,54 @@
1
+ # coding: utf-8
2
+
3
+ module ActiveInteraction
4
+ class Base
5
+ # @!method self.decimal(*attributes, options = {})
6
+ # Creates accessors for the attributes and ensures that values passed to
7
+ # the attributes are BigDecimals. Numerics and String values are
8
+ # converted into BigDecimals.
9
+ #
10
+ # @!macro filter_method_params
11
+ #
12
+ # @example
13
+ # decimal :amount, digits: 4
14
+ #
15
+ # @since 1.2.0
16
+ end
17
+
18
+ # @private
19
+ class DecimalFilter < AbstractNumericFilter
20
+ def cast(value)
21
+ case value
22
+ when Numeric
23
+ BigDecimal.new(value, digits)
24
+ when String
25
+ decimal_from_string(value)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # @return [Integer]
34
+ def digits
35
+ options.fetch(:digits, 0)
36
+ end
37
+
38
+ # @param value [String] string that has to be converted
39
+ #
40
+ # @return [BigDecimal]
41
+ #
42
+ # @raise [InvalidValueError] if given value can not be converted
43
+ def decimal_from_string(value)
44
+ Float(value)
45
+ BigDecimal.new(value, digits)
46
+ rescue ArgumentError
47
+ raise InvalidValueError, "Given value: #{value.inspect}"
48
+ end
49
+
50
+ def klass
51
+ BigDecimal
52
+ end
53
+ end
54
+ end
@@ -27,6 +27,10 @@ module ActiveInteraction
27
27
  end
28
28
  end
29
29
 
30
+ def database_column_type
31
+ self.class.slug
32
+ end
33
+
30
34
  private
31
35
 
32
36
  # @param value [File, #tempfile]
@@ -33,6 +33,10 @@ module ActiveInteraction
33
33
  end
34
34
  end
35
35
 
36
+ def database_column_type
37
+ :datetime
38
+ end
39
+
36
40
  private
37
41
 
38
42
  def klass
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+
3
+ require 'ostruct'
4
+
5
+ module ActiveInteraction
6
+ # Holds a group of inputs together for passing from {Base} to {Filter}s.
7
+ #
8
+ # @since 1.2.0
9
+ #
10
+ # @private
11
+ class GroupedInput < OpenStruct
12
+ unless method_defined?(:[])
13
+ def [](name)
14
+ send(name)
15
+ end
16
+ end
17
+
18
+ unless method_defined?(:[]=)
19
+ def []=(name, value)
20
+ send("#{name}=", value)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -11,6 +11,7 @@ en:
11
11
  boolean: boolean
12
12
  date: date
13
13
  date_time: date time
14
+ decimal: decimal
14
15
  file: file
15
16
  float: float
16
17
  hash: hash
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+
3
+ module ActiveInteraction
4
+ # Groups inputs ending in "(*N*i)" into {GroupedInput}.
5
+ #
6
+ # @since 1.2.0
7
+ #
8
+ # @private
9
+ module InputProcessor
10
+ class << self
11
+ GROUPED_INPUT_PATTERN = /\A(.+)\((\d+)i\)\z/
12
+ private_constant :GROUPED_INPUT_PATTERN
13
+
14
+ def reserved?(name)
15
+ name.to_s.start_with?('_interaction_')
16
+ end
17
+
18
+ def process(inputs)
19
+ inputs.stringify_keys.sort.each_with_object({}) do |(k, v), h|
20
+ fail ReservedNameError, k.inspect if reserved?(k)
21
+
22
+ if (match = GROUPED_INPUT_PATTERN.match(k))
23
+ assign_to_group!(h, *match.captures, v)
24
+ else
25
+ h[k.to_sym] = v
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def assign_to_group!(inputs, key, index, value)
33
+ key = key.to_sym
34
+
35
+ inputs[key] = GroupedInput.new unless inputs[key].is_a?(GroupedInput)
36
+ inputs[key][index] = value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -5,5 +5,5 @@ module ActiveInteraction
5
5
  # The version number.
6
6
  #
7
7
  # @return [Gem::Version]
8
- VERSION = Gem::Version.new('1.1.7')
8
+ VERSION = Gem::Version.new('1.2.0')
9
9
  end
@@ -6,6 +6,10 @@ InteractionWithFilter = Class.new(TestInteraction) do
6
6
  float :thing
7
7
  end
8
8
 
9
+ InteractionWithDateFilter = Class.new(TestInteraction) do
10
+ date :thing
11
+ end
12
+
9
13
  AddInteraction = Class.new(TestInteraction) do
10
14
  float :x, :y
11
15
 
@@ -62,59 +66,92 @@ describe ActiveInteraction::Base do
62
66
  validates :thing, presence: true
63
67
  end
64
68
  end
65
- let(:thing) { SecureRandom.hex }
66
-
67
- before { inputs.merge!(thing: thing) }
68
69
 
69
- it 'sets the attribute' do
70
- expect(interaction.thing).to eql thing
71
- end
70
+ context 'validation' do
71
+ context 'failing' do
72
+ it 'returns an invalid outcome' do
73
+ expect(interaction).to be_invalid
74
+ end
75
+ end
72
76
 
73
- context 'failing validations' do
74
- before { inputs.merge!(thing: nil) }
77
+ context 'passing' do
78
+ before { inputs.merge!(thing: SecureRandom.hex) }
75
79
 
76
- it 'returns an invalid outcome' do
77
- expect(interaction).to be_invalid
80
+ it 'returns a valid outcome' do
81
+ expect(interaction).to be_valid
82
+ end
78
83
  end
79
84
  end
80
85
 
81
- context 'passing validations' do
82
- it 'returns a valid outcome' do
83
- expect(interaction).to be_valid
86
+ context 'with a single input' do
87
+ let(:thing) { SecureRandom.hex }
88
+ before { inputs.merge!(thing: thing) }
89
+
90
+ it 'sets the attribute' do
91
+ expect(interaction.thing).to eql thing
84
92
  end
85
93
  end
86
94
  end
87
95
 
88
- describe 'with a filter' do
96
+ context 'with a filter' do
89
97
  let(:described_class) { InteractionWithFilter }
90
98
 
91
- context 'failing validations' do
92
- before { inputs.merge!(thing: thing) }
99
+ context 'validation' do
100
+ context 'failing' do
101
+ before { inputs.merge!(thing: thing) }
102
+
103
+ context 'with an invalid value' do
104
+ let(:thing) { 'a' }
105
+
106
+ it 'sets the attribute to the filtered value' do
107
+ expect(interaction.thing).to equal thing
108
+ end
109
+ end
93
110
 
94
- context 'with an invalid value' do
95
- let(:thing) { 'a' }
111
+ context 'without a value' do
112
+ let(:thing) { nil }
96
113
 
97
- it 'sets the attribute to the filtered value' do
98
- expect(interaction.thing).to equal thing
114
+ it 'sets the attribute to the filtered value' do
115
+ expect(interaction.thing).to equal thing
116
+ end
99
117
  end
100
118
  end
101
119
 
102
- context 'without a value' do
103
- let(:thing) { nil }
120
+ context 'passing' do
121
+ before { inputs.merge!(thing: 1) }
104
122
 
105
- it 'sets the attribute to the filtered value' do
106
- expect(interaction.thing).to equal thing
123
+ it 'returns a valid outcome' do
124
+ expect(interaction).to be_valid
107
125
  end
108
126
  end
109
127
  end
110
128
 
111
- context 'passing validations' do
129
+ context 'with a single input' do
112
130
  before { inputs.merge!(thing: 1) }
113
131
 
114
132
  it 'sets the attribute to the filtered value' do
115
133
  expect(interaction.thing).to eql 1.0
116
134
  end
117
135
  end
136
+
137
+ context 'with multiple inputs' do
138
+ let(:described_class) { InteractionWithDateFilter }
139
+ let(:year) { 2012 }
140
+ let(:month) { 1 }
141
+ let(:day) { 2 }
142
+
143
+ before do
144
+ inputs.merge!(
145
+ 'thing(1i)' => year.to_s,
146
+ 'thing(2i)' => month.to_s,
147
+ 'thing(3i)' => day.to_s
148
+ )
149
+ end
150
+
151
+ it 'returns a Date' do
152
+ expect(interaction.thing).to eql Date.new(year, month, day)
153
+ end
154
+ end
118
155
  end
119
156
  end
120
157
 
@@ -245,11 +282,10 @@ describe ActiveInteraction::Base do
245
282
  expect(result[:thing]).to eql thing
246
283
  end
247
284
 
248
- it 'calls ActiveRecord::Base.transaction' do
249
- allow(ActiveRecord::Base).to receive(:transaction)
285
+ it 'calls #transaction' do
286
+ expect_any_instance_of(described_class).to receive(:transaction)
287
+ .once.with(no_args)
250
288
  outcome
251
- expect(ActiveRecord::Base).to have_received(:transaction)
252
- .with(no_args)
253
289
  end
254
290
  end
255
291
  end
@@ -275,6 +311,31 @@ describe ActiveInteraction::Base do
275
311
  end
276
312
  end
277
313
 
314
+ describe '#column_for_attribute(name)' do
315
+ let(:described_class) { InteractionWithFilter }
316
+ let(:column) { outcome.column_for_attribute(name) }
317
+
318
+ context 'name is not an input name' do
319
+ let(:name) { SecureRandom.hex }
320
+
321
+ it 'returns nil if the attribute cannot be found' do
322
+ expect(column).to be_nil
323
+ end
324
+ end
325
+
326
+ context 'name is an input name' do
327
+ let(:name) { InteractionWithFilter.filters.keys.first }
328
+
329
+ it 'returns a FilterColumn' do
330
+ expect(column).to be_a ActiveInteraction::FilterColumn
331
+ end
332
+
333
+ it 'returns a FilterColumn of type boolean' do
334
+ expect(column.type).to eql :float
335
+ end
336
+ end
337
+ end
338
+
278
339
  describe '#inputs' do
279
340
  let(:described_class) { InteractionWithFilter }
280
341
  let(:other_val) { SecureRandom.hex }
@@ -2,32 +2,6 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe ActiveRecord::Base do
6
- describe '.transaction' do
7
- it 'raises an error' do
8
- expect { described_class.transaction }.to raise_error LocalJumpError
9
- end
10
-
11
- it 'silently rescues ActiveRecord::Rollback' do
12
- expect do
13
- described_class.transaction do
14
- fail ActiveRecord::Rollback
15
- end
16
- end.to_not raise_error
17
- end
18
-
19
- context 'with a block' do
20
- it 'yields to the block' do
21
- expect { |b| described_class.transaction(&b) }.to yield_with_no_args
22
- end
23
-
24
- it 'accepts an argument' do
25
- expect { described_class.transaction(nil) {} }.to_not raise_error
26
- end
27
- end
28
- end
29
- end
30
-
31
5
  describe ActiveInteraction::Runnable do
32
6
  include_context 'concerns', ActiveInteraction::Runnable
33
7
 
@@ -0,0 +1,114 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ActiveRecord::Base do
6
+ describe '.transaction' do
7
+ it 'raises an error' do
8
+ expect { described_class.transaction }.to raise_error LocalJumpError
9
+ end
10
+
11
+ it 'silently rescues ActiveRecord::Rollback' do
12
+ expect do
13
+ described_class.transaction do
14
+ fail ActiveRecord::Rollback
15
+ end
16
+ end.to_not raise_error
17
+ end
18
+
19
+ context 'with a block' do
20
+ it 'yields to the block' do
21
+ expect { |b| described_class.transaction(&b) }.to yield_with_no_args
22
+ end
23
+
24
+ it 'accepts an argument' do
25
+ expect { described_class.transaction(nil) {} }.to_not raise_error
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ describe ActiveInteraction::Transactable do
32
+ include_context 'concerns', ActiveInteraction::Transactable
33
+
34
+ describe '.transaction' do
35
+ it 'returns nil' do
36
+ expect(klass.transaction(true)).to be_nil
37
+ end
38
+
39
+ it 'accepts a flag parameter' do
40
+ expect { klass.transaction(true) }.to_not raise_error
41
+ end
42
+
43
+ it 'also accepts an options parameter' do
44
+ expect { klass.transaction(true, {}) }.to_not raise_error
45
+ end
46
+ end
47
+
48
+ describe '.transaction?' do
49
+ it 'defaults to true' do
50
+ expect(klass.transaction?).to be_true
51
+ end
52
+
53
+ it 'returns the stored value' do
54
+ klass.transaction(false)
55
+ expect(klass.transaction?).to be_false
56
+ end
57
+ end
58
+
59
+ describe '.transaction_options' do
60
+ it 'defaults to an empty hash' do
61
+ expect(klass.transaction_options).to eql({})
62
+ end
63
+
64
+ it 'returns the stored value' do
65
+ h = { rand => rand }
66
+ klass.transaction(klass.transaction?, h)
67
+ expect(klass.transaction_options).to eql h
68
+ end
69
+ end
70
+
71
+ describe '#transaction' do
72
+ let(:block) { -> { value } }
73
+ let(:result) { instance.transaction(&block) }
74
+ let(:value) { double }
75
+
76
+ before do
77
+ allow(ActiveRecord::Base).to receive(:transaction).and_call_original
78
+ end
79
+
80
+ it 'returns nil' do
81
+ expect(instance.transaction).to be_nil
82
+ end
83
+
84
+ context 'with transactions disabled' do
85
+ before do
86
+ klass.transaction(false)
87
+ end
88
+
89
+ it 'returns the value of the block' do
90
+ expect(result).to eql value
91
+ end
92
+
93
+ it 'does not call ActiveRecord::Base.transaction' do
94
+ expect(ActiveRecord::Base).to_not have_received(:transaction)
95
+ end
96
+ end
97
+
98
+ context 'with transactions enabled' do
99
+ before do
100
+ klass.transaction(true)
101
+ end
102
+
103
+ it 'returns the value of the block' do
104
+ expect(result).to eql value
105
+ end
106
+
107
+ it 'calls ActiveRecord::Base.transaction' do
108
+ result
109
+ expect(ActiveRecord::Base).to have_received(:transaction)
110
+ .once.with(klass.transaction_options, &block)
111
+ end
112
+ end
113
+ end
114
+ end