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 +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +24 -1
- data/lib/active_interaction.rb +4 -0
- data/lib/active_interaction/active_model.rb +6 -5
- data/lib/active_interaction/base.rb +27 -51
- data/lib/active_interaction/core.rb +42 -0
- data/lib/active_interaction/errors.rb +8 -5
- data/lib/active_interaction/filter.rb +3 -14
- data/lib/active_interaction/filters/abstract_date_time_filter.rb +37 -0
- data/lib/active_interaction/filters/abstract_numeric_filter.rb +25 -0
- data/lib/active_interaction/filters/array_filter.rb +9 -3
- data/lib/active_interaction/filters/date_filter.rb +3 -26
- data/lib/active_interaction/filters/date_time_filter.rb +3 -26
- data/lib/active_interaction/filters/float_filter.rb +5 -14
- data/lib/active_interaction/filters/hash_filter.rb +2 -2
- data/lib/active_interaction/filters/integer_filter.rb +5 -14
- data/lib/active_interaction/filters/time_filter.rb +1 -21
- data/lib/active_interaction/pipeline.rb +91 -0
- data/lib/active_interaction/version.rb +1 -1
- data/spec/active_interaction/base_spec.rb +1 -19
- data/spec/active_interaction/core_spec.rb +97 -0
- data/spec/active_interaction/pipeline_spec.rb +117 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4988ddb8d0259f53b92c2d8d81226902c0030d86
|
4
|
+
data.tar.gz: 8e36ae7a40b895596de17a7b94a5e568354dd686
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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
|
data/lib/active_interaction.rb
CHANGED
@@ -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
|
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, '
|
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
|
6
|
-
|
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
|
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
|
-
|
48
|
-
|
49
|
-
|
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 <
|
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
|
46
|
-
|
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 <
|
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
|
46
|
-
|
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 <
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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, '
|
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 <
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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 <
|
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
|
@@ -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::
|
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.
|
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.
|
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
|