active_interaction 1.1.7 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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