active_interaction 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 02f4ec454ba99aa3327ab6d539a8dd2f6e9b8622
4
- data.tar.gz: e20fe1fb15d09040a7c00379962f53ac41d26cf1
3
+ metadata.gz: 152f0300bf27243acb0d7f0068b44a8b7c66fa8c
4
+ data.tar.gz: d2741f24682a5ede49215e0ddb7191625f27e189
5
5
  SHA512:
6
- metadata.gz: ab410e55639bb75899387091d50213f0b53b4d58de8b4cc0c3cc36d9c0f2acdc247bfe88662d276ed635a3fc695631a291c6c1f6234a7a46c38b4615127b3c7f
7
- data.tar.gz: f48693acfcc32d503fdc03af08cad9b2ed1a4c7fb32b7dac467f30e325ab97f3647c7b79d865686d26a1cd93fb04ca9b449e52a8d247086a3bd1cb27ab85aad2
6
+ metadata.gz: b2976e0ff9e7b3ad09181672922fad54b821b46f8e4380afa1eb6579121f4b7cc6fc854cd59bdbf983c5ab0047dc56744ebb010585016511cd8ea193a13b733c
7
+ data.tar.gz: a2f08efa2b55ebb4ebbc86f1ea51cce4d5adc465fe6d5cc8d2c6667a4aebeac40e87041d8690fd94b36a6f753b541ee58e70662c1836a56269fe71c932413a8d
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # [Master][]
2
2
 
3
- # [1.0.0][]
3
+ # [1.0.1][] (2014-02-04)
4
+
5
+ - Short circuit `valid?` after successfully running an interaction.
6
+ - Fix a bug that prevented merging interpolated symbolic errors.
7
+ - Use `:invalid_type` instead of `:invalid` as I18n key for type errors.
8
+ - Fix a bug that skipped setting up accessors for imported filters.
9
+
10
+ # [1.0.0][] (2014-01-21)
4
11
 
5
12
  - **Replace `Filters` with a hash.** To iterate over `Filter` objects, use
6
13
  `Interaction.filters.values`.
@@ -118,7 +125,8 @@
118
125
 
119
126
  - Initial release.
120
127
 
121
- [master]: https://github.com/orgsync/active_interaction/compare/v1.0.0...master
128
+ [master]: https://github.com/orgsync/active_interaction/compare/v1.0.1...master
129
+ [1.0.0]: https://github.com/orgsync/active_interaction/compare/v1.0.0...v1.0.1
122
130
  [1.0.0]: https://github.com/orgsync/active_interaction/compare/v0.10.2...v1.0.0
123
131
  [0.10.2]: https://github.com/orgsync/active_interaction/compare/v0.10.1...v0.10.2
124
132
  [0.10.1]: https://github.com/orgsync/active_interaction/compare/v0.10.0...v0.10.1
data/README.md CHANGED
@@ -208,27 +208,26 @@ called it with `run!`). If something went wrong, execution will halt
208
208
  immediately and the errors will be moved onto the caller.
209
209
 
210
210
  ```ruby
211
- class DoSomeMath < ActiveInteraction::Base
212
- integer :x, :y
211
+ class AddThree < ActiveInteraction::Base
212
+ integer :x
213
213
  def execute
214
- sum = compose(Add, inputs)
215
- square = compose(Square, x: sum)
216
- compose(Add, x: square, y: square)
214
+ compose(Add, x: x, y: 3)
217
215
  end
218
216
  end
219
- DoSomeMath.run!(x: 3, y: 5)
220
- # 128 => ((3 + 5) ** 2) * 2
217
+ AddThree.run!(x: 5)
218
+ # => 8
221
219
  ```
222
220
 
221
+ To bring in filters from another interaction, use `import_filters`. Combined
222
+ with `inputs`, delegating to another interaction is a piece of cake.
223
+
223
224
  ```ruby
224
- class AddThree < ActiveInteraction::Base
225
- integer :y
225
+ class AddAndDouble < ActiveInteraction::Base
226
+ import_filters Add
226
227
  def execute
227
- compose(Add, x: 3, y: y)
228
+ compose(Add, inputs) * 2
228
229
  end
229
230
  end
230
- AddThree.run!(y: nil)
231
- # => ActiveInteraction::InvalidInteractionError: Y is required
232
231
  ```
233
232
 
234
233
  ## How do I translate an interaction?
@@ -258,7 +257,7 @@ hsilgne:
258
257
  errors:
259
258
  messages:
260
259
  invalid: dilavni si
261
- invalid_nested: '%{type} dilav a ton si'
260
+ invalid_type: '%{type} dilav a ton si'
262
261
  missing: deriuqer si
263
262
  ```
264
263
 
@@ -283,7 +282,7 @@ work done in [Mutations][17].
283
282
  [0]: https://github.com/orgsync/active_interaction
284
283
  [1]: https://badge.fury.io/rb/active_interaction.png
285
284
  [2]: https://badge.fury.io/rb/active_interaction "Gem Version"
286
- [3]: https://travis-ci.org/orgsync/active_interaction.png
285
+ [3]: https://travis-ci.org/orgsync/active_interaction.png?branch=master
287
286
  [4]: https://travis-ci.org/orgsync/active_interaction "Build Status"
288
287
  [5]: https://coveralls.io/repos/orgsync/active_interaction/badge.png
289
288
  [6]: https://coveralls.io/r/orgsync/active_interaction "Coverage Status"
@@ -42,5 +42,5 @@ I18n.backend.load_translations(
42
42
  #
43
43
  # @since 1.0.0
44
44
  #
45
- # @version 1.0.0
45
+ # @version 1.0.1
46
46
  module ActiveInteraction end
@@ -37,6 +37,27 @@ module ActiveInteraction
37
37
  include Hashable
38
38
  include Missable
39
39
 
40
+ # @!method run(inputs = {})
41
+ # @note If the interaction inputs are valid and there are no runtime
42
+ # errors and execution completed successfully, {#valid?} will always
43
+ # return true.
44
+ #
45
+ # Runs validations and if there are no errors it will call {#execute}.
46
+ #
47
+ # @param (see ActiveInteraction::Base#initialize)
48
+ #
49
+ # @return [Base]
50
+
51
+ # @!method run!(inputs = {})
52
+ # Like {.run} except that it returns the value of {#execute} or raises
53
+ # an exception if there were any validation errors.
54
+ #
55
+ # @param (see ActiveInteraction::Base.run)
56
+ #
57
+ # @return (see ActiveInteraction::Runnable::ClassMethods#run!)
58
+ #
59
+ # @raise (see ActiveInteraction::Runnable::ClassMethods#run!)
60
+
40
61
  # Get or set the description.
41
62
  #
42
63
  # @example
@@ -77,25 +98,6 @@ module ActiveInteraction
77
98
  end
78
99
  end
79
100
 
80
- # @!method run(inputs = {})
81
- # Runs validations and if there are no errors it will call {#execute}.
82
- #
83
- # @param (see ActiveInteraction::Base#initialize)
84
- #
85
- # @return [Base]
86
- loop
87
-
88
- # @!method run!(inputs = {})
89
- # Like {.run} except that it returns the value of {#execute} or raises
90
- # an exception if there were any validation errors.
91
- #
92
- # @param (see ActiveInteraction::Base.run)
93
- #
94
- # @return (see ActiveInteraction::Runnable::ClassMethods#run!)
95
- #
96
- # @raise (see ActiveInteraction::Runnable::ClassMethods#run!)
97
- loop
98
-
99
101
  private
100
102
 
101
103
  # @param klass [Class]
@@ -104,12 +106,7 @@ module ActiveInteraction
104
106
  def add_filter(klass, name, options, &block)
105
107
  fail InvalidFilterError, name.inspect if reserved?(name)
106
108
 
107
- filter = klass.new(name, options, &block)
108
- filters[name] = filter
109
- attr_accessor name
110
- define_method("#{name}?") { !public_send(name).nil? }
111
-
112
- filter.default if filter.default?
109
+ initialize_filter(klass.new(name, options, &block))
113
110
  end
114
111
 
115
112
  # Import filters from another interaction.
@@ -123,6 +120,8 @@ module ActiveInteraction
123
120
  #
124
121
  # @return (see .filters)
125
122
  #
123
+ # @raise [ArgumentError] If both `:only` and `:except` are given.
124
+ #
126
125
  # @!visibility public
127
126
  def import_filters(klass, options = {})
128
127
  if options.key?(:only) && options.key?(:except)
@@ -136,7 +135,7 @@ module ActiveInteraction
136
135
  other_filters.select! { |k, _| only.include?(k) } if only
137
136
  other_filters.reject! { |k, _| except.include?(k) } if except
138
137
 
139
- filters.merge!(other_filters)
138
+ other_filters.values.each { |filter| initialize_filter(filter) }
140
139
  end
141
140
 
142
141
  # @param klass [Class]
@@ -144,6 +143,16 @@ module ActiveInteraction
144
143
  klass.instance_variable_set(:@_interaction_filters, filters.dup)
145
144
  end
146
145
 
146
+ # @param filter [Filter]
147
+ def initialize_filter(filter)
148
+ filters[filter.name] = filter
149
+
150
+ attr_accessor filter.name
151
+ define_method("#{filter.name}?") { !public_send(filter.name).nil? }
152
+
153
+ filter.default if filter.default?
154
+ end
155
+
147
156
  # @param symbol [Symbol]
148
157
  #
149
158
  # @return [Boolean]
@@ -169,7 +178,6 @@ module ActiveInteraction
169
178
  # @param inputs (see ActiveInteraction::Base#initialize)
170
179
  #
171
180
  # @return (see ActiveInteraction::Base.run!)
172
- loop
173
181
 
174
182
  # @!method execute
175
183
  # @abstract
@@ -180,7 +188,6 @@ module ActiveInteraction
180
188
  # ActiveRecord is available.
181
189
  #
182
190
  # @raise (see ActiveInteraction::Runnable#execute)
183
- loop
184
191
 
185
192
  # Returns the inputs provided to {.run} or {.run!} after being cast based
186
193
  # on the filters in the class.
@@ -56,15 +56,21 @@ module ActiveInteraction
56
56
  if errors.empty?
57
57
  @_interaction_result = result
58
58
  @_interaction_runtime_errors = nil
59
+ @_interaction_valid = true
59
60
  else
60
61
  @_interaction_result = nil
61
62
  @_interaction_runtime_errors = errors.dup
63
+ @_interaction_valid = false
62
64
  end
63
65
  end
64
66
 
65
67
  # @return [Boolean]
66
68
  def valid?(*)
67
- super || (self.result = nil)
69
+ unless instance_variable_defined?(:@_interaction_valid)
70
+ @_interaction_valid = false
71
+ end
72
+
73
+ @_interaction_valid || super || (self.result = nil)
68
74
  end
69
75
 
70
76
  private
@@ -90,15 +90,14 @@ module ActiveInteraction
90
90
  def add_sym(attribute, symbol = :invalid, message = nil, options = {})
91
91
  add(attribute, message || symbol, options)
92
92
 
93
- symbolic[attribute] ||= []
94
- symbolic[attribute] << symbol
93
+ symbolic[attribute] += [symbol]
95
94
  end
96
95
 
97
96
  # @see ActiveModel::Errors#initialize
98
97
  #
99
98
  # @private
100
99
  def initialize(*)
101
- @symbolic = {}.with_indifferent_access
100
+ @symbolic = Hash.new([]).with_indifferent_access
102
101
 
103
102
  super
104
103
  end
@@ -128,7 +127,7 @@ module ActiveInteraction
128
127
  # @return [Errors]
129
128
  def merge!(other)
130
129
  other.symbolic.each do |attribute, symbols|
131
- symbols.each { |s| add_sym(attribute, s) }
130
+ symbols.each { |s| symbolic[attribute] += [s] }
132
131
  end
133
132
 
134
133
  other.messages.each do |attribute, messages|
@@ -21,6 +21,9 @@ module ActiveInteraction
21
21
 
22
22
  # @private
23
23
  class TimeFilter < AbstractDateTimeFilter
24
+ alias_method :_klass, :klass
25
+ private :_klass
26
+
24
27
  def cast(value)
25
28
  case value
26
29
  when Numeric
@@ -41,7 +44,7 @@ module ActiveInteraction
41
44
  end
42
45
 
43
46
  def klasses
44
- [Time, klass.at(0).class]
47
+ [_klass, klass.at(0).class]
45
48
  end
46
49
  end
47
50
  end
@@ -12,7 +12,7 @@ module ActiveInteraction
12
12
  begin
13
13
  filter.cast(inputs[name])
14
14
  rescue InvalidValueError
15
- errors << [name, :invalid, nil, type: type(filter)]
15
+ errors << [name, :invalid_type, nil, type: type(filter)]
16
16
  rescue MissingValueError
17
17
  errors << [name, :missing]
18
18
  end
@@ -5,5 +5,5 @@ module ActiveInteraction
5
5
  # The version number.
6
6
  #
7
7
  # @return [Gem::Version]
8
- ActiveInteraction::VERSION = Gem::Version.new('1.0.0')
8
+ VERSION = Gem::Version.new('1.0.1')
9
9
  end
@@ -389,60 +389,68 @@ describe ActiveInteraction::Base do
389
389
  end
390
390
 
391
391
  describe '.import_filters' do
392
- let(:described_class) do
393
- Class.new(TestInteraction) do
394
- import_filters AddInteraction
392
+ shared_context 'import_filters context' do
393
+ let(:klass) { AddInteraction }
394
+ let(:only) { nil }
395
+ let(:except) { nil }
396
+
397
+ let(:described_class) do
398
+ interaction = klass
399
+ options = {}
400
+ options[:only] = only unless only.nil?
401
+ options[:except] = except unless except.nil?
402
+
403
+ Class.new(TestInteraction) { import_filters interaction, options }
395
404
  end
396
405
  end
397
406
 
398
- it 'imports the filters' do
399
- expect(described_class.filters).to eq AddInteraction.filters
400
- end
407
+ shared_examples 'import_filters examples' do
408
+ include_context 'import_filters context'
401
409
 
402
- context 'with :only' do
403
- let(:described_class) do
404
- Class.new(TestInteraction) do
405
- import_filters AddInteraction, only: [:x]
406
- end
410
+ it 'imports the filters' do
411
+ expect(described_class.filters).to eq klass.filters
412
+ .select { |k, _| only.nil? ? true : only.include?(k) }
413
+ .reject { |k, _| except.nil? ? false : except.include?(k) }
407
414
  end
408
415
 
409
416
  it 'does not modify the source' do
410
- filters = AddInteraction.filters.dup
417
+ filters = klass.filters.dup
411
418
  described_class
412
- expect(AddInteraction.filters).to eq filters
419
+ expect(klass.filters).to eq filters
413
420
  end
414
421
 
415
- it 'imports the filters' do
416
- expect(described_class.filters).to eq AddInteraction.filters
417
- .select { |k, _| k == :x }
418
- end
419
- end
422
+ it 'responds to readers, writers, and predicates' do
423
+ instance = described_class.new
420
424
 
421
- context 'with :except' do
422
- let(:described_class) do
423
- Class.new(TestInteraction) do
424
- import_filters AddInteraction, except: [:x]
425
+ described_class.filters.keys.each do |name|
426
+ [name, "#{name}=", "#{name}?"].each do |method|
427
+ expect(instance).to respond_to method
428
+ end
425
429
  end
426
430
  end
431
+ end
427
432
 
428
- it 'does not modify the source' do
429
- filters = AddInteraction.filters.dup
430
- described_class
431
- expect(AddInteraction.filters).to eq filters
432
- end
433
+ context 'with neither :only nor :except' do
434
+ include_examples 'import_filters examples'
435
+ end
433
436
 
434
- it 'imports the filters' do
435
- expect(described_class.filters).to eq AddInteraction.filters
436
- .reject { |k, _| k == :x }
437
- end
437
+ context 'with :only' do
438
+ include_examples 'import_filters examples'
439
+
440
+ let(:only) { [:x] }
441
+ end
442
+
443
+ context 'with :except' do
444
+ include_examples 'import_filters examples'
445
+
446
+ let(:except) { [:x] }
438
447
  end
439
448
 
440
449
  context 'with :only & :except' do
441
- let(:described_class) do
442
- Class.new(TestInteraction) do
443
- import_filters AddInteraction, only: nil, except: nil
444
- end
445
- end
450
+ include_context 'import_filters context'
451
+
452
+ let(:only) { [] }
453
+ let(:except) { [] }
446
454
 
447
455
  it 'raises an error' do
448
456
  expect { described_class }.to raise_error ArgumentError
@@ -123,12 +123,6 @@ describe ActiveInteraction::Runnable do
123
123
  it 'returns false' do
124
124
  expect(instance).to_not be_valid
125
125
  end
126
-
127
- it 'sets the result to nil' do
128
- instance.result = result
129
- instance.valid?
130
- expect(instance.result).to be_nil
131
- end
132
126
  end
133
127
  end
134
128
 
@@ -162,6 +156,29 @@ describe ActiveInteraction::Runnable do
162
156
  end
163
157
  end
164
158
  end
159
+
160
+ context 'with invalid post-execution state' do
161
+ before do
162
+ klass.class_exec do
163
+ attr_accessor :attribute
164
+
165
+ validate { errors.add(:attribute) if attribute }
166
+
167
+ def execute
168
+ self.attribute = true
169
+ end
170
+ end
171
+ end
172
+
173
+ it 'is valid' do
174
+ expect(outcome).to be_valid
175
+ end
176
+
177
+ it 'stays valid' do
178
+ outcome.attribute = true
179
+ expect(outcome).to be_valid
180
+ end
181
+ end
165
182
  end
166
183
 
167
184
  describe '.run!' do
@@ -10,7 +10,7 @@ describe ActiveInteraction::Errors do
10
10
  attr_reader :attribute
11
11
 
12
12
  def self.name
13
- SecureRandom.hex
13
+ @name ||= SecureRandom.hex
14
14
  end
15
15
  end
16
16
  end
@@ -129,5 +129,21 @@ describe ActiveInteraction::Errors do
129
129
  expect(errors.symbolic[:attribute]).to eq [:invalid]
130
130
  end
131
131
  end
132
+
133
+ context 'with an interpolated symbolic error' do
134
+ before do
135
+ I18n.backend.store_translations('en', activemodel: {
136
+ errors: { models: { klass.name => { attributes: { attribute: {
137
+ invalid_type: 'is not a valid %{type}'
138
+ } } } } }
139
+ })
140
+
141
+ other.add_sym(:attribute, :invalid_type, type: nil)
142
+ end
143
+
144
+ it 'does not raise an error' do
145
+ expect { errors.merge!(other) }.to_not raise_error
146
+ end
147
+ end
132
148
  end
133
149
  end
@@ -19,6 +19,26 @@ describe ActiveInteraction::ModelFilter, :filter do
19
19
  it 'returns the instance' do
20
20
  expect(filter.cast(value)).to eq value
21
21
  end
22
+
23
+ it 'handles reconstantizing' do
24
+ expect(filter.cast(value)).to eq value
25
+
26
+ Object.send(:remove_const, :Model)
27
+ Model = Class.new
28
+ value = Model.new
29
+
30
+ expect(filter.cast(value)).to eq value
31
+ end
32
+ end
33
+
34
+ context 'with class as a superclass' do
35
+ before do
36
+ options.merge!(class: Model.superclass)
37
+ end
38
+
39
+ it 'returns the instance' do
40
+ expect(filter.cast(value)).to eq value
41
+ end
22
42
  end
23
43
 
24
44
  context 'with class as a String' do
@@ -39,30 +39,24 @@ describe I18nInteraction do
39
39
  let(:translation) { I18n.translate(key, type: type, raise: true) }
40
40
  let(:type) { I18n.translate("#{described_class.i18n_scope}.types.hash") }
41
41
 
42
- context ':invalid' do
43
- let(:key) { "#{described_class.i18n_scope}.errors.messages.invalid" }
42
+ shared_examples 'translations' do |key, value|
43
+ context key.inspect do
44
+ let(:key) { "#{described_class.i18n_scope}.errors.messages.#{key}" }
44
45
 
45
- it 'has a translation' do
46
- expect { translation }.to_not raise_error
47
- end
48
-
49
- it 'returns the translation' do
50
- inputs.merge!(a: Object.new)
51
- expect(outcome.errors[:a]).to eq [translation]
52
- end
53
- end
46
+ before { inputs[:a] = value }
54
47
 
55
- context ':missing' do
56
- let(:key) { "#{described_class.i18n_scope}.errors.messages.missing" }
48
+ it 'has a translation' do
49
+ expect { translation }.to_not raise_error
50
+ end
57
51
 
58
- it 'has a translation' do
59
- expect { translation }.to_not raise_error
60
- end
61
-
62
- it 'returns the translation' do
63
- expect(outcome.errors[:a]).to eq [translation]
52
+ it 'returns the translation' do
53
+ expect(outcome.errors[:a]).to include translation
54
+ end
64
55
  end
65
56
  end
57
+
58
+ include_examples 'translations', :invalid_type, Object.new
59
+ include_examples 'translations', :missing, nil
66
60
  end
67
61
  end
68
62
 
@@ -80,8 +74,8 @@ describe I18nInteraction do
80
74
  before do
81
75
  I18n.backend.store_translations('hsilgne', active_interaction: {
82
76
  errors: { messages: {
83
- invalid: "%{type} #{'invalid'.reverse}",
84
- invalid_nested: 'invalid_nested'.reverse,
77
+ invalid: 'is invalid'.reverse,
78
+ invalid_type: "%{type} #{'is not a valid'.reverse}",
85
79
  missing: 'missing'.reverse
86
80
  } },
87
81
  types: TYPES.each_with_object({}) { |e, a| a[e] = e.reverse }
@@ -71,6 +71,12 @@ describe TimeInteraction do
71
71
  it 'returns the correct value' do
72
72
  expect(result[:a]).to eq a
73
73
  end
74
+
75
+ it 'handles time zone changes' do
76
+ outcome
77
+ allow(Time).to receive(:zone).and_return(nil)
78
+ expect(described_class.run(inputs)).to be_invalid
79
+ end
74
80
  end
75
81
  end
76
82
  end
@@ -38,18 +38,18 @@ describe ActiveInteraction::Validation do
38
38
  let(:exception) { ActiveInteraction::InvalidValueError }
39
39
  let(:filter) { ActiveInteraction::FloatFilter.new(:name, {}) }
40
40
 
41
- it 'returns an :invalid_nested error' do
41
+ it 'returns an :invalid_type error' do
42
42
  type = I18n.translate(
43
43
  "#{ActiveInteraction::Base.i18n_scope}.types.#{filter.class.slug}")
44
44
 
45
- expect(result).to eq [[filter.name, :invalid, nil, type: type]]
45
+ expect(result).to eq [[filter.name, :invalid_type, nil, type: type]]
46
46
  end
47
47
  end
48
48
 
49
49
  context 'MissingValueError' do
50
50
  let(:exception) { ActiveInteraction::MissingValueError }
51
51
 
52
- it 'returns an :invalid_nested error' do
52
+ it 'returns an :msising error' do
53
53
  expect(result).to eq [[filter.name, :missing]]
54
54
  end
55
55
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_interaction
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Lasseigne
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-01-22 00:00:00.000000000 Z
12
+ date: 2014-02-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel