active_interaction 1.6.1 → 2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -5
- data/README.md +35 -32
- data/lib/active_interaction.rb +3 -4
- data/lib/active_interaction/backports.rb +47 -0
- data/lib/active_interaction/base.rb +8 -25
- data/lib/active_interaction/concerns/runnable.rb +4 -14
- data/lib/active_interaction/errors.rb +12 -82
- data/lib/active_interaction/filters/array_filter.rb +3 -9
- data/lib/active_interaction/filters/file_filter.rb +5 -24
- data/lib/active_interaction/filters/hash_filter.rb +6 -13
- data/lib/active_interaction/filters/interface_filter.rb +2 -2
- data/lib/active_interaction/filters/{model_filter.rb → object_filter.rb} +4 -5
- data/lib/active_interaction/locale/en.yml +0 -1
- data/lib/active_interaction/version.rb +1 -1
- data/spec/active_interaction/base_spec.rb +15 -14
- data/spec/active_interaction/concerns/runnable_spec.rb +2 -34
- data/spec/active_interaction/errors_spec.rb +5 -87
- data/spec/active_interaction/filters/array_filter_spec.rb +2 -2
- data/spec/active_interaction/filters/file_filter_spec.rb +4 -4
- data/spec/active_interaction/filters/hash_filter_spec.rb +1 -17
- data/spec/active_interaction/filters/{model_filter_spec.rb → object_filter_spec.rb} +17 -17
- data/spec/active_interaction/integration/array_interaction_spec.rb +10 -0
- data/spec/active_interaction/integration/hash_interaction_spec.rb +12 -2
- data/spec/active_interaction/integration/object_interaction_spec.rb +16 -0
- metadata +8 -11
- data/lib/active_interaction/concerns/transactable.rb +0 -81
- data/spec/active_interaction/concerns/transactable_spec.rb +0 -135
- data/spec/active_interaction/integration/model_interaction_spec.rb +0 -16
@@ -89,89 +89,20 @@ module ActiveInteraction
|
|
89
89
|
end
|
90
90
|
private_constant :Interrupt
|
91
91
|
|
92
|
-
# An extension that provides
|
93
|
-
# and testing easier.
|
92
|
+
# An extension that provides the ability to merge other errors into itself.
|
94
93
|
class Errors < ActiveModel::Errors
|
95
|
-
# Maps attributes to arrays of symbolic messages.
|
96
|
-
#
|
97
|
-
# @return [Hash{Symbol => Array<Symbol>}]
|
98
|
-
attr_reader :symbolic
|
99
|
-
ActiveInteraction.deprecate self, :symbolic, 'use `details` instead'
|
100
|
-
|
101
|
-
def details
|
102
|
-
h = Hash.new([]).with_indifferent_access
|
103
|
-
@symbolic.each { |k, vs| vs.each { |v| h[k] += [{ error: v }] } }
|
104
|
-
h
|
105
|
-
end
|
106
|
-
|
107
|
-
alias_method :add_without_details, :add
|
108
|
-
def add_with_details(attribute, message = :invalid, options = {})
|
109
|
-
message = message.call if message.respond_to?(:call)
|
110
|
-
@symbolic[attribute] += [message] if message.is_a?(Symbol)
|
111
|
-
add_without_details(attribute, message, options)
|
112
|
-
end
|
113
|
-
alias_method :add, :add_with_details
|
114
|
-
|
115
|
-
# Adds a symbolic error message to an attribute.
|
116
|
-
#
|
117
|
-
# @example
|
118
|
-
# errors.add_sym(:attribute)
|
119
|
-
# errors.details
|
120
|
-
# # => {:attribute=>[{:error=>:invalid}]}
|
121
|
-
# errors.messages
|
122
|
-
# # => {:attribute=>["is invalid"]}
|
123
|
-
#
|
124
|
-
# @param attribute [Symbol] The attribute to add an error to.
|
125
|
-
# @param symbol [Symbol, nil] The symbolic error to add.
|
126
|
-
# @param message [String, Symbol, Proc, nil] The message to add.
|
127
|
-
# @param options [Hash]
|
128
|
-
#
|
129
|
-
# @return (see #symbolic)
|
130
|
-
#
|
131
|
-
# @see ActiveModel::Errors#add
|
132
|
-
def add_sym(attribute, symbol = :invalid, message = nil, options = {})
|
133
|
-
add_without_details(attribute, message || symbol, options)
|
134
|
-
|
135
|
-
@symbolic[attribute] += [symbol]
|
136
|
-
end
|
137
|
-
ActiveInteraction.deprecate self, :add_sym, 'use `add` instead'
|
138
|
-
|
139
|
-
# @see ActiveModel::Errors#initialize
|
140
|
-
#
|
141
|
-
# @private
|
142
|
-
def initialize(*)
|
143
|
-
@symbolic = Hash.new([]).with_indifferent_access
|
144
|
-
|
145
|
-
super
|
146
|
-
end
|
147
|
-
|
148
|
-
# @see ActiveModel::Errors#initialize_dup
|
149
|
-
#
|
150
|
-
# @private
|
151
|
-
def initialize_dup(other)
|
152
|
-
@symbolic = Hash.new([]).with_indifferent_access
|
153
|
-
other.details.each { |k, vs| vs.each { |v| @symbolic[k] += [v[:error]] } }
|
154
|
-
|
155
|
-
super
|
156
|
-
end
|
157
|
-
|
158
|
-
# @see ActiveModel::Errors#clear
|
159
|
-
#
|
160
|
-
# @private
|
161
|
-
def clear
|
162
|
-
@symbolic.clear
|
163
|
-
|
164
|
-
super
|
165
|
-
end
|
166
|
-
|
167
94
|
# Merge other errors into this one.
|
168
95
|
#
|
169
96
|
# @param other [Errors]
|
170
97
|
#
|
171
98
|
# @return [Errors]
|
172
99
|
def merge!(other)
|
173
|
-
|
174
|
-
|
100
|
+
if other.respond_to?(:details)
|
101
|
+
merge_details!(other)
|
102
|
+
else
|
103
|
+
merge_messages!(other)
|
104
|
+
end
|
105
|
+
|
175
106
|
self
|
176
107
|
end
|
177
108
|
|
@@ -186,12 +117,11 @@ module ActiveInteraction
|
|
186
117
|
end
|
187
118
|
|
188
119
|
def merge_details!(other)
|
189
|
-
other.details.each do |attribute,
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
@symbolic[attribute] += [error]
|
120
|
+
other.details.each do |attribute, details|
|
121
|
+
details.each do |detail|
|
122
|
+
detail = detail.dup
|
123
|
+
error = detail.delete(:error)
|
124
|
+
add(attribute, error, detail) unless added?(attribute, error, detail)
|
195
125
|
end
|
196
126
|
end
|
197
127
|
end
|
@@ -64,16 +64,10 @@ module ActiveInteraction
|
|
64
64
|
# @return [Array<Class>]
|
65
65
|
def classes
|
66
66
|
result = [Array]
|
67
|
+
return result unless Object.const_defined?(:ActiveRecord)
|
68
|
+
return result unless ActiveRecord.const_defined?(:Relation)
|
67
69
|
|
68
|
-
|
69
|
-
ActiveRecord::Relation
|
70
|
-
ActiveRecord::Associations::CollectionProxy
|
71
|
-
].each do |name|
|
72
|
-
next unless (klass = name.safe_constantize)
|
73
|
-
result.push(klass)
|
74
|
-
end
|
75
|
-
|
76
|
-
result
|
70
|
+
result.push(ActiveRecord::Relation)
|
77
71
|
end
|
78
72
|
|
79
73
|
# @param filter [Filter]
|
@@ -4,9 +4,8 @@ module ActiveInteraction
|
|
4
4
|
class Base
|
5
5
|
# @!method self.file(*attributes, options = {})
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# in Rails params that include a file upload.
|
7
|
+
# the attributes respond to the `eof?` method. This is useful when passing
|
8
|
+
# in Rails params that include a file upload or another generic IO object.
|
10
9
|
#
|
11
10
|
# @!macro filter_method_params
|
12
11
|
#
|
@@ -15,35 +14,17 @@ module ActiveInteraction
|
|
15
14
|
end
|
16
15
|
|
17
16
|
# @private
|
18
|
-
class FileFilter <
|
17
|
+
class FileFilter < InterfaceFilter
|
19
18
|
register :file
|
20
19
|
|
21
|
-
def cast(value)
|
22
|
-
value = extract_file(value)
|
23
|
-
|
24
|
-
case value
|
25
|
-
when File, Tempfile
|
26
|
-
value
|
27
|
-
else
|
28
|
-
super
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
20
|
def database_column_type
|
33
21
|
self.class.slug
|
34
22
|
end
|
35
23
|
|
36
24
|
private
|
37
25
|
|
38
|
-
|
39
|
-
|
40
|
-
# @return [File]
|
41
|
-
def extract_file(value)
|
42
|
-
if value.respond_to?(:tempfile)
|
43
|
-
value.tempfile
|
44
|
-
else
|
45
|
-
value
|
46
|
-
end
|
26
|
+
def methods
|
27
|
+
[:eof?]
|
47
28
|
end
|
48
29
|
end
|
49
30
|
end
|
@@ -8,17 +8,13 @@ module ActiveInteraction
|
|
8
8
|
#
|
9
9
|
# @!macro filter_method_params
|
10
10
|
# @param block [Proc] filter methods to apply for select keys
|
11
|
-
# @option options [Boolean] :strip (true)
|
12
|
-
# keys are symbolized. Ruby does not GC symbols so this can cause
|
13
|
-
# memory bloat. Setting this option to `false` and passing in non-safe
|
14
|
-
# input (e.g. Rails `params`) opens your software to a denial of
|
15
|
-
# service attack.)
|
11
|
+
# @option options [Boolean] :strip (true) remove unknown keys
|
16
12
|
#
|
17
13
|
# @example
|
18
14
|
# hash :order
|
19
15
|
# @example
|
20
16
|
# hash :order do
|
21
|
-
#
|
17
|
+
# object :item
|
22
18
|
# integer :quantity, default: 1
|
23
19
|
# end
|
24
20
|
end
|
@@ -32,11 +28,12 @@ module ActiveInteraction
|
|
32
28
|
def cast(value)
|
33
29
|
case value
|
34
30
|
when Hash
|
35
|
-
value =
|
31
|
+
value = value.with_indifferent_access
|
32
|
+
initial = strip? ? ActiveSupport::HashWithIndifferentAccess.new : value
|
36
33
|
|
37
|
-
filters.each_with_object(
|
34
|
+
filters.each_with_object(initial) do |(name, filter), h|
|
38
35
|
clean_value(h, name.to_s, filter, value)
|
39
|
-
end
|
36
|
+
end
|
40
37
|
else
|
41
38
|
super
|
42
39
|
end
|
@@ -79,9 +76,5 @@ module ActiveInteraction
|
|
79
76
|
def strip?
|
80
77
|
options.fetch(:strip, true)
|
81
78
|
end
|
82
|
-
|
83
|
-
def stringify_the_symbol_keys(hash)
|
84
|
-
self.class.transform_keys(hash) { |k| k.is_a?(Symbol) ? k.to_s : k }
|
85
|
-
end
|
86
79
|
end
|
87
80
|
end
|
@@ -7,8 +7,8 @@ module ActiveInteraction
|
|
7
7
|
# the attributes implement an interface.
|
8
8
|
#
|
9
9
|
# @!macro filter_method_params
|
10
|
-
# @option options [Array<Symbol>] :methods ([]) the methods that
|
11
|
-
# conforming to this interface should respond to
|
10
|
+
# @option options [Array<String,Symbol>] :methods ([]) the methods that
|
11
|
+
# objects conforming to this interface should respond to
|
12
12
|
#
|
13
13
|
# @example
|
14
14
|
# interface :anything
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module ActiveInteraction
|
4
4
|
class Base
|
5
|
-
# @!method self.
|
5
|
+
# @!method self.object(*attributes, options = {})
|
6
6
|
# Creates accessors for the attributes and ensures that values passed to
|
7
7
|
# the attributes are the correct class.
|
8
8
|
#
|
@@ -11,14 +11,13 @@ module ActiveInteraction
|
|
11
11
|
# Class name used to ensure the value.
|
12
12
|
#
|
13
13
|
# @example
|
14
|
-
#
|
14
|
+
# object :account
|
15
15
|
# @example
|
16
|
-
#
|
16
|
+
# object :account, class: User
|
17
17
|
end
|
18
18
|
|
19
19
|
# @private
|
20
|
-
class
|
21
|
-
register :model
|
20
|
+
class ObjectFilter < Filter
|
22
21
|
register :object
|
23
22
|
|
24
23
|
def cast(value, reconstantize = true)
|
@@ -19,7 +19,7 @@ AddInteraction = Class.new(TestInteraction) do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
InterruptInteraction = Class.new(TestInteraction) do
|
22
|
-
|
22
|
+
object :x, :y,
|
23
23
|
class: Object,
|
24
24
|
default: nil
|
25
25
|
|
@@ -248,8 +248,9 @@ describe ActiveInteraction::Base do
|
|
248
248
|
before do
|
249
249
|
@execute = described_class.instance_method(:execute)
|
250
250
|
described_class.send(:define_method, :execute) do
|
251
|
-
errors.add(:thing, '
|
252
|
-
errors.
|
251
|
+
errors.add(:thing, 'is invalid')
|
252
|
+
errors.add(:thing, :invalid)
|
253
|
+
true
|
253
254
|
end
|
254
255
|
end
|
255
256
|
|
@@ -263,16 +264,22 @@ describe ActiveInteraction::Base do
|
|
263
264
|
expect(outcome).to be_invalid
|
264
265
|
end
|
265
266
|
|
266
|
-
it 'sets the result
|
267
|
-
expect(result).to
|
267
|
+
it 'sets the result' do
|
268
|
+
expect(result).to be true
|
268
269
|
end
|
269
270
|
|
270
271
|
it 'has errors' do
|
271
|
-
expect(outcome.errors.messages[:thing]).to eql
|
272
|
+
expect(outcome.errors.messages[:thing]).to eql [
|
273
|
+
'is invalid',
|
274
|
+
'is invalid'
|
275
|
+
]
|
272
276
|
end
|
273
277
|
|
274
|
-
it 'has
|
275
|
-
expect(outcome.errors.
|
278
|
+
it 'has detailed errors' do
|
279
|
+
expect(outcome.errors.details[:thing]).to eql [
|
280
|
+
{ error: 'is invalid' },
|
281
|
+
{ error: :invalid }
|
282
|
+
]
|
276
283
|
end
|
277
284
|
end
|
278
285
|
|
@@ -283,12 +290,6 @@ describe ActiveInteraction::Base do
|
|
283
290
|
it 'sets the result' do
|
284
291
|
expect(result[:thing]).to eql thing
|
285
292
|
end
|
286
|
-
|
287
|
-
it 'calls #transaction' do
|
288
|
-
expect_any_instance_of(described_class).to receive(:transaction)
|
289
|
-
.once.with(no_args)
|
290
|
-
outcome
|
291
|
-
end
|
292
293
|
end
|
293
294
|
end
|
294
295
|
|
@@ -101,9 +101,9 @@ describe ActiveInteraction::Runnable do
|
|
101
101
|
context 'with an error' do
|
102
102
|
include_context 'with an error'
|
103
103
|
|
104
|
-
it '
|
104
|
+
it 'sets the result' do
|
105
105
|
instance.result = result
|
106
|
-
expect(instance.result).to
|
106
|
+
expect(instance.result).to eql result
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
@@ -180,38 +180,6 @@ describe ActiveInteraction::Runnable do
|
|
180
180
|
end
|
181
181
|
end
|
182
182
|
|
183
|
-
context 'with an execute where composition fails' do
|
184
|
-
before do
|
185
|
-
interaction = Class.new(TestInteraction) do
|
186
|
-
validate { errors.add(:base) }
|
187
|
-
end
|
188
|
-
|
189
|
-
klass.send(:define_method, :execute) { compose(interaction) }
|
190
|
-
end
|
191
|
-
|
192
|
-
it 'rolls back the transaction' do
|
193
|
-
instance = klass.new
|
194
|
-
|
195
|
-
allow(instance).to receive(:raise)
|
196
|
-
instance.send(:run)
|
197
|
-
expect(instance).to have_received(:raise)
|
198
|
-
.with(ActiveRecord::Rollback)
|
199
|
-
end
|
200
|
-
|
201
|
-
context 'without a transaction' do
|
202
|
-
before { klass.transaction(false) }
|
203
|
-
|
204
|
-
it 'does not roll back' do
|
205
|
-
instance = klass.new
|
206
|
-
|
207
|
-
allow(instance).to receive(:raise)
|
208
|
-
instance.send(:run)
|
209
|
-
expect(instance).to_not have_received(:raise)
|
210
|
-
.with(ActiveRecord::Rollback)
|
211
|
-
end
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
183
|
context 'with invalid post-execution state' do
|
216
184
|
before do
|
217
185
|
klass.class_exec do
|
@@ -17,88 +17,6 @@ describe ActiveInteraction::Errors do
|
|
17
17
|
|
18
18
|
subject(:errors) { described_class.new(klass.new) }
|
19
19
|
|
20
|
-
describe '#add_sym' do
|
21
|
-
it 'defaults to :invalid' do
|
22
|
-
errors.add_sym(:attribute)
|
23
|
-
expect(errors.symbolic[:attribute]).to eql [:invalid]
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'adds a symbol' do
|
27
|
-
errors.add_sym(:attribute, :symbol)
|
28
|
-
expect(errors.symbolic[:attribute]).to eql [:symbol]
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'accepts a message' do
|
32
|
-
errors.add_sym(:attribute, :symbol, 'message')
|
33
|
-
expect(errors.symbolic[:attribute]).to eql [:symbol]
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'accepts a message and options' do
|
37
|
-
errors.add_sym(:attribute, :symbol, 'message', key: :value)
|
38
|
-
expect(errors.symbolic[:attribute]).to eql [:symbol]
|
39
|
-
end
|
40
|
-
|
41
|
-
context 'calling #add' do
|
42
|
-
before do
|
43
|
-
allow(errors).to receive(:add_without_details)
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'with the default' do
|
47
|
-
errors.add_sym(:attribute)
|
48
|
-
expect(errors).to have_received(:add_without_details).once
|
49
|
-
.with(:attribute, :invalid, {})
|
50
|
-
end
|
51
|
-
|
52
|
-
it 'with a symbol' do
|
53
|
-
errors.add_sym(:attribute, :symbol)
|
54
|
-
expect(errors).to have_received(:add_without_details).once
|
55
|
-
.with(:attribute, :symbol, {})
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'with a symbol and message' do
|
59
|
-
errors.add_sym(:attribute, :symbol, 'message')
|
60
|
-
expect(errors).to have_received(:add_without_details).once
|
61
|
-
.with(:attribute, 'message', {})
|
62
|
-
end
|
63
|
-
|
64
|
-
it 'with a symbol, message and options' do
|
65
|
-
errors.add_sym(:attribute, :symbol, 'message', key: :value)
|
66
|
-
expect(errors).to have_received(:add_without_details).once
|
67
|
-
.with(:attribute, 'message', key: :value)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
describe '#initialize' do
|
73
|
-
it 'sets symbolic to an empty hash' do
|
74
|
-
expect(errors.symbolic).to eql({})
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
describe '#initialize_dup' do
|
79
|
-
let(:errors_dup) { errors.dup }
|
80
|
-
|
81
|
-
before do
|
82
|
-
errors.add_sym(:attribute)
|
83
|
-
end
|
84
|
-
|
85
|
-
it 'dups symbolic' do
|
86
|
-
expect(errors_dup.symbolic).to eql errors.symbolic
|
87
|
-
expect(errors_dup.symbolic).to_not equal errors.symbolic
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
describe '#clear' do
|
92
|
-
before do
|
93
|
-
errors.add_sym(:attribute)
|
94
|
-
end
|
95
|
-
|
96
|
-
it 'clears symbolic' do
|
97
|
-
errors.clear
|
98
|
-
expect(errors.symbolic).to be_empty
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
20
|
describe '#merge!' do
|
103
21
|
let(:other) { described_class.new(klass.new) }
|
104
22
|
|
@@ -119,18 +37,18 @@ describe ActiveInteraction::Errors do
|
|
119
37
|
end
|
120
38
|
end
|
121
39
|
|
122
|
-
context 'with a
|
40
|
+
context 'with a detailed error' do
|
123
41
|
before do
|
124
|
-
other.
|
42
|
+
other.add(:attribute)
|
125
43
|
end
|
126
44
|
|
127
45
|
it 'adds the error' do
|
128
46
|
errors.merge!(other)
|
129
|
-
expect(errors.
|
47
|
+
expect(errors.details[:attribute]).to eql [{ error: :invalid }]
|
130
48
|
end
|
131
49
|
end
|
132
50
|
|
133
|
-
context 'with an interpolated
|
51
|
+
context 'with an interpolated detailed error' do
|
134
52
|
before do
|
135
53
|
I18n.backend.store_translations('en',
|
136
54
|
activemodel: {
|
@@ -147,7 +65,7 @@ describe ActiveInteraction::Errors do
|
|
147
65
|
}
|
148
66
|
})
|
149
67
|
|
150
|
-
other.
|
68
|
+
other.add(:attribute, :invalid_type, type: nil)
|
151
69
|
end
|
152
70
|
|
153
71
|
it 'does not raise an error' do
|