active_interaction 1.6.1 → 2.0.0

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