active_interaction 0.6.1 → 0.7.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.
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