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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/README.md +3 -2
- data/lib/active_interaction.rb +7 -1
- data/lib/active_interaction/base.rb +61 -13
- data/lib/active_interaction/concerns/runnable.rb +2 -18
- data/lib/active_interaction/concerns/transactable.rb +72 -0
- data/lib/active_interaction/errors.rb +7 -0
- data/lib/active_interaction/filter.rb +23 -2
- data/lib/active_interaction/filter_column.rb +59 -0
- data/lib/active_interaction/filters/abstract_date_time_filter.rb +14 -0
- data/lib/active_interaction/filters/abstract_filter.rb +4 -7
- data/lib/active_interaction/filters/abstract_numeric_filter.rb +4 -0
- data/lib/active_interaction/filters/boolean_filter.rb +4 -0
- data/lib/active_interaction/filters/date_time_filter.rb +3 -0
- data/lib/active_interaction/filters/decimal_filter.rb +54 -0
- data/lib/active_interaction/filters/file_filter.rb +4 -0
- data/lib/active_interaction/filters/time_filter.rb +4 -0
- data/lib/active_interaction/grouped_input.rb +24 -0
- data/lib/active_interaction/locale/en.yml +1 -0
- data/lib/active_interaction/modules/input_processor.rb +40 -0
- data/lib/active_interaction/version.rb +1 -1
- data/spec/active_interaction/base_spec.rb +90 -29
- data/spec/active_interaction/concerns/runnable_spec.rb +0 -26
- data/spec/active_interaction/concerns/transactable_spec.rb +114 -0
- data/spec/active_interaction/filter_column_spec.rb +96 -0
- data/spec/active_interaction/filter_spec.rb +15 -11
- data/spec/active_interaction/filters/array_filter_spec.rb +13 -5
- data/spec/active_interaction/filters/boolean_filter_spec.rb +6 -0
- data/spec/active_interaction/filters/date_filter_spec.rb +76 -5
- data/spec/active_interaction/filters/date_time_filter_spec.rb +87 -5
- data/spec/active_interaction/filters/decimal_filter_spec.rb +70 -0
- data/spec/active_interaction/filters/file_filter_spec.rb +11 -3
- data/spec/active_interaction/filters/float_filter_spec.rb +12 -4
- data/spec/active_interaction/filters/hash_filter_spec.rb +16 -8
- data/spec/active_interaction/filters/integer_filter_spec.rb +12 -4
- data/spec/active_interaction/filters/model_filter_spec.rb +12 -5
- data/spec/active_interaction/filters/string_filter_spec.rb +11 -3
- data/spec/active_interaction/filters/symbol_filter_spec.rb +10 -2
- data/spec/active_interaction/filters/time_filter_spec.rb +87 -5
- data/spec/active_interaction/grouped_input_spec.rb +19 -0
- data/spec/active_interaction/modules/input_processor_spec.rb +75 -0
- data/spec/support/filters.rb +6 -0
- metadata +16 -1
@@ -8,14 +8,11 @@ module ActiveInteraction
|
|
8
8
|
#
|
9
9
|
# @private
|
10
10
|
class AbstractFilter < Filter
|
11
|
-
|
12
|
-
attr_reader :klass
|
13
|
-
private :klass
|
14
|
-
|
15
|
-
def initialize(*)
|
16
|
-
super
|
11
|
+
private
|
17
12
|
|
18
|
-
|
13
|
+
# @return [Class]
|
14
|
+
def klass
|
15
|
+
@klass ||= self.class.slug.to_s.camelize.constantize
|
19
16
|
end
|
20
17
|
end
|
21
18
|
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
|
@@ -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
|
@@ -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
|
@@ -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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
74
|
-
|
77
|
+
context 'passing' do
|
78
|
+
before { inputs.merge!(thing: SecureRandom.hex) }
|
75
79
|
|
76
|
-
|
77
|
-
|
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 '
|
82
|
-
|
83
|
-
|
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
|
-
|
96
|
+
context 'with a filter' do
|
89
97
|
let(:described_class) { InteractionWithFilter }
|
90
98
|
|
91
|
-
context '
|
92
|
-
|
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
|
-
|
95
|
-
|
111
|
+
context 'without a value' do
|
112
|
+
let(:thing) { nil }
|
96
113
|
|
97
|
-
|
98
|
-
|
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 '
|
103
|
-
|
120
|
+
context 'passing' do
|
121
|
+
before { inputs.merge!(thing: 1) }
|
104
122
|
|
105
|
-
it '
|
106
|
-
expect(interaction
|
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 '
|
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
|
249
|
-
|
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
|