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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -5
  3. data/README.md +35 -32
  4. data/lib/active_interaction.rb +3 -4
  5. data/lib/active_interaction/backports.rb +47 -0
  6. data/lib/active_interaction/base.rb +8 -25
  7. data/lib/active_interaction/concerns/runnable.rb +4 -14
  8. data/lib/active_interaction/errors.rb +12 -82
  9. data/lib/active_interaction/filters/array_filter.rb +3 -9
  10. data/lib/active_interaction/filters/file_filter.rb +5 -24
  11. data/lib/active_interaction/filters/hash_filter.rb +6 -13
  12. data/lib/active_interaction/filters/interface_filter.rb +2 -2
  13. data/lib/active_interaction/filters/{model_filter.rb → object_filter.rb} +4 -5
  14. data/lib/active_interaction/locale/en.yml +0 -1
  15. data/lib/active_interaction/version.rb +1 -1
  16. data/spec/active_interaction/base_spec.rb +15 -14
  17. data/spec/active_interaction/concerns/runnable_spec.rb +2 -34
  18. data/spec/active_interaction/errors_spec.rb +5 -87
  19. data/spec/active_interaction/filters/array_filter_spec.rb +2 -2
  20. data/spec/active_interaction/filters/file_filter_spec.rb +4 -4
  21. data/spec/active_interaction/filters/hash_filter_spec.rb +1 -17
  22. data/spec/active_interaction/filters/{model_filter_spec.rb → object_filter_spec.rb} +17 -17
  23. data/spec/active_interaction/integration/array_interaction_spec.rb +10 -0
  24. data/spec/active_interaction/integration/hash_interaction_spec.rb +12 -2
  25. data/spec/active_interaction/integration/object_interaction_spec.rb +16 -0
  26. metadata +8 -11
  27. data/lib/active_interaction/concerns/transactable.rb +0 -81
  28. data/spec/active_interaction/concerns/transactable_spec.rb +0 -135
  29. 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 symbolic error messages to make introspection
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
- merge_messages!(other)
174
- merge_details!(other) if other.respond_to?(:details)
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, hashes|
190
- hashes.each do |hash|
191
- error = hash[:error]
192
- next if @symbolic[attribute].include?(error)
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
- %w[
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
- # the attributes are Files or Tempfiles. It will also extract a file
8
- # from any object with a `tempfile` method. This is useful when passing
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 < Filter
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
- # @param value [File, #tempfile]
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) strip unknown keys (Note: All
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
- # model :item
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 = stringify_the_symbol_keys(value)
31
+ value = value.with_indifferent_access
32
+ initial = strip? ? ActiveSupport::HashWithIndifferentAccess.new : value
36
33
 
37
- filters.each_with_object(strip? ? {} : value) do |(name, filter), h|
34
+ filters.each_with_object(initial) do |(name, filter), h|
38
35
  clean_value(h, name.to_s, filter, value)
39
- end.symbolize_keys
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 objects
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.model(*attributes, options = {})
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
- # model :account
14
+ # object :account
15
15
  # @example
16
- # model :account, class: User
16
+ # object :account, class: User
17
17
  end
18
18
 
19
19
  # @private
20
- class ModelFilter < Filter
21
- register :model
20
+ class ObjectFilter < Filter
22
21
  register :object
23
22
 
24
23
  def cast(value, reconstantize = true)
@@ -17,7 +17,6 @@ en:
17
17
  hash: hash
18
18
  integer: integer
19
19
  interface: interface
20
- model: model
21
20
  object: object
22
21
  string: string
23
22
  symbol: symbol
@@ -5,5 +5,5 @@ module ActiveInteraction
5
5
  # The version number.
6
6
  #
7
7
  # @return [Gem::Version]
8
- VERSION = Gem::Version.new('1.6.1')
8
+ VERSION = Gem::Version.new('2.0.0')
9
9
  end
@@ -19,7 +19,7 @@ AddInteraction = Class.new(TestInteraction) do
19
19
  end
20
20
 
21
21
  InterruptInteraction = Class.new(TestInteraction) do
22
- model :x, :y,
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, 'error')
252
- errors.add_sym(:thing, :error, 'error')
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 to nil' do
267
- expect(result).to be_nil
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 %w[error error]
272
+ expect(outcome.errors.messages[:thing]).to eql [
273
+ 'is invalid',
274
+ 'is invalid'
275
+ ]
272
276
  end
273
277
 
274
- it 'has symbolic errors' do
275
- expect(outcome.errors.symbolic[:thing]).to eql [:error]
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 'does not set the result' do
104
+ it 'sets the result' do
105
105
  instance.result = result
106
- expect(instance.result).to be_nil
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 symbolic error' do
40
+ context 'with a detailed error' do
123
41
  before do
124
- other.add_sym(:attribute)
42
+ other.add(:attribute)
125
43
  end
126
44
 
127
45
  it 'adds the error' do
128
46
  errors.merge!(other)
129
- expect(errors.symbolic[:attribute]).to eql [:invalid]
47
+ expect(errors.details[:attribute]).to eql [{ error: :invalid }]
130
48
  end
131
49
  end
132
50
 
133
- context 'with an interpolated symbolic error' do
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.add_sym(:attribute, :invalid_type, type: nil)
68
+ other.add(:attribute, :invalid_type, type: nil)
151
69
  end
152
70
 
153
71
  it 'does not raise an error' do