active_interaction 1.0.0 → 1.0.1

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