active_interaction 0.8.0 → 0.9.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: 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