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 +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
|