active_interaction 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2f6687ab2fb463ecc05f0e785e9c05cf0b5d1172
4
- data.tar.gz: d75174715db45471c960c5e1bb4effffb2ab9b7d
3
+ metadata.gz: 1139d1df0d150418f22b231b09423c894ba98c41
4
+ data.tar.gz: c53767591c098f28c46ffa8ae77fb31ac714e310
5
5
  SHA512:
6
- metadata.gz: 113199ae4863c2a7d78764179c9bd5caec7d516af78c921f1e788b5e9b1a2e251814b3e82786e790facc45d616f069e8a7b4e9e226a4d96b160e98e0c019e180
7
- data.tar.gz: 5fefc55875416c15613c3edf28ca86d65996404ddb297037980360bab4f27a87bba7bad61b0eb6b9d51eaebce3fb337cee7234574b31a45d3b2356ec78514ef9
6
+ metadata.gz: 5524134f37f76b6252d1ee82a2fe9d879aae1debec5d46679b66eb608e42bd49668c59532c5e2fda8f64999b5582649fb090c78e0c9c7048df2d05cd7e729474
7
+ data.tar.gz: 591d0b74feed33a7a7d39ebe67f61adea4c7f5721541d9c570ef39c8dd06b53f4cfd74ee8bac8dc8a78508c447d942a7e0cacbb80813392640ddce8d79f9b512
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # [Master][]
2
2
 
3
+ # [0.9.0][] (2013-12-02)
4
+
5
+ - Add experimental composition implementation
6
+ (`ActiveInteraction::Base#compose`).
7
+ - Remove `ActiveInteraction::Pipeline`.
8
+
3
9
  # [0.8.0][] (2013-11-14)
4
10
 
5
11
  - Add ability to document interactions and filters.
@@ -8,7 +14,6 @@
8
14
 
9
15
  - Add ability to chain a series of interactions together with
10
16
  `ActiveInteraction::Pipeline`.
11
- - Refactor internals (abstract filters & core class).
12
17
 
13
18
  # [0.6.1][] (2013-11-14)
14
19
 
@@ -16,18 +21,17 @@
16
21
 
17
22
  # [0.6.0][] (2013-11-14)
18
23
 
19
- - Error class now end with `Error`.
20
- - By default, strip unlisted keys from hashes. To retain the old behavior, set
21
- `strip: false` on a hash filter.
22
- - Prevent specifying defaults (other than `nil` or `{}`) on hash filters. Set
23
- defaults on the nested filters instead.
24
+ - **Error class now end with `Error`.**
25
+ - **By default, strip unlisted keys from hashes. To retain the old behavior,
26
+ set `strip: false` on a hash filter.**
27
+ - **Prevent specifying defaults (other than `nil` or `{}`) on hash filters. Set
28
+ defaults on the nested filters instead.**
24
29
  - Add ability to introspect interactions with `filters`.
25
30
  - Fix bug that prevented listing multiple attributes in a hash filter.
26
31
  - Allow getting all of the user-supplied inputs in an interaction with
27
32
  `inputs`.
28
33
  - Fix bug that prevented hash filters from being nested in array filters.
29
34
  - Replace `allow_nil: true` with `default: nil`.
30
- - Refactor internals.
31
35
  - Add a symbol filter.
32
36
  - Allow adding symbolic errors with `errors.add_sym` and retrieving them with
33
37
  `errors.symbolic`.
@@ -76,11 +80,12 @@
76
80
 
77
81
  - Correct gemspec dependencies on activemodel.
78
82
 
79
- # [0.1.0][] (2013-08-12)
83
+ # [0.1.0][] (2013-07-12)
80
84
 
81
85
  - Initial release.
82
86
 
83
- [master]: https://github.com/orgsync/active_interaction/compare/v0.8.0...master
87
+ [master]: https://github.com/orgsync/active_interaction/compare/v0.9.0...master
88
+ [0.9.0]: https://github.com/orgsync/active_interaction/compare/v0.9.0...0.9.0
84
89
  [0.8.0]: https://github.com/orgsync/active_interaction/compare/v0.7.0...v0.8.0
85
90
  [0.7.0]: https://github.com/orgsync/active_interaction/compare/v0.6.1...v0.7.0
86
91
  [0.6.1]: https://github.com/orgsync/active_interaction/compare/v0.6.0...v0.6.1
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.8.0'
28
+ gem 'active_interaction', '~> 0.9.0'
29
29
  ```
30
30
 
31
31
  And then execute:
@@ -117,7 +117,7 @@ Or, you can do this:
117
117
  ```ruby
118
118
  result = UserSignup.run!(params)
119
119
  # Either returns the result of execute,
120
- # or raises ActiveInteraction::InteractionInvalid
120
+ # or raises ActiveInteraction::InvalidInteractionError
121
121
  ```
122
122
 
123
123
  ## What can I pass to an interaction?
@@ -202,26 +202,36 @@ Check out the [documentation][] for a full list of methods.
202
202
 
203
203
  ## How do I compose interactions?
204
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.
205
+ (Note: this feature is experimental. See [#41][] & [#79][].)
206
+
207
+ You can run interactions from within other interactions by calling `compose`.
208
+ If the interaction is successful, it'll return the result (just like if you had
209
+ called it with `run!`). If something went wrong, execution will halt
210
+ immediately and the errors will be moved onto the caller.
208
211
 
209
212
  ```ruby
210
- pipeline = ActiveInteraction::Pipeline.new do
211
- pipe Add
212
- pipe Square, :x
213
- pipe Add, -> result { { x: result, y: result } }
213
+ class DoSomeMath < ActiveInteraction::Base
214
+ integer :x, :y
215
+ def execute
216
+ sum = compose(Add, inputs)
217
+ square = compose(Square, x: sum)
218
+ compose(Add, x: square, y: square)
219
+ end
214
220
  end
215
- outcome = pipeline.run(x: 3, y: 5)
216
- outcome.result
217
- # => 128 # ((3 + 5) ** 2) * 2
221
+ DoSomeMath.run!(x: 3, y: 5)
222
+ # 128 => ((3 + 5) ** 2) * 2
218
223
  ```
219
224
 
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
+ ```ruby
226
+ class AddThree < ActiveInteraction::Base
227
+ integer :y
228
+ def execute
229
+ compose(Add, x: 3, y: y)
230
+ end
231
+ end
232
+ AddThree.run!(y: nil)
233
+ # => ActiveInteraction::InvalidInteractionError: Y is required
234
+ ```
225
235
 
226
236
  ## How do I translate an interaction?
227
237
 
@@ -270,6 +280,8 @@ p Interaction.run.errors.messages
270
280
 
271
281
  This project was inspired by the fantastic work done in [Mutations][].
272
282
 
283
+ [#41]: https://github.com/orgsync/active_interaction/issues/41
284
+ [#79]: https://github.com/orgsync/active_interaction/issues/79
273
285
  [1]: https://badge.fury.io/rb/active_interaction "Gem Version"
274
286
  [2]: https://travis-ci.org/orgsync/active_interaction "Build Status"
275
287
  [3]: https://coveralls.io/r/orgsync/active_interaction "Coverage Status"
@@ -283,4 +295,4 @@ This project was inspired by the fantastic work done in [Mutations][].
283
295
  [gem version]: https://badge.fury.io/rb/active_interaction.png
284
296
  [mutations]: https://github.com/cypriss/mutations
285
297
  [project page]: http://orgsync.github.io/active_interaction/
286
- [semantic versioning]: http://semver.org
298
+ [semantic versioning]: http://semver.org/spec/v2.0.0.html
@@ -27,7 +27,6 @@ require 'active_interaction/filters/symbol_filter'
27
27
  require 'active_interaction/filters/time_filter'
28
28
 
29
29
  require 'active_interaction/base'
30
- require 'active_interaction/pipeline'
31
30
 
32
31
  I18n.backend.load_translations(
33
32
  Dir.glob(File.join(%w(lib active_interaction locale *.yml)))
@@ -54,6 +54,7 @@ module ActiveInteraction
54
54
  begin
55
55
  send("#{filter.name}=", filter.clean(options[filter.name]))
56
56
  rescue InvalidValueError, MissingValueError
57
+ # Validators (#input_errors) will add errors if appropriate.
57
58
  end
58
59
  end
59
60
  end
@@ -86,8 +87,7 @@ module ActiveInteraction
86
87
  # Returns the output from {#execute} if there are no validation errors or
87
88
  # `nil` otherwise.
88
89
  #
89
- # @return [Nil] if there are validation errors.
90
- # @return [Object] if there are no validation errors.
90
+ # @return [Object, nil] the output or nil if there were validation errors
91
91
  def result
92
92
  @_interaction_result
93
93
  end
@@ -120,7 +120,13 @@ module ActiveInteraction
120
120
  def self.run(*args)
121
121
  new(*args).tap do |interaction|
122
122
  if interaction.valid?
123
- result = transaction { interaction.execute }
123
+ result = transaction do
124
+ begin
125
+ interaction.execute
126
+ rescue Interrupt
127
+ # Inner interaction failed. #compose handles merging errors.
128
+ end
129
+ end
124
130
 
125
131
  if interaction.errors.empty?
126
132
  interaction.instance_variable_set(:@_interaction_result, result)
@@ -162,15 +168,22 @@ module ActiveInteraction
162
168
  end
163
169
 
164
170
  def runtime_errors
165
- return unless @_interaction_runtime_errors
166
-
167
- @_interaction_runtime_errors.symbolic.each do |attribute, symbols|
168
- symbols.each { |symbol| errors.add_sym(attribute, symbol) }
171
+ if @_interaction_runtime_errors
172
+ errors.merge!(@_interaction_runtime_errors)
169
173
  end
174
+ end
170
175
 
171
- @_interaction_runtime_errors.messages.each do |attribute, messages|
172
- messages.each { |message| errors.add(attribute, message) }
176
+ def compose(interaction, options = {})
177
+ outcome = interaction.run(options)
178
+ return outcome.result if outcome.valid?
179
+
180
+ # This can't use Errors#merge! because the errors have to be added to
181
+ # base.
182
+ outcome.errors.full_messages.each do |message|
183
+ errors.add(:base, message) unless errors.added?(:base, message)
173
184
  end
185
+
186
+ raise Interrupt
174
187
  end
175
188
  end
176
189
  end
@@ -2,9 +2,6 @@ module ActiveInteraction
2
2
  # Top-level error class. All other errors subclass this.
3
3
  Error = Class.new(StandardError)
4
4
 
5
- # Raised when trying to run an empty pipeline.
6
- EmptyPipelineError = Class.new(Error)
7
-
8
5
  # Raised if a class name is invalid.
9
6
  InvalidClassError = Class.new(Error)
10
7
 
@@ -29,6 +26,10 @@ module ActiveInteraction
29
26
  # Raised if there is no default value.
30
27
  NoDefaultError = Class.new(Error)
31
28
 
29
+ # @private
30
+ Interrupt = Class.new(::Interrupt)
31
+ private_constant :Interrupt
32
+
32
33
  # A small extension to provide symbolic error messages to make introspecting
33
34
  # and testing easier.
34
35
  #
@@ -80,5 +81,28 @@ module ActiveInteraction
80
81
  symbolic.clear
81
82
  super
82
83
  end
84
+
85
+ # Merge other errors into this one.
86
+ #
87
+ # @param other [Errors]
88
+ #
89
+ # @return [Errors]
90
+ def merge!(other)
91
+ other.symbolic.each do |attribute, symbols|
92
+ symbols.each do |symbol|
93
+ add_sym(attribute, symbol)
94
+ end
95
+ end
96
+
97
+ other.messages.each do |attribute, messages|
98
+ messages.each do |message|
99
+ unless added?(attribute, message)
100
+ add(attribute, message)
101
+ end
102
+ end
103
+ end
104
+
105
+ self
106
+ end
83
107
  end
84
108
  end
@@ -85,6 +85,7 @@ module ActiveInteraction
85
85
  begin
86
86
  CLASSES[klass.slug] = klass
87
87
  rescue InvalidClassError
88
+ # Ignore classes with invalid slugs.
88
89
  end
89
90
  end
90
91
  end
@@ -40,7 +40,7 @@ module ActiveInteraction
40
40
  end
41
41
  end
42
42
 
43
- def method_missing(*args, &block)
43
+ def method_missing(*_, &block)
44
44
  super do |klass, names, options|
45
45
  filter = klass.new(name, options, &block)
46
46
 
@@ -1,13 +1,13 @@
1
1
  begin
2
2
  require 'active_record'
3
3
  rescue LoadError
4
+ # ActiveRecord is an optional dependency.
4
5
  end
5
6
 
6
7
  module ActiveInteraction
7
- # Functionality common between {Base} and {Pipeline}.
8
+ # Functionality common between {Base}.
8
9
  #
9
10
  # @see Base
10
- # @see Pipeline
11
11
  module Core
12
12
  # Get or set the description.
13
13
  #
@@ -1,3 +1,3 @@
1
1
  module ActiveInteraction
2
- VERSION = Gem::Version.new('0.8.0')
2
+ VERSION = Gem::Version.new('0.9.0')
3
3
  end
@@ -272,6 +272,55 @@ describe ActiveInteraction::Base do
272
272
  end
273
273
  end
274
274
 
275
+ describe '#compose' do
276
+ let(:described_class) { InterruptInteraction }
277
+ let(:x) { rand }
278
+ let(:y) { rand }
279
+
280
+ AddInteraction = Class.new(ActiveInteraction::Base) do
281
+ float :x, :y
282
+
283
+ def execute
284
+ x + y
285
+ end
286
+ end
287
+
288
+ InterruptInteraction = Class.new(ActiveInteraction::Base) do
289
+ model :x, :y,
290
+ class: Object,
291
+ default: nil
292
+
293
+ def execute
294
+ compose(AddInteraction, inputs)
295
+ end
296
+ end
297
+
298
+ context 'with valid composition' do
299
+ before do
300
+ options.merge!(x: x, y: y)
301
+ end
302
+
303
+ it 'is valid' do
304
+ expect(outcome).to be_valid
305
+ end
306
+
307
+ it 'returns the sum' do
308
+ expect(result).to eq x + y
309
+ end
310
+ end
311
+
312
+ context 'with invalid composition' do
313
+ it 'is invalid' do
314
+ expect(outcome).to be_invalid
315
+ end
316
+
317
+ it 'has the correct errors' do
318
+ expect(outcome.errors[:base]).
319
+ to match_array ['X is required', 'Y is required']
320
+ end
321
+ end
322
+ end
323
+
275
324
  describe '#execute' do
276
325
  it 'raises an error' do
277
326
  expect { interaction.execute }.to raise_error NotImplementedError
@@ -18,7 +18,7 @@ describe HashInteraction do
18
18
  it_behaves_like 'an interaction', :hash, -> { {} }
19
19
 
20
20
  context 'with options[:a]' do
21
- let(:a) { { 'x' => {} } }
21
+ let(:a) { { x: {} } }
22
22
 
23
23
  before { options.merge!(a: a) }
24
24
 
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Lasseigne
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-11-14 00:00:00.000000000 Z
12
+ date: 2013-12-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel
@@ -174,7 +174,6 @@ files:
174
174
  - lib/active_interaction/modules/method_missing.rb
175
175
  - lib/active_interaction/modules/overload_hash.rb
176
176
  - lib/active_interaction/modules/validation.rb
177
- - lib/active_interaction/pipeline.rb
178
177
  - lib/active_interaction/version.rb
179
178
  - lib/active_interaction.rb
180
179
  - spec/active_interaction/base_spec.rb
@@ -211,7 +210,6 @@ files:
211
210
  - spec/active_interaction/modules/method_missing_spec.rb
212
211
  - spec/active_interaction/modules/overload_hash_spec.rb
213
212
  - spec/active_interaction/modules/validation_spec.rb
214
- - spec/active_interaction/pipeline_spec.rb
215
213
  - spec/spec_helper.rb
216
214
  - spec/support/filters.rb
217
215
  - spec/support/interactions.rb
@@ -277,7 +275,6 @@ test_files:
277
275
  - spec/active_interaction/modules/method_missing_spec.rb
278
276
  - spec/active_interaction/modules/overload_hash_spec.rb
279
277
  - spec/active_interaction/modules/validation_spec.rb
280
- - spec/active_interaction/pipeline_spec.rb
281
278
  - spec/spec_helper.rb
282
279
  - spec/support/filters.rb
283
280
  - spec/support/interactions.rb
@@ -1,92 +0,0 @@
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
-
64
- transaction do
65
- function, interaction = @steps.first
66
- outcome = interaction.run(function.call(*args))
67
- @steps.drop(1).reduce(outcome) { |o, (f, i)| bind(o, f, i) }
68
- end
69
- end
70
-
71
- private
72
-
73
- def bind(outcome, function, interaction)
74
- if outcome.valid?
75
- interaction.run(function.call(outcome.result))
76
- else
77
- outcome
78
- end
79
- end
80
-
81
- def lambdafy(thing)
82
- case thing
83
- when NilClass
84
- -> result { result }
85
- when Symbol
86
- -> result { { thing => result } }
87
- else
88
- thing
89
- end
90
- end
91
- end
92
- end
@@ -1,117 +0,0 @@
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