active_interaction 0.6.1 → 0.7.0

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: ddc5d64fad3052b09b16259d6aae6760411b384b
4
- data.tar.gz: 1c3569cfc819b0153c2c875cf5674c29382fc956
3
+ metadata.gz: 4988ddb8d0259f53b92c2d8d81226902c0030d86
4
+ data.tar.gz: 8e36ae7a40b895596de17a7b94a5e568354dd686
5
5
  SHA512:
6
- metadata.gz: 399f01cd79b55412083c29364afa80e53660e28ccbfc9d01788a138e30152ee61f4e6307e7a8f496a1b058557555ece132bf496b9e7d623b580e9414e203b030
7
- data.tar.gz: c25734f3428395f3c063c7a6b1f867b65107dc806a7e5a391b38d8692fcd46a391cc8f3259aca4df2911bfb377fe45adb96393f7738eb2cfc7acde95cf401d86
6
+ metadata.gz: 7af4c2ad4e2ae219859035746672c653318d7fbf9de7fe50f857c2518852cf6b78d92cbaf2161702c2c489f065f60a3af3bd2e8b9fbe205791b805cdd5b39975
7
+ data.tar.gz: 6454349ce56378be4373257973c4ac710712299e963ebe165828eca0ef90d5b09f257c8ca6763ea5791f2217a95f3eac5253cd9ab7b5bc5fc8c31fc418275909
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # [Master][]
2
2
 
3
+ # [0.7.0][] (2013-11-14)
4
+
5
+ - Add ability to chain a series of interactions together with
6
+ `ActiveInteraction::Pipeline`.
7
+ - Refactor internals (abstract filters & core class).
8
+
3
9
  # [0.6.1][] (2013-11-14)
4
10
 
5
11
  - Re-release. Forgot to merge into master.
@@ -70,7 +76,8 @@
70
76
 
71
77
  - Initial release.
72
78
 
73
- [master]: https://github.com/orgsync/active_interaction/compare/v0.6.1...master
79
+ [master]: https://github.com/orgsync/active_interaction/compare/v0.7.0...master
80
+ [0.7.0]: https://github.com/orgsync/active_interaction/compare/v0.6.1...v0.7.0
74
81
  [0.6.1]: https://github.com/orgsync/active_interaction/compare/v0.6.0...v0.6.1
75
82
  [0.6.0]: https://github.com/orgsync/active_interaction/compare/v0.5.0...v0.6.0
76
83
  [0.5.0]: https://github.com/orgsync/active_interaction/compare/v0.4.0...v0.5.0
data/README.md CHANGED
@@ -25,7 +25,7 @@ This project uses [semantic versioning][].
25
25
  Add it to your Gemfile:
26
26
 
27
27
  ```ruby
28
- gem 'active_interaction', '~> 0.6.1'
28
+ gem 'active_interaction', '~> 0.7.0'
29
29
  ```
30
30
 
31
31
  And then execute:
@@ -200,6 +200,29 @@ end
200
200
 
201
201
  Check out the [documentation][] for a full list of methods.
202
202
 
203
+ ## How do I compose interactions?
204
+
205
+ You can run many interactions in series by setting up a pipeline. Simply list
206
+ the interactions you want to run with `pipe`. Transforming the output of an
207
+ interaction into the input of the next one is accomplished with lambdas.
208
+
209
+ ```ruby
210
+ pipeline = ActiveInteraction::Pipeline.new do
211
+ pipe Add
212
+ pipe Square, :x
213
+ pipe Add, -> result { { x: result, y: result } }
214
+ end
215
+ outcome = pipeline.run(x: 3, y: 5)
216
+ outcome.result
217
+ # => 128 # ((3 + 5) ** 2) * 2
218
+ ```
219
+
220
+ The whole pipeline executes in a single transaction. The pipeline returns the
221
+ outcome of the last successful interaction. An error in the pipeline will
222
+ short-circuit and stop execution immediately.
223
+
224
+ While pipelines are similar to interactions, the two are not substitutable.
225
+
203
226
  ## How do I translate an interaction?
204
227
 
205
228
  ActiveInteraction is i18n-aware out of the box! All you have to do
@@ -6,6 +6,8 @@ require 'active_interaction/active_model'
6
6
  require 'active_interaction/method_missing'
7
7
  require 'active_interaction/overload_hash'
8
8
  require 'active_interaction/filter'
9
+ require 'active_interaction/filters/abstract_date_time_filter'
10
+ require 'active_interaction/filters/abstract_numeric_filter'
9
11
  require 'active_interaction/filters/array_filter'
10
12
  require 'active_interaction/filters/boolean_filter'
11
13
  require 'active_interaction/filters/date_filter'
@@ -20,7 +22,9 @@ require 'active_interaction/filters/symbol_filter'
20
22
  require 'active_interaction/filters/time_filter'
21
23
  require 'active_interaction/filters'
22
24
  require 'active_interaction/validation'
25
+ require 'active_interaction/core'
23
26
  require 'active_interaction/base'
27
+ require 'active_interaction/pipeline'
24
28
 
25
29
  I18n.backend.load_translations(
26
30
  Dir.glob(File.join(%w(lib active_interaction locale *.yml)))
@@ -3,10 +3,15 @@ module ActiveInteraction
3
3
  module ActiveModel
4
4
  extend ::ActiveSupport::Concern
5
5
 
6
- extend ::ActiveModel::Naming
7
6
  include ::ActiveModel::Conversion
8
7
  include ::ActiveModel::Validations
9
8
 
9
+ extend ::ActiveModel::Naming
10
+
11
+ def i18n_scope
12
+ self.class.i18n_scope
13
+ end
14
+
10
15
  def new_record?
11
16
  true
12
17
  end
@@ -15,10 +20,6 @@ module ActiveInteraction
15
20
  false
16
21
  end
17
22
 
18
- def i18n_scope
19
- self.class.i18n_scope
20
- end
21
-
22
23
  # @private
23
24
  module ClassMethods
24
25
  def i18n_scope
@@ -1,10 +1,5 @@
1
1
  require 'active_support/core_ext/hash/indifferent_access'
2
2
 
3
- begin
4
- require 'active_record'
5
- rescue LoadError
6
- end
7
-
8
3
  module ActiveInteraction
9
4
  # @abstract Subclass and override {#execute} to implement a custom
10
5
  # ActiveInteraction class.
@@ -31,26 +26,12 @@ module ActiveInteraction
31
26
  # end
32
27
  class Base
33
28
  include ActiveModel
29
+
30
+ extend Core
34
31
  extend MethodMissing
35
32
  extend OverloadHash
36
33
 
37
- validate do
38
- Validation.validate(self.class.filters, inputs).each do |error|
39
- errors.add_sym(*error)
40
- end
41
- end
42
-
43
- validate do
44
- return unless instance_variable_defined?(:@_interaction_runtime_errors)
45
-
46
- @_interaction_runtime_errors.symbolic.each do |attribute, symbols|
47
- symbols.each { |symbol| errors.add_sym(attribute, symbol) }
48
- end
49
-
50
- @_interaction_runtime_errors.messages.each do |attribute, messages|
51
- messages.each { |message| errors.add(attribute, message) }
52
- end
53
- end
34
+ validate :input_errors, :runtime_errors
54
35
 
55
36
  # Returns the inputs provided to {.run} or {.run!} after being cast based
56
37
  # on the filters in the class.
@@ -120,18 +101,7 @@ module ActiveInteraction
120
101
 
121
102
  # @private
122
103
  def valid?(*args)
123
- super || @_interaction_result = nil
124
- end
125
-
126
- # @private
127
- def self.transaction
128
- return unless block_given?
129
-
130
- if defined?(ActiveRecord)
131
- ::ActiveRecord::Base.transaction { yield }
132
- else
133
- yield
134
- end
104
+ super(*args) || (@_interaction_result = nil)
135
105
  end
136
106
 
137
107
  # Get all the filters defined on this interaction.
@@ -164,26 +134,10 @@ module ActiveInteraction
164
134
  end
165
135
  end
166
136
 
167
- # Like {.run} except that it returns the value of {#execute} or raises an
168
- # exception if there were any validation errors.
169
- #
170
- # @param (see .run)
171
- #
172
- # @return The return value of {#execute}.
173
- #
174
- # @raise [InteractionInvalidError] if there are any errors on the model.
175
- def self.run!(*args)
176
- outcome = run(*args)
177
- if outcome.invalid?
178
- raise InteractionInvalidError, outcome.errors.full_messages.join(', ')
179
- end
180
- outcome.result
181
- end
182
-
183
137
  # @private
184
138
  def self.method_missing(*args, &block)
185
139
  super do |klass, names, options|
186
- raise InvalidFilterError, 'no name' if names.empty?
140
+ raise InvalidFilterError, 'missing attribute name' if names.empty?
187
141
 
188
142
  names.each do |attribute|
189
143
  if attribute.to_s.start_with?('_interaction_')
@@ -194,9 +148,31 @@ module ActiveInteraction
194
148
  filters.add(filter)
195
149
  attr_accessor filter.name
196
150
 
151
+ # This isn't required, but it makes invalid defaults raise errors on
152
+ # class definition instead of on execution.
197
153
  filter.default if filter.has_default?
198
154
  end
199
155
  end
200
156
  end
157
+
158
+ private
159
+
160
+ def input_errors
161
+ Validation.validate(self.class.filters, inputs).each do |error|
162
+ errors.add_sym(*error)
163
+ end
164
+ end
165
+
166
+ def runtime_errors
167
+ return unless instance_variable_defined?(:@_interaction_runtime_errors)
168
+
169
+ @_interaction_runtime_errors.symbolic.each do |attribute, symbols|
170
+ symbols.each { |symbol| errors.add_sym(attribute, symbol) }
171
+ end
172
+
173
+ @_interaction_runtime_errors.messages.each do |attribute, messages|
174
+ messages.each { |message| errors.add(attribute, message) }
175
+ end
176
+ end
201
177
  end
202
178
  end
@@ -0,0 +1,42 @@
1
+ begin
2
+ require 'active_record'
3
+ rescue LoadError
4
+ end
5
+
6
+ module ActiveInteraction
7
+ # Functionality common between {Base} and {Pipeline}.
8
+ #
9
+ # @see Base
10
+ # @see Pipeline
11
+ module Core
12
+ # Like {Base.run} except that it returns the value of {Base#execute} or
13
+ # raises an exception if there were any validation errors.
14
+ #
15
+ # @param (see Base.run)
16
+ #
17
+ # @return [Object] the return value of {Base#execute}
18
+ #
19
+ # @raise [InvalidInteractionError] if the outcome is invalid
20
+ def run!(*args)
21
+ outcome = run(*args)
22
+
23
+ if outcome.valid?
24
+ outcome.result
25
+ else
26
+ raise InvalidInteractionError, outcome.errors.full_messages.join(', ')
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def transaction(*args)
33
+ return unless block_given?
34
+
35
+ if defined?(ActiveRecord)
36
+ ::ActiveRecord::Base.transaction(*args) { yield }
37
+ else
38
+ yield
39
+ end
40
+ end
41
+ end
42
+ end
@@ -2,8 +2,8 @@ module ActiveInteraction
2
2
  # Top-level error class. All other errors subclass this.
3
3
  Error = Class.new(StandardError)
4
4
 
5
- # Raised if an interaction is invalid.
6
- InteractionInvalidError = Class.new(Error)
5
+ # Raised when trying to run an empty pipeline.
6
+ EmptyPipelineError = Class.new(Error)
7
7
 
8
8
  # Raised if a class name is invalid.
9
9
  InvalidClassError = Class.new(Error)
@@ -14,18 +14,21 @@ module ActiveInteraction
14
14
  # Raised if a filter has an invalid definition.
15
15
  InvalidFilterError = Class.new(Error)
16
16
 
17
+ # Raised if an interaction is invalid.
18
+ InvalidInteractionError = Class.new(Error)
19
+
17
20
  # Raised if a user-supplied value is invalid.
18
21
  InvalidValueError = Class.new(Error)
19
22
 
20
- # Raised if there is no default value.
21
- NoDefaultError = Class.new(Error)
22
-
23
23
  # Raised if a filter cannot be found.
24
24
  MissingFilterError = Class.new(Error)
25
25
 
26
26
  # Raised if no value is given.
27
27
  MissingValueError = Class.new(Error)
28
28
 
29
+ # Raised if there is no default value.
30
+ NoDefaultError = Class.new(Error)
31
+
29
32
  # A small extension to provide symbolic error messages to make introspecting
30
33
  # and testing easier.
31
34
  #
@@ -79,18 +79,12 @@ module ActiveInteraction
79
79
  match.captures.first.underscore.to_sym
80
80
  end
81
81
 
82
- # @param klass [Class]
83
- #
84
- # @return [nil]
85
- #
86
82
  # @private
87
83
  def inherited(klass)
88
84
  begin
89
85
  CLASSES[klass.slug] = klass
90
86
  rescue InvalidClassError
91
87
  end
92
-
93
- super
94
88
  end
95
89
  end
96
90
 
@@ -129,7 +123,9 @@ module ActiveInteraction
129
123
  #
130
124
  # @return [Object]
131
125
  #
132
- # @raise (see #cast)
126
+ # @raise [InvalidValueError] if the value is invalid
127
+ # @raise [MissingValueError] if the value is missing and the input is
128
+ # required
133
129
  # @raise (see #default)
134
130
  #
135
131
  # @see #default
@@ -185,13 +181,6 @@ module ActiveInteraction
185
181
  options.has_key?(:default)
186
182
  end
187
183
 
188
- # @param value [Object]
189
- #
190
- # @return [nil]
191
- #
192
- # @raise [InvalidValueError] if the value is invalid
193
- # @raise [MissingValueError] if the value is missing and the input is required
194
- #
195
184
  # @private
196
185
  def cast(value)
197
186
  case value
@@ -0,0 +1,37 @@
1
+ module ActiveInteraction
2
+ # @private
3
+ class AbstractDateTimeFilter < Filter
4
+ def cast(value)
5
+ case value
6
+ when klass
7
+ value
8
+ when String
9
+ begin
10
+ if has_format?
11
+ klass.strptime(value, format)
12
+ else
13
+ klass.parse(value)
14
+ end
15
+ rescue ArgumentError
16
+ super
17
+ end
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def format
26
+ options.fetch(:format)
27
+ end
28
+
29
+ def has_format?
30
+ options.has_key?(:format)
31
+ end
32
+
33
+ def klass
34
+ raise NotImplementedError
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveInteraction
2
+ # @private
3
+ class AbstractNumericFilter < Filter
4
+ def cast(value)
5
+ case value
6
+ when klass
7
+ value
8
+ when Numeric, String
9
+ begin
10
+ send(klass.name, value)
11
+ rescue ArgumentError
12
+ super
13
+ end
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def klass
22
+ raise NotImplementedError
23
+ end
24
+ end
25
+ end
@@ -44,9 +44,15 @@ module ActiveInteraction
44
44
  super do |klass, names, options|
45
45
  filter = klass.new(name, options, &block)
46
46
 
47
- raise InvalidFilterError, 'multiple nested filters' if filters.any?
48
- raise InvalidFilterError, 'nested name' unless names.empty?
49
- raise InvalidDefaultError, 'nested default' if filter.has_default?
47
+ if filters.any?
48
+ raise InvalidFilterError, 'multiple filters in array block'
49
+ end
50
+ unless names.empty?
51
+ raise InvalidFilterError, 'attribute names in array block'
52
+ end
53
+ if filter.has_default?
54
+ raise InvalidDefaultError, 'default values in array block'
55
+ end
50
56
 
51
57
  filters.add(filter)
52
58
  end
@@ -20,34 +20,11 @@ module ActiveInteraction
20
20
  end
21
21
 
22
22
  # @private
23
- class DateFilter < Filter
24
- def cast(value)
25
- case value
26
- when Date
27
- value
28
- when String
29
- begin
30
- if has_format?
31
- Date.strptime(value, format)
32
- else
33
- Date.parse(value)
34
- end
35
- rescue ArgumentError
36
- super
37
- end
38
- else
39
- super
40
- end
41
- end
42
-
23
+ class DateFilter < AbstractDateTimeFilter
43
24
  private
44
25
 
45
- def has_format?
46
- options.has_key?(:format)
47
- end
48
-
49
- def format
50
- options.fetch(:format)
26
+ def klass
27
+ Date
51
28
  end
52
29
  end
53
30
  end
@@ -20,34 +20,11 @@ module ActiveInteraction
20
20
  end
21
21
 
22
22
  # @private
23
- class DateTimeFilter < Filter
24
- def cast(value)
25
- case value
26
- when DateTime
27
- value
28
- when String
29
- begin
30
- if has_format?
31
- DateTime.strptime(value, format)
32
- else
33
- DateTime.parse(value)
34
- end
35
- rescue ArgumentError
36
- super
37
- end
38
- else
39
- super
40
- end
41
- end
42
-
23
+ class DateTimeFilter < AbstractDateTimeFilter
43
24
  private
44
25
 
45
- def has_format?
46
- options.has_key?(:format)
47
- end
48
-
49
- def format
50
- options.fetch(:format)
26
+ def klass
27
+ DateTime
51
28
  end
52
29
  end
53
30
  end
@@ -15,20 +15,11 @@ module ActiveInteraction
15
15
  end
16
16
 
17
17
  # @private
18
- class FloatFilter < Filter
19
- def cast(value)
20
- case value
21
- when Numeric
22
- value.to_f
23
- when String
24
- begin
25
- Float(value)
26
- rescue ArgumentError
27
- super
28
- end
29
- else
30
- super
31
- end
18
+ class FloatFilter < AbstractNumericFilter
19
+ private
20
+
21
+ def klass
22
+ Float
32
23
  end
33
24
  end
34
25
  end
@@ -50,8 +50,8 @@ module ActiveInteraction
50
50
  end
51
51
 
52
52
  def method_missing(*args, &block)
53
- super do |klass, names, options|
54
- raise InvalidFilterError, 'no name' if names.empty?
53
+ super(*args) do |klass, names, options|
54
+ raise InvalidFilterError, 'missing attribute name' if names.empty?
55
55
 
56
56
  names.each do |name|
57
57
  filters.add(klass.new(name, options, &block))
@@ -14,20 +14,11 @@ module ActiveInteraction
14
14
  end
15
15
 
16
16
  # @private
17
- class IntegerFilter < Filter
18
- def cast(value)
19
- case value
20
- when Numeric
21
- value.to_i
22
- when String
23
- begin
24
- Integer(value)
25
- rescue ArgumentError
26
- super
27
- end
28
- else
29
- super
30
- end
17
+ class IntegerFilter < AbstractNumericFilter
18
+ private
19
+
20
+ def klass
21
+ Integer
31
22
  end
32
23
  end
33
24
  end
@@ -21,23 +21,11 @@ module ActiveInteraction
21
21
  end
22
22
 
23
23
  # @private
24
- class TimeFilter < Filter
24
+ class TimeFilter < AbstractDateTimeFilter
25
25
  def cast(value)
26
26
  case value
27
- when klass
28
- value
29
27
  when Numeric
30
28
  time.at(value)
31
- when String
32
- begin
33
- if has_format?
34
- klass.strptime(value, format)
35
- else
36
- klass.parse(value)
37
- end
38
- rescue ArgumentError
39
- super
40
- end
41
29
  else
42
30
  super
43
31
  end
@@ -45,14 +33,6 @@ module ActiveInteraction
45
33
 
46
34
  private
47
35
 
48
- def format
49
- options.fetch(:format)
50
- end
51
-
52
- def has_format?
53
- options.has_key?(:format)
54
- end
55
-
56
36
  def klass
57
37
  time.at(0).class
58
38
  end
@@ -0,0 +1,91 @@
1
+ begin
2
+ require 'active_record'
3
+ rescue LoadError
4
+ end
5
+
6
+ module ActiveInteraction
7
+ # Compose interactions by piping them together.
8
+ #
9
+ # @since 0.7.0
10
+ class Pipeline
11
+ include Core
12
+
13
+ # Set up a pipeline with a series of interactions.
14
+ #
15
+ # @example
16
+ # ActiveInteraction::Pipeline.new do
17
+ # pipe InteractionOne
18
+ # pipe InteractionTwo
19
+ # end
20
+ def initialize(&block)
21
+ @steps = []
22
+ instance_eval(&block) if block_given?
23
+ end
24
+
25
+ # Add an interaction to the end of the pipeline.
26
+ #
27
+ # @example With a lambda
28
+ # pipe Interaction, -> result { { a: result, b: result } }
29
+ #
30
+ # @example With a symbol
31
+ # pipe Interaction, :thing
32
+ # # -> result { { thing: result } }
33
+ #
34
+ # @example With nil
35
+ # pipe Interaction
36
+ # # -> result { result }
37
+ #
38
+ # @param interaction [Base] the interaction to add
39
+ # @param function [Proc] a function to convert the output of an interaction
40
+ # into the input for the next one
41
+ # @param function [Symbol] a shortcut for creating a function that puts the
42
+ # output into a hash with this key
43
+ # @param function [nil] a shortcut for creating a function that passes the
44
+ # output straight through
45
+ #
46
+ # @return [Pipeline]
47
+ def pipe(interaction, function = nil)
48
+ @steps << [lambdafy(function), interaction]
49
+ self
50
+ end
51
+
52
+ # Run all the interactions in the pipeline. If any interaction fails, stop
53
+ # and return immediately without running any more interactions.
54
+ #
55
+ # @param (see Base.run)
56
+ #
57
+ # @return [Base] an instance of the last successful interaction in the
58
+ # pipeline
59
+ #
60
+ # @raise [EmptyPipelineError] if nothing is in the pipeline
61
+ def run(*args)
62
+ raise EmptyPipelineError if @steps.empty?
63
+ transaction do
64
+ function, interaction = @steps.first
65
+ outcome = interaction.run(function.call(*args))
66
+ @steps[1..-1].reduce(outcome) { |o, (f, i)| bind(o, f, i) }
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def bind(outcome, function, interaction)
73
+ if outcome.valid?
74
+ interaction.run(function.call(outcome.result))
75
+ else
76
+ outcome
77
+ end
78
+ end
79
+
80
+ def lambdafy(thing)
81
+ case thing
82
+ when NilClass
83
+ -> result { result }
84
+ when Symbol
85
+ -> result { { thing => result } }
86
+ else
87
+ thing
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveInteraction
2
- VERSION = Gem::Version.new('0.6.1')
2
+ VERSION = Gem::Version.new('0.7.0')
3
3
  end
@@ -234,24 +234,6 @@ describe ActiveInteraction::Base do
234
234
  expect(described_class).to have_received(:transaction).once.
235
235
  with(no_args)
236
236
  end
237
-
238
- context 'with ActiveRecord' do
239
- before do
240
- ActiveRecord = Class.new
241
- ActiveRecord::Base = double
242
- allow(ActiveRecord::Base).to receive(:transaction)
243
- end
244
-
245
- after do
246
- Object.send(:remove_const, :ActiveRecord)
247
- end
248
-
249
- it 'calls ActiveRecord::Base.transaction' do
250
- outcome
251
- expect(ActiveRecord::Base).to have_received(:transaction).once.
252
- with(no_args)
253
- end
254
- end
255
237
  end
256
238
  end
257
239
 
@@ -262,7 +244,7 @@ describe ActiveInteraction::Base do
262
244
  it 'raises an error' do
263
245
  expect {
264
246
  result
265
- }.to raise_error ActiveInteraction::InteractionInvalidError
247
+ }.to raise_error ActiveInteraction::InvalidInteractionError
266
248
  end
267
249
  end
268
250
 
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveInteraction::Core do
4
+ let(:model) do
5
+ Class.new do
6
+ include ActiveInteraction::Core
7
+ end
8
+ end
9
+
10
+ subject(:instance) { model.new }
11
+
12
+ describe '#run!' do
13
+ let(:errors) { double(full_messages: []) }
14
+ let(:outcome) { double(errors: errors, result: result) }
15
+ let(:result) { double }
16
+
17
+ before do
18
+ allow(instance).to receive(:run).and_return(outcome)
19
+ end
20
+
21
+ shared_examples '#run!' do
22
+ let(:options) { double }
23
+
24
+ it 'calls #run' do
25
+ expect(instance).to receive(:run).once.with(options)
26
+ instance.run!(options) rescue nil
27
+ end
28
+ end
29
+
30
+ context 'with invalid outcome' do
31
+ include_examples '#run!'
32
+
33
+ before do
34
+ allow(outcome).to receive(:valid?).and_return(false)
35
+ end
36
+
37
+ it 'raises an error' do
38
+ expect {
39
+ instance.run!
40
+ }.to raise_error ActiveInteraction::InvalidInteractionError
41
+ end
42
+ end
43
+
44
+ context 'with valid outcome' do
45
+ include_examples '#run!'
46
+
47
+ before do
48
+ allow(outcome).to receive(:valid?).and_return(true)
49
+ end
50
+
51
+ it 'returns the result' do
52
+ expect(instance.run!).to eq result
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '#transaction' do
58
+ context 'without ActiveRecord' do
59
+ it 'returns nil' do
60
+ expect(instance.send(:transaction)).to be_nil
61
+ end
62
+
63
+ it 'yields' do
64
+ expect { |b| instance.send(:transaction, &b) }.to yield_control
65
+ end
66
+ end
67
+
68
+ context 'with ActiveRecord' do
69
+ before do
70
+ ActiveRecord = Class.new
71
+ ActiveRecord::Base = double
72
+ allow(ActiveRecord::Base).to receive(:transaction)
73
+ end
74
+
75
+ after do
76
+ Object.send(:remove_const, :ActiveRecord)
77
+ end
78
+
79
+ it 'returns nil' do
80
+ expect(instance.send(:transaction)).to be_nil
81
+ end
82
+
83
+ it 'calls ActiveRecord::Base#transaction' do
84
+ block = Proc.new {}
85
+ expect(ActiveRecord::Base).to receive(:transaction).once.with(no_args)
86
+ instance.send(:transaction, &block)
87
+ end
88
+
89
+ it 'calls ActiveRecord::Base#transaction' do
90
+ args = [:a, :b, :c]
91
+ block = Proc.new {}
92
+ expect(ActiveRecord::Base).to receive(:transaction).once.with(*args)
93
+ instance.send(:transaction, *args, &block)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveInteraction::Pipeline do
4
+ let(:invalid_interaction) do
5
+ Class.new(TestInteraction) do
6
+ float :a
7
+ validates :a, inclusion: { in: [] }
8
+ def execute; a end
9
+ end
10
+ end
11
+
12
+ let(:square_interaction) do
13
+ Class.new(TestInteraction) do
14
+ float :a
15
+ def execute; a ** 2 end
16
+ end
17
+ end
18
+
19
+ let(:swap_interaction) do
20
+ Class.new(TestInteraction) do
21
+ float :a, :b
22
+ def execute; { a: b, b: a } end
23
+ end
24
+ end
25
+
26
+ it 'raises an error with no pipes' do
27
+ pipeline = described_class.new
28
+ expect {
29
+ pipeline.run
30
+ }.to raise_error ActiveInteraction::EmptyPipelineError
31
+ end
32
+
33
+ it 'returns an invalid outcome with one invalid pipe' do
34
+ interaction = invalid_interaction
35
+ pipeline = described_class.new do
36
+ pipe interaction
37
+ end
38
+
39
+ options = { a: rand }
40
+ expect(pipeline.run(options)).to be_invalid
41
+ end
42
+
43
+ it 'succeeds with one pipe' do
44
+ interaction = swap_interaction
45
+ pipeline = described_class.new do
46
+ pipe interaction
47
+ end
48
+
49
+ options = { a: rand, b: rand }
50
+ expect(pipeline.run(options).result).to eq(a: options[:b], b: options[:a])
51
+ end
52
+
53
+ it 'succeeds with two pipes' do
54
+ interaction = swap_interaction
55
+ pipeline = described_class.new do
56
+ pipe interaction
57
+ pipe interaction
58
+ end
59
+
60
+ options = { a: rand, b: rand }
61
+ expect(pipeline.run(options).result).to eq options
62
+ end
63
+
64
+ it 'succeeds with an implicit transformation' do
65
+ interaction = square_interaction
66
+ pipeline = described_class.new do
67
+ pipe interaction
68
+ end
69
+
70
+ options = { a: rand }
71
+ expect(pipeline.run(options).result).to eq options[:a] ** 2
72
+ end
73
+
74
+ it 'succeeds with a symbolic transformation' do
75
+ interaction = square_interaction
76
+ pipeline = described_class.new do
77
+ pipe interaction, :a
78
+ end
79
+
80
+ options = rand
81
+ expect(pipeline.run(options).result).to eq options ** 2
82
+ end
83
+
84
+ it 'succeeds with a lambda transformation' do
85
+ interaction = square_interaction
86
+ pipeline = described_class.new do
87
+ pipe interaction, -> result { { a: 2 * result } }
88
+ end
89
+
90
+ options = rand
91
+ expect(pipeline.run(options).result).to eq (2 * options) ** 2
92
+ end
93
+
94
+ describe '#run!' do
95
+ it 'raises an error with one invalid pipe' do
96
+ interaction = invalid_interaction
97
+ pipeline = described_class.new do
98
+ pipe interaction
99
+ end
100
+
101
+ options = { a: rand }
102
+ expect {
103
+ pipeline.run!(options)
104
+ }.to raise_error ActiveInteraction::InvalidInteractionError
105
+ end
106
+
107
+ it 'returns the outcome with one valid pipe' do
108
+ interaction = square_interaction
109
+ pipeline = described_class.new do
110
+ pipe interaction
111
+ end
112
+
113
+ options = { a: rand }
114
+ expect(pipeline.run!(options)).to eq options[:a] ** 2
115
+ end
116
+ end
117
+ 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: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Lasseigne
@@ -153,8 +153,11 @@ extra_rdoc_files: []
153
153
  files:
154
154
  - lib/active_interaction/active_model.rb
155
155
  - lib/active_interaction/base.rb
156
+ - lib/active_interaction/core.rb
156
157
  - lib/active_interaction/errors.rb
157
158
  - lib/active_interaction/filter.rb
159
+ - lib/active_interaction/filters/abstract_date_time_filter.rb
160
+ - lib/active_interaction/filters/abstract_numeric_filter.rb
158
161
  - lib/active_interaction/filters/array_filter.rb
159
162
  - lib/active_interaction/filters/boolean_filter.rb
160
163
  - lib/active_interaction/filters/date_filter.rb
@@ -170,11 +173,13 @@ files:
170
173
  - lib/active_interaction/filters.rb
171
174
  - lib/active_interaction/method_missing.rb
172
175
  - lib/active_interaction/overload_hash.rb
176
+ - lib/active_interaction/pipeline.rb
173
177
  - lib/active_interaction/validation.rb
174
178
  - lib/active_interaction/version.rb
175
179
  - lib/active_interaction.rb
176
180
  - spec/active_interaction/active_model_spec.rb
177
181
  - spec/active_interaction/base_spec.rb
182
+ - spec/active_interaction/core_spec.rb
178
183
  - spec/active_interaction/errors_spec.rb
179
184
  - spec/active_interaction/filter_spec.rb
180
185
  - spec/active_interaction/filters/array_filter_spec.rb
@@ -205,6 +210,7 @@ files:
205
210
  - spec/active_interaction/integration/time_interaction_spec.rb
206
211
  - spec/active_interaction/method_missing_spec.rb
207
212
  - spec/active_interaction/overload_hash_spec.rb
213
+ - spec/active_interaction/pipeline_spec.rb
208
214
  - spec/active_interaction/validation_spec.rb
209
215
  - spec/spec_helper.rb
210
216
  - spec/support/filters.rb
@@ -232,13 +238,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
232
238
  version: '0'
233
239
  requirements: []
234
240
  rubyforge_project:
235
- rubygems_version: 2.1.10
241
+ rubygems_version: 2.1.11
236
242
  signing_key:
237
243
  specification_version: 4
238
244
  summary: Manage application specific business logic.
239
245
  test_files:
240
246
  - spec/active_interaction/active_model_spec.rb
241
247
  - spec/active_interaction/base_spec.rb
248
+ - spec/active_interaction/core_spec.rb
242
249
  - spec/active_interaction/errors_spec.rb
243
250
  - spec/active_interaction/filter_spec.rb
244
251
  - spec/active_interaction/filters/array_filter_spec.rb
@@ -269,6 +276,7 @@ test_files:
269
276
  - spec/active_interaction/integration/time_interaction_spec.rb
270
277
  - spec/active_interaction/method_missing_spec.rb
271
278
  - spec/active_interaction/overload_hash_spec.rb
279
+ - spec/active_interaction/pipeline_spec.rb
272
280
  - spec/active_interaction/validation_spec.rb
273
281
  - spec/spec_helper.rb
274
282
  - spec/support/filters.rb