stepladder 0.0.2 → 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7cd6ff328453446dc663837b4e70fd2df7d82de39429409418445e597b393d86
4
+ data.tar.gz: 9907fe3ed9411bcb7dc50ad62a08cd50faaecfd5bed31d172948e5997c3c87dc
5
+ SHA512:
6
+ metadata.gz: 6ad4c68022bf02a60ad99c60fd7ed77b5d67f20e88e1adf344b1957807249065329279cc70afcf6759886f4fdfb0e49152bf406533215693a57e261df66142d1
7
+ data.tar.gz: 262b43d27e1d36b5da47a648611d75b912545a6487ec1acf493b08c91ce39c3b10ca3e355de5b969e27558aeaaaee0519ba8fd3c5ccc186ce23b8a8155be7cec
data/.gitignore CHANGED
@@ -1,4 +1,6 @@
1
1
  ./spikes/*
2
2
  .rvmrc
3
+ .ruby-version
4
+ .ruby-gemset
3
5
  Gemfile.lock
4
6
  pkg
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ bundler_args: --without development
3
+ rvm:
4
+ - 2.5.0
5
+ - jruby-19mode
data/Gemfile CHANGED
@@ -1,5 +1,8 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
- gem 'cucumber'
4
- gem 'aruba'
5
- gem 'rspec'
3
+ gem 'rake', '10.3.2'
4
+ gem 'rspec', '3.1.0'
5
+ gem 'rspec-core', '3.1.2'
6
+ gem 'rspec-its', '1.0.1'
7
+ gem 'rspec-given', '3.8.0'
8
+ gem 'pry'
data/README.md CHANGED
@@ -1,112 +1,255 @@
1
+ [![Build Status](https://travis-ci.org/joelhelbling/stepladder.png)](https://travis-ci.org/joelhelbling/stepladder)
2
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/joelhelbling/stepladder)
3
+
1
4
  # The Stepladder Framework
2
5
 
3
6
  _"How many Ruby fibers does it take to screw in a lightbulb?"_
4
7
 
5
8
  ## Quick Start
6
9
 
7
- ### Workers have tasks...
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'stepladder'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself:
8
21
 
9
- Initialize with a block of code:
22
+ $ gem install steplader
23
+
24
+ And then use it:
10
25
 
11
26
  ```ruby
12
- source_worker = Stepladder::Worker.new { "hulk" }
27
+ require 'stepladder'
28
+ ```
29
+
30
+ ## New! Stepladders now has a DSL!
31
+
32
+ As of version ?? there is now a DSL which provides convenient shorthand
33
+ for several common types of workers. Let's look at them.
13
34
 
14
- source_worker.product #=> "hulk"
35
+ But first, be sure to include the DSL mixin:
36
+
37
+ ```ruby
38
+ include Stepladder::Dsl
15
39
  ```
16
- If you supply a worker with another worker as its supplier, then you
17
- can give it a task which accepts a value:
40
+
41
+ ### Source Worker
42
+
43
+ At the headwater of every Stepladder pipeline, there is a source worker
44
+ which is able to generate its own work items.
45
+
46
+ Here is possibly the simplest of source workers, which provides the same
47
+ string each time it is invoked:
18
48
 
19
49
  ```ruby
20
- relay_worker = Stepladder::Worker.new { |name| name.upcase }
21
- relay_worker.supplier = source_worker
50
+ worker = source_worker { "Number 9" }
22
51
 
23
- relay_worker.product #=> "HULK"
52
+ worker.product #=> "Number 9"
53
+ worker.product #=> "Number 9"
54
+ worker.product #=> "Number 9"
55
+ # ...ad nauseum...
24
56
  ```
25
57
 
26
- You can also initialize a worker by passing in a callable object
27
- as its task:
58
+ Sometimes you want a worker which generates until it doesn't:
28
59
 
29
60
  ```ruby
30
- capitalizer = Proc.new { |name| name.capitalize }
31
- relay_worker = Stepladder::Worker.new(task: capitalizer, supplier: source_worker)
61
+ counter = 0
62
+ worker = source_worker do
63
+ if counter < 3
64
+ counter += 1
65
+ counter + 1000
66
+ end
67
+ end
32
68
 
33
- relay_worker.product #=> 'Hulk'
69
+ worker.product #=> 1001
70
+ worker.product #=> 1002
71
+ worker.product #=> 1003
72
+ worker.product #=> nil
73
+ worker.product #=> nil (and will be nil henceforth)
34
74
  ```
35
75
 
36
- A worker also has an accessor for its @task:
76
+ If all you want is a source worker which generates a series of numbers
77
+ the DSL provides an easier way to do it:
37
78
 
38
79
  ```ruby
39
- doofusizer = Proc.new { |name| name.gsub(/u/, 'oo') }
40
- relay_worker.task = doofusizer
80
+ w1 = source_worker [0,1,2]
81
+ w2 = source_worker (0..2)
41
82
 
42
- relay_worker.product #=> 'hoolk'
83
+ w1.product == w2.product #=> 0
84
+ w1.product == w2.product #=> 1
85
+ w1.product == w2.product #=> 2
86
+ w1.product == w2.product #=> nil (and henceforth, etc.)
43
87
  ```
44
88
 
45
- And finally, you can provide a task by directly overriding the
46
- worker's #task instance method:
89
+ ### Relay Worker
90
+
91
+ This is perhaps the most "normal" kind of worker in Stepladder. It
92
+ accepts a value and returns some transformation of that value:
47
93
 
48
94
  ```ruby
49
- def relay_worker.task(name)
50
- name.to_sym
95
+ squarer = relay_worker do |number|
96
+ number ** 2
51
97
  end
98
+ ```
99
+
100
+ Of course a relay worker needs a source from which to get values upon
101
+ which to operate:
52
102
 
53
- relay_worker.product #=> :hulk
103
+ ```ruby
104
+ source = source_worker (0..3)
105
+
106
+ squarer.supplier = source
54
107
  ```
55
108
 
56
- Even workers without a task have a task; all workers actually come
57
- with a default task which simply passes on the received value unchanged:
109
+ Or, if you prefer, the DSL provides a vertical pipe for linking the
110
+ workers together into a pipeline:
58
111
 
59
112
  ```ruby
60
- useless_worker = Stepladder::Worker.new(supplier: source_worker)
113
+ pipeline = source | squarer
61
114
 
62
- useless_worker.product #=> 'hulk'
115
+ pipeline.product #=> 0
116
+ pipeline.product #=> 1
117
+ pipeline.product #=> 4
118
+ pipeline.product #=> 9
119
+ pipeline.product #=> nil
63
120
  ```
64
121
 
65
- This turns out to be helpful in implementing filter workers, which are up next.
122
+ ### Side Worker
66
123
 
67
- ### Workers can have filters...
124
+ As we travel down the pipeline, from time to time, we will want to
125
+ stop and smell the roses, or write to a log file, or drop a beat, or
126
+ create some other kind of side-effect. That's the purpose of the
127
+ `side_worker`. A side worker will pass through the same value that
128
+ it received, but as it does so, it can perform some kind side-effect
129
+ work.
68
130
 
69
- Given a source worker which provides integers 1-3:
131
+ ```
132
+ source = source_worker (0..3)
70
133
 
71
- ```ruby
72
- source = Stepladder::Worker.new do
73
- (1..3).each { |number| handoff number }
134
+ evens = []
135
+ even_stasher = side_effect do |value|
136
+ if value % 2 == 0
137
+ evens << value
138
+ end
74
139
  end
140
+
141
+ # re-using "squarer" from above example...
142
+ pipeline = source | even_stasher | squarer
143
+
144
+ pipeline.product #=> 0
145
+ pipeline.product #=> 1
146
+ pipeline.product #=> 4
147
+ pipeline.product #=> 9
148
+ pipeline.product #=> nil
149
+
150
+ evens #=> [0, 2]
75
151
  ```
76
152
 
77
- ...we can define a subscribing worker with a filter:
153
+ Notice that the output is the same as the previous example, even though
154
+ we put this `even_stasher` `side_worker` in the middle of the pipeline.
155
+
156
+ However, we can also see that the `evens` array now contains the even numbers from `source` (the side effect).
157
+
158
+ _But wait,_ you want to say, _can't we still create side effects with
159
+ regular ole relay workers?_ Why, yes. Yes you can. Ruby being what
160
+ it is, there really isn't a way to prevent the implementation of any
161
+ worker from creating side effects.
162
+
163
+ _And still wait,_ you'll also be wanting to say, _isn't it possible
164
+ that a side worker could mutate the value as it's passed through?_ And
165
+ again, yes. It would be very difficult\* in the Ruby language (where
166
+ so many things are passed by reference) to perfectly prevent a side
167
+ worker from mutating the value. But please don't.
168
+
169
+ The side effect worker's purpose is to provide _intentionality_ and
170
+ clarity. When you're creating a side effect, let it be very clear.
171
+ And don't do regular, pure functional style work in the same worker.
172
+
173
+ This will make side effects easier to troubleshoot; if you pull them
174
+ out of the pipeline, the pipeline's output shouldn't change. By the
175
+ same token, if there is a problem with a side effect, troubleshooting
176
+ it will be much simpler if the side effects are already isolated and
177
+ named.
178
+
179
+ \* _Under consideration: a variant of the side-effect which attempts
180
+ to prevent side-effects by doing a `Marshal.dump/load`. But the
181
+ potential overhead in all that marshalling makes me hesitant to make
182
+ this the default behavior. Making it available as an option, however,
183
+ opens the possibility to troubleshoot side effects: if the marshalling
184
+ eliminates an unwanted mutation, then chances are that you have a
185
+ side effect that is doing mutation._
186
+
187
+ ### Filter Worker
188
+
189
+ The filter worker simply passes through the values which are given
190
+ to it, but _only_ those values which result in truthiness when your
191
+ provided callable is run against it. Values which result in
192
+ falsiness are simply discarded.
78
193
 
79
194
  ```ruby
80
- odd_number_filter = Proc.new { |number| number % 2 > 0 }
81
- filter_worker = Stepladder::Worker.new filter: odd_number_filter
195
+ source = source_worker (0..5)
196
+
197
+ filter = filter_worker do |value|
198
+ value % 2 == 0
199
+ end
82
200
 
83
- filter_worker.product #=> 1
84
- filter_worker.product #=> 3
85
- filter_worker.product #=> nil
201
+ pipeline = source | filter
202
+
203
+ pipeline.product #=> 0
204
+ pipeline.product #=> 2
205
+ pipeline.product #=> 4
206
+ pipeline.product #=> nil
86
207
  ```
87
208
 
88
- ### The pipeline DSL
209
+ ### Batch Worker
89
210
 
90
- You can stitch your workers together using the vertical pipe ("|") like so:
211
+ The batch worker gathers outputs into batches and the returns each
212
+ batch as its output.
91
213
 
92
214
  ```ruby
93
- pipeline = source_worker | filter_worker | relay_worker | another worker
215
+ source = source_worker (0..7)
216
+
217
+ batch = batch_worker gathering: 3
218
+
219
+ pipeline = source | batch
220
+
221
+ pipeline.product #=> [0, 1, 2]
222
+ pipeline.product #=> [3, 4, 5]
223
+ pipeline.product #=> [6, 7]
224
+ pipeline.product #=> nil
94
225
  ```
95
226
 
96
- ...and then just call on that pipeline (it's actually the last worker in the
97
- chain):
227
+ Notice how the final batch doesn't have full compliment of three.
228
+
229
+ Fixed-numbered batch workers are useful for things like pagination,
230
+ perhaps, but sometimes we don't want a batch to include a specific
231
+ number of items, but to batch together all items until a certain
232
+ condition is met. So batch worker can also accept a callable:
98
233
 
99
234
  ```ruby
100
- while next_value = pipeline.product do
101
- do_something_with next_value
102
- # etc.
235
+ source = source_worker [
236
+ "some", "rain", "must\n", "fall", "but", "ok" ]
237
+
238
+ line_reader = batch_worker do |value|
239
+ value.end_with? "\n"
103
240
  end
241
+
242
+ pipeline = source | line_reader
243
+
244
+ pipeline.product #=> ["some", "rain", "must\n"]
245
+ pipeline.product #=> ["fall", "but", "ok"]
246
+ pipeline.product #=> nil
104
247
  ```
105
248
 
106
249
  ## Origins of Stepladder
107
250
 
108
251
  Stepladder grew out of experimentation with Ruby fibers, after readings
109
- [Dave Thomas' demo of Ruby fibers](http://pragdave.blogs.pragprog.com/pragdave/2007/12/pipelines-using.html), wherein he created a
252
+ [Dave Thomas' demo of Ruby fibers](http://pragdave.me/blog/2007/12/30/pipelines-using-fibers-in-ruby-19/), wherein he created a
110
253
  pipeline of fiber processes, emulating the style and syntax of the
111
254
  \*nix command line. I noticed that, courtesy of fibers' extremely
112
255
  low surface area, fiber-to-fiber collaborators could operate with
@@ -176,50 +319,59 @@ just _common objects_ coupling-- this little remaining coupling is
176
319
  mitigated by the possibility of having coupled code _live together_.
177
320
  I call this feature the _folded collaborators_ effect.
178
321
 
179
- Consider the following -code- vaporware:
322
+ Consider the following ~~code~~ vaporware:
180
323
 
181
324
  ```ruby
182
- ME = "joelhelbling"
325
+ SUBJECT = 'kitteh'
183
326
 
184
- module Stepladder
185
- tweet_getter = Worker.new do
186
- twitter_api.fetch_my_tweets
187
- end
327
+ include Stepladder::Dsl
188
328
 
189
- about_me_filter = Proc.new { |tweet| tweet.referenced.include? ME }
190
- just_about_me_getter = Worker.new filter: about_me_filter
329
+ tweet_getter = source_worker do
330
+ twitter_api.fetch_my_tweets
331
+ end
191
332
 
192
- tweet_formatter = Worker.new do |tweet|
193
- apply_format_to tweet
194
- end
333
+ about_me = filter_worker do |tweet|
334
+ tweet.referenced.include? SUBJECT
335
+ end
336
+
337
+ tweet_formatter = relay_worker do |tweet|
338
+ apply_format_to tweet
339
+ end
340
+
341
+ kitteh_tweets = tweet_getter | about_me | tweet_formatter
195
342
 
196
- formatted_tweets = tweet_getter | just_about_me_getter | tweet_formatter
343
+ while tweet = kitteh_tweets.product
344
+ display(tweet)
197
345
  end
198
346
  ```
199
347
 
200
348
  None of these tasks have hard coupling with each other. If we were to
201
- insert another worker between the filter and the formatter, neither of those
202
- workers would need changes in their code, assuming the inserted worker plays
203
- nicely with the objects they're all passing along.
349
+ insert another worker between the filter and the formatter, neither of
350
+ those workers would need changes in their code, assuming the inserted
351
+ worker plays nicely with the objects they're all passing along.
204
352
 
205
353
  Which brings me to the point: these workers to have a dependency upon the
206
354
  objects they're handing off and receiving. But we have the capability to
207
355
  coordinate those workers in a centralized location (such as in this code
208
356
  example).
209
357
 
210
- ## Ok, but why is it called "Stepladder"?
211
-
212
- This framework's name was inspired by a conversation with Tim Wingfield
213
- in which we joked about the profusion of new frameworks in the Ruby
214
- community. We quickly began riffing on a fictional framework called
215
- "Stepladder" which all the cool kids, we asserted, were (or would soon
216
- be) using.
358
+ ## Stepladder::Worker
217
359
 
218
- I have waited a long time to make that farce a reality, but hey, I take
219
- joke frameworks very seriously. ;)
220
- ([Really?](http://github.com/joelhelbling/really))
360
+ The Stepladder::Worker documentation has been moved
361
+ [here](docs/stepladder/worker.md).
221
362
 
222
363
  ## Roadmap
223
364
 
224
- - add a nicer top-layer to the DSL --no reason we should have to do
225
- all that `Worker.new` stuff
365
+ - `splitter_worker` -- would accept a value and return an array. The
366
+ elements of that returned array would then be output as separate values
367
+ to the next worker in the pipeline.
368
+ - `batch_worker trailing: n` -- accepts a value, and returns the last n
369
+ values. This would mean that no values would be returned until n
370
+ values had been accumulated. This could be useful for things like
371
+ rolling averages.
372
+ - `side_worker(:hardened) { |v| do_stuff_with(v) }` -- the `:hardened`
373
+ flag would attempt to ensure no side effects may occur by using
374
+ `Marshal` to dump/load the value before handing it to the workers
375
+ callable. Also might make a runtime-wide toggle which hardens all
376
+ side_workers, which could be useful in flushing out side workers
377
+ which are doing inadvertent mutation.
data/Rakefile CHANGED
@@ -1,3 +1,11 @@
1
- require 'bundler'
1
+ require 'rspec/core/rake_task'
2
2
 
3
3
  Bundler::GemHelper.install_tasks
4
+
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.verbose = false
7
+ t.pattern = 'spec/lib/**/*_spec.rb'
8
+ t.rspec_opts = " --format doc"
9
+ end
10
+
11
+ task default: :spec
@@ -0,0 +1,103 @@
1
+ # Stepladder::Worker
2
+
3
+ _The workhorse of the Stepladder framework._
4
+
5
+ ## Workers have tasks...
6
+
7
+ Initialize with a block of code:
8
+
9
+ ```ruby
10
+ source_worker = Stepladder::Worker.new { "hulk" }
11
+
12
+ source_worker.product #=> "hulk"
13
+ ```
14
+ If you supply a worker with another worker as its supplier, then you
15
+ can give it a task which accepts a value:
16
+
17
+ ```ruby
18
+ relay_worker = Stepladder::Worker.new { |name| name.upcase }
19
+ relay_worker.supplier = source_worker
20
+
21
+ relay_worker.product #=> "HULK"
22
+ ```
23
+
24
+ You can also initialize a worker by passing in a callable object
25
+ as its task:
26
+
27
+ ```ruby
28
+ capitalizer = Proc.new { |name| name.capitalize }
29
+ relay_worker = Stepladder::Worker.new(task: capitalizer, supplier: source_worker)
30
+
31
+ relay_worker.product #=> 'Hulk'
32
+ ```
33
+
34
+ A worker also has an accessor for its @task:
35
+
36
+ ```ruby
37
+ doofusizer = Proc.new { |name| name.gsub(/u/, 'oo') }
38
+ relay_worker.task = doofusizer
39
+
40
+ relay_worker.product #=> 'hoolk'
41
+ ```
42
+
43
+ And finally, you can provide a task by directly overriding the
44
+ worker's #task instance method:
45
+
46
+ ```ruby
47
+ def relay_worker.task(name)
48
+ name.to_sym
49
+ end
50
+
51
+ relay_worker.product #=> :hulk
52
+ ```
53
+
54
+ Even workers without a task have a task; all workers actually come
55
+ with a default task which simply passes on the received value unchanged:
56
+
57
+ ```ruby
58
+ useless_worker = Stepladder::Worker.new(supplier: source_worker)
59
+
60
+ useless_worker.product #=> 'hulk'
61
+ ```
62
+
63
+ This turns out to be helpful in implementing filter workers, which are up next.
64
+
65
+ ## Workers can have filters...
66
+
67
+ Given a source worker which provides integers 1-3:
68
+
69
+ ```ruby
70
+ source = Stepladder::Worker.new do
71
+ (1..3).each { |number| handoff number }
72
+ end
73
+ ```
74
+
75
+ ...we can define a subscribing worker with a filter:
76
+
77
+ ```ruby
78
+ odd_number_filter = Proc.new { |number| number % 2 > 0 }
79
+ filter_worker = Stepladder::Worker.new filter: odd_number_filter
80
+
81
+ filter_worker.product #=> 1
82
+ filter_worker.product #=> 3
83
+ filter_worker.product #=> nil
84
+ ```
85
+
86
+ ## The pipeline DSL
87
+
88
+ You can stitch your workers together using the vertical pipe ("|") like so:
89
+
90
+ ```ruby
91
+ pipeline = source_worker | filter_worker | relay_worker | another worker
92
+ ```
93
+
94
+ ...and then just call on that pipeline (it's actually the last worker in the
95
+ chain):
96
+
97
+ ```ruby
98
+ while next_value = pipeline.product do
99
+ do_something_with next_value
100
+ # etc.
101
+ end
102
+ ```
103
+
@@ -0,0 +1,137 @@
1
+ module Stepladder
2
+ class WorkerInitializationError < StandardError; end
3
+
4
+ module Dsl
5
+ def source_worker(argument=nil, &block)
6
+ ensure_correct_arity_for!(argument, block)
7
+
8
+ series = series_from(argument)
9
+ callable = setup_callable_for(block, series)
10
+
11
+ return Worker.new(&callable) if series.nil?
12
+
13
+ Worker.new do
14
+ series.each(&callable)
15
+
16
+ while true do
17
+ handoff nil
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ def relay_worker(&block)
24
+ ensure_regular_arity(block)
25
+
26
+ Worker.new do |value|
27
+ value && block.call(value)
28
+ end
29
+ end
30
+
31
+ def side_worker(&block)
32
+ ensure_regular_arity(block)
33
+
34
+ Worker.new do |value|
35
+ value.tap do |v|
36
+ v && block.call(v)
37
+ end
38
+ end
39
+ end
40
+
41
+ def filter_worker(argument=nil, &block)
42
+ if (block && argument.respond_to?(:call))
43
+ throw_with 'You cannot supply two callables'
44
+ end
45
+ callable = argument.respond_to?(:call) ? argument : block
46
+
47
+ ensure_callable(callable)
48
+ Worker.new filter: block
49
+ end
50
+
51
+ def batch_worker(options = {gathering: 1}, &block)
52
+ ensure_regular_arity(block) if block
53
+
54
+ Worker.new.tap do |worker|
55
+ worker.instance_variable_set(:@batch_size, options[:gathering])
56
+ worker.instance_variable_set(:@batch_complete_block, block)
57
+
58
+ def worker.task(value)
59
+ if value
60
+ @collection = [value]
61
+ until batch_complete?(@collection.last)
62
+ @collection << supplier.product
63
+ end
64
+ @collection.compact
65
+ end
66
+ end
67
+
68
+ def worker.batch_complete?(value)
69
+ return true if value.nil?
70
+ if @batch_complete_block
71
+ !! @batch_complete_block.call(value)
72
+ else
73
+ @collection.size >= @batch_size
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def handoff(something)
80
+ Fiber.yield something
81
+ end
82
+
83
+ private
84
+
85
+ def throw_with(*msg)
86
+ raise WorkerInitializationError.new([msg].flatten.join(' '))
87
+ end
88
+
89
+ def ensure_callable(callable)
90
+ unless callable && callable.respond_to?(:call)
91
+ throw_with 'You must supply a callable'
92
+ end
93
+ end
94
+
95
+ def ensure_regular_arity(block)
96
+ if block.arity != 1
97
+ throw_with \
98
+ "Worker must accept exactly one argument (arity == 1)"
99
+ end
100
+ end
101
+
102
+ # only valid for #source_worker
103
+ def ensure_correct_arity_for!(argument, block)
104
+ return unless block
105
+ if argument
106
+ ensure_regular_arity(block)
107
+ else
108
+ if block.arity > 0
109
+ throw_with \
110
+ 'Source worker cannot accept any arguments (arity == 0)'
111
+ end
112
+ end
113
+ end
114
+
115
+ def series_from(series)
116
+ return if series.nil?
117
+ case
118
+ when series.respond_to?(:to_a)
119
+ series.to_a
120
+ when series.respond_to?(:scan)
121
+ series.scan(/./)
122
+ else
123
+ [series]
124
+ end
125
+ end
126
+
127
+ def setup_callable_for(block, series)
128
+ return block unless series
129
+ if block
130
+ return Proc.new { |value| handoff block.call(value) }
131
+ else
132
+ return Proc.new { |value| handoff value }
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -1,3 +1,3 @@
1
1
  module Stepladder
2
- VERSION = "0.0.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -6,20 +6,20 @@ module Stepladder
6
6
  @supplier = p[:supplier]
7
7
  @filter = p[:filter] || default_filter
8
8
  @task = block || p[:task]
9
+ # don't define default task here
10
+ # because we want to allow for
11
+ # an initialized worker to have
12
+ # a task injected, including
13
+ # method-based tasks.
9
14
  end
10
15
 
11
16
  def product
12
- if ready_to_work?
13
- work.resume
14
- end
17
+ ensure_ready_to_work!
18
+ workflow.resume
15
19
  end
16
20
 
17
21
  def ready_to_work?
18
- @task ||= default_task
19
- if (task_accepts_a_value? && supplier.nil?)
20
- raise "This worker's task expects to receive a value from a supplier, but has no supplier."
21
- end
22
- true
22
+ @task && (supplier || !task_accepts_a_value?)
23
23
  end
24
24
 
25
25
  def |(subscribing_worker)
@@ -29,12 +29,23 @@ module Stepladder
29
29
 
30
30
  private
31
31
 
32
- def work
32
+ def ensure_ready_to_work!
33
+ @task ||= default_task
34
+ # at this point we will ensure a task exists
35
+ # because we know that the worker is being
36
+ # asked for product
37
+
38
+ unless ready_to_work?
39
+ raise "This worker's task expects to receive a value from a supplier, but has no supplier."
40
+ end
41
+ end
42
+
43
+ def workflow
33
44
  @my_little_machine ||= Fiber.new do
34
45
  loop do
35
46
  value = supplier && supplier.product
36
47
  if value.nil? || passes_filter?(value)
37
- handoff @task.call(value)
48
+ Fiber.yield @task.call(value)
38
49
  end
39
50
  end
40
51
  end
@@ -76,7 +87,3 @@ module Stepladder
76
87
 
77
88
  end
78
89
  end
79
-
80
- def handoff(product)
81
- Fiber.yield product
82
- end
data/lib/stepladder.rb ADDED
@@ -0,0 +1,3 @@
1
+ require_relative 'stepladder/version'
2
+ require_relative 'stepladder/worker'
3
+ require_relative 'stepladder/dsl'
@@ -0,0 +1,256 @@
1
+ require 'spec_helper'
2
+
3
+ module Stepladder
4
+ describe Dsl do
5
+ include Stepladder::Dsl
6
+
7
+ describe '#source_worker' do
8
+ context 'normal usage' do
9
+ context 'with an array' do
10
+ Given(:worker) { source_worker [:fee, :fi] }
11
+
12
+ Then { worker.product == :fee }
13
+ And { worker.product == :fi }
14
+ And { worker.product.nil? }
15
+ end
16
+
17
+ context 'with a range' do
18
+ Given(:worker) { source_worker (0..2) }
19
+
20
+ Then { worker.product == 0 }
21
+ And { worker.product == 1 }
22
+ And { worker.product == 2 }
23
+ And { worker.product.nil? }
24
+ end
25
+
26
+ context 'with a string' do
27
+ Given(:worker) { source_worker 'abc' }
28
+
29
+ Then { worker.product == 'a' }
30
+ And { worker.product == 'b' }
31
+ And { worker.product == 'c' }
32
+ And { worker.product.nil? }
33
+ end
34
+
35
+ context 'with a hash' do
36
+ Given(:worker) { source_worker({foo: 2, bar: 'yarr'}) }
37
+
38
+ Then { worker.product == [:foo, 2] }
39
+ And { worker.product == [:bar, 'yarr'] }
40
+ And { worker.product.nil? }
41
+ end
42
+
43
+ context 'with a anything else' do
44
+ Given(:worker) { source_worker :foo }
45
+
46
+ Then { worker.product == :foo }
47
+ And { worker.product.nil? }
48
+ end
49
+
50
+ context 'with a callable' do
51
+ Given(:worker) do
52
+ notes = %i[doh ray me fa so la ti]
53
+ source_worker do
54
+ notes.shift if notes.size > 4
55
+ end
56
+ end
57
+
58
+ Then { worker.product == :doh }
59
+ And { worker.product == :ray }
60
+ And { worker.product == :me }
61
+ And { worker.product.nil? }
62
+ end
63
+
64
+ context 'with a callable and an argument' do
65
+ Given(:notes) { %i[so la ti] }
66
+ Given(:worker) do
67
+ source_worker notes do |note|
68
+ note.to_s.upcase
69
+ end
70
+ end
71
+
72
+ Then { worker.product == 'SO' }
73
+ And { worker.product == 'LA' }
74
+ And { worker.product == 'TI' }
75
+ And { worker.product.nil? }
76
+ end
77
+ end
78
+
79
+ context 'illegal usage' do
80
+ context 'with no argument and arity > 0' do
81
+ Given(:invocation) do
82
+ -> { source_worker { |v| v * 2 } }
83
+ end
84
+ Then { expect(invocation).to raise_error(/arity == 0/) }
85
+ end
86
+ end
87
+
88
+ context 'with argument' do
89
+ context 'and arity == 0' do
90
+ Given(:invocation) do
91
+ -> { source_worker([]) { :boo } }
92
+ end
93
+ Then { expect(invocation).to raise_error(/arity == 1/) }
94
+ end
95
+ context 'and arity > 1' do
96
+ Given(:invocation) do
97
+ -> { source_worker([]) { |p, q| :boo } }
98
+ end
99
+ Then { expect(invocation).to raise_error(/arity == 1/) }
100
+ end
101
+ end
102
+ end
103
+
104
+ describe '#relay_worker' do
105
+ context 'normal usage' do
106
+ Given(:source) { source_worker %w[better stronger faster] }
107
+ Given(:relay) do
108
+ relay_worker { |v| v.gsub(/t/, '+') }
109
+ end
110
+
111
+ When { source | relay }
112
+
113
+ Then { relay.product == 'be++er' }
114
+ And { relay.product == 's+ronger' }
115
+ And { relay.product == 'fas+er' }
116
+ And { relay.product.nil? }
117
+ end
118
+
119
+ context 'illegal usage' do
120
+ context 'arity == 0' do
121
+ Given(:invocation) do
122
+ -> { relay_worker() { :foo } }
123
+ end
124
+
125
+ Then { expect(invocation).to raise_error(/arity == 1/) }
126
+ end
127
+ end
128
+ end
129
+
130
+ describe '#side_worker' do
131
+ context 'normal usage' do
132
+ Given(:source) { source_worker (0..2) }
133
+ Given(:side_effect) { [] }
134
+ Given(:worker) do
135
+ side_effect
136
+ side_worker { |v| side_effect << v * 2 }
137
+ end
138
+
139
+ When { source | worker }
140
+
141
+ Then { worker.product == 0 }
142
+ And { worker.product == 1 }
143
+ And { worker.product == 2 }
144
+ And { worker.product.nil? }
145
+ And { side_effect == [0, 2, 4] }
146
+ end
147
+
148
+ context 'illegal usage' do
149
+ context 'arity == 0' do
150
+ Given(:invocation) do
151
+ -> { side_worker() { :foo } }
152
+ end
153
+
154
+ Then { expect(invocation).to raise_error(/arity == 1/) }
155
+ end
156
+ end
157
+ end
158
+
159
+ describe '#filter_worker' do
160
+ context 'normal usage' do
161
+ Given(:source) { source_worker (1..3) }
162
+
163
+ When { source | filter }
164
+
165
+ Given(:filter) do
166
+ filter_worker { |v| v % 2 == 0 }
167
+ end
168
+
169
+ Then { filter.product == 2 }
170
+ And { expect(filter.product).to be_nil }
171
+ end
172
+
173
+ context 'illegal usage' do
174
+ context 'requires a callable' do
175
+ context 'with no arguments or block' do
176
+ Given(:invocation) { -> { filter_worker } }
177
+ Then { expect(invocation).to raise_error(/supply a callable/) }
178
+ end
179
+
180
+ context 'with no callable' do
181
+ Given(:invocation) { -> { filter_worker :foo } }
182
+ Then { expect(invocation).to raise_error(/supply a callable/) }
183
+ end
184
+
185
+ context 'with callable arg' do
186
+ Given(:arg) { Proc.new { true } }
187
+ Given(:invocation) { -> { filter_worker arg } }
188
+ Then { expect(invocation).to_not raise_error }
189
+ end
190
+
191
+ context 'with both callable arg and block' do
192
+ Given(:arg) { Proc.new { true } }
193
+ Given(:invocation) { -> { filter_worker(arg) do false; end } }
194
+ Then { expect(invocation).to raise_error(/two callables/) }
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ describe '#batch_worker' do
201
+ Given(:source) { source_worker (0..7) }
202
+
203
+ context 'normal usage' do
204
+ When { source | worker }
205
+
206
+ context 'with specified "gathering" batch size' do
207
+ Given(:worker) do
208
+ batch_worker gathering: 3
209
+ end
210
+
211
+ Then { worker.product == [ 0, 1, 2 ] }
212
+ And { worker.product == [ 3, 4, 5 ] }
213
+ And { worker.product == [ 6, 7 ] }
214
+ And { worker.product.nil? }
215
+ end
216
+
217
+ context 'defaults to batch size of 1' do
218
+ Given(:source) { source_worker [8,9] }
219
+ Given(:worker) { batch_worker }
220
+
221
+ Then { worker.product == [ 8 ] }
222
+ And { worker.product == [ 9 ] }
223
+ And { worker.product.nil? }
224
+ end
225
+
226
+ context 'collects until condition' do
227
+ Given(:source) { source_worker (1..5) }
228
+ Given(:worker) do
229
+ batch_worker { |n| n % 2 == 0 }
230
+ end
231
+
232
+ Then { worker.product == [ 1, 2 ] }
233
+ And { worker.product == [ 3, 4 ] }
234
+ And { worker.product == [ 5 ] }
235
+ And { worker.product.nil? }
236
+ end
237
+ end
238
+
239
+ context 'illegal usage' do
240
+ context 'requires a callable' do
241
+ context 'with arity == 0' do
242
+ Given(:invocation) { -> { batch_worker() { :foo } } }
243
+ Then { expect(invocation).to raise_error(/arity == 1/) }
244
+ end
245
+
246
+ context 'with arity > 1' do
247
+ Given(:invocation) { -> { batch_worker() { |a,b| :foo } } }
248
+ Then { expect(invocation).to raise_error(/arity == 1/) }
249
+ end
250
+
251
+ end
252
+ end
253
+ end
254
+
255
+ end
256
+ end
@@ -1,9 +1,49 @@
1
1
  require 'spec_helper'
2
- require 'stepladder/worker'
3
2
 
4
3
  module Stepladder
5
4
  describe Worker do
6
- it { should respond_to(:product, :supplier, :supplier=) }
5
+ it { should respond_to(:product, :supplier, :supplier=, :"|") }
6
+
7
+ describe "readiness" do
8
+ context "with no supplier" do
9
+ context "with no task" do
10
+ it { should_not be_ready_to_work }
11
+ end
12
+ context "with a task which accepts a value" do
13
+ subject do
14
+ Worker.new { |value| value.to_s }
15
+ end
16
+ it { should_not be_ready_to_work }
17
+ end
18
+ context "with a task which doesn't accept a value" do
19
+ subject do
20
+ Worker.new { "foo" }
21
+ end
22
+ it { should be_ready_to_work }
23
+ end
24
+ end
25
+
26
+ context "with a supplier" do
27
+ before do
28
+ subject.supplier = Worker.new { "foofoo" }
29
+ end
30
+ context "with no task" do
31
+ it { should_not be_ready_to_work }
32
+ end
33
+ context "with a task which accepts a value" do
34
+ subject do
35
+ Worker.new { |value| value.upcase }
36
+ end
37
+ it { should be_ready_to_work }
38
+ end
39
+ context "with a task which doesn't accept a value" do
40
+ subject do
41
+ Worker.new { "bar" }
42
+ end
43
+ it { should be_ready_to_work }
44
+ end
45
+ end
46
+ end
7
47
 
8
48
  describe "can accept a task" do
9
49
  let(:result) { double }
@@ -47,7 +87,7 @@ module Stepladder
47
87
  supplier.stub(:product).and_return(result)
48
88
  subject.supplier = supplier
49
89
  def subject.task(value)
50
- handoff value
90
+ Fiber.yield value
51
91
  end
52
92
  end
53
93
  its(:product) { should be_copasetic }
@@ -74,13 +114,13 @@ module Stepladder
74
114
 
75
115
  end
76
116
 
77
- describe "= WORKER TYPES =" do
117
+ describe "= EXAMPLE WORKER TYPES =" do
78
118
 
79
119
  let(:source_worker) do
80
120
  Worker.new do
81
121
  numbers = (1..3).to_a
82
- until numbers.empty?
83
- handoff numbers.shift
122
+ while value = numbers.shift
123
+ Fiber.yield value
84
124
  end
85
125
  end
86
126
  end
@@ -166,31 +206,29 @@ module Stepladder
166
206
  end
167
207
  end
168
208
 
169
- describe "Also, there's a pipeline dsl:" do
170
- let(:subscribing_worker) { relay_worker }
171
- let(:pipeline) { source_worker | subscribing_worker }
209
+ end
172
210
 
173
- subject { pipeline }
211
+ describe "#|" do
212
+ Given(:source_worker) { Worker.new { :foo } }
213
+ Given(:subscribing_worker) { Worker.new { |v| "#{v}_bar".to_sym } }
174
214
 
175
- it "lets you daisy-chain workers using \"|\"" do
176
- subject.inspect
177
- subscribing_worker.supplier.should == source_worker
178
- end
179
- end
215
+ When(:pipeline) { source_worker | subscribing_worker }
180
216
 
217
+ Then { subscribing_worker.supplier == source_worker }
218
+ Then { pipeline.product == :foo_bar }
219
+ Then { pipeline == subscribing_worker }
181
220
  end
182
221
 
183
222
  describe "#product" do
184
- before do
185
- supplier.stub(:product).and_return(result)
186
- subject.supplier = supplier
187
- end
188
- let(:result) { :foo }
189
- let(:supplier) { double }
223
+ Given(:work_product) { :whatever }
224
+ Given { supplier.stub(:product).and_return(work_product) }
225
+ Given { subject.supplier = supplier }
226
+ Given(:supplier) { double }
227
+
228
+ context "resumes a fiber" do
229
+ Given { Fiber.any_instance.should_receive(:resume).and_return(work_product) }
190
230
 
191
- it "resumes a fiber" do
192
- Fiber.any_instance.should_receive(:resume).and_return(result)
193
- subject.product.should == result
231
+ Then { subject.product }
194
232
  end
195
233
  end
196
234
 
data/spec/spec_helper.rb CHANGED
@@ -1 +1,16 @@
1
+ require 'rspec/its'
2
+ require 'rspec/given'
3
+ require 'pry'
4
+
1
5
  $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
6
+
7
+ require 'stepladder'
8
+
9
+ RSpec.configure do |cfg|
10
+ cfg.expect_with :rspec do |c|
11
+ c.syntax = [:should, :expect]
12
+ end
13
+ cfg.mock_with :rspec do |c|
14
+ c.syntax = [:should, :expect]
15
+ end
16
+ end
data/stepladder.gemspec CHANGED
@@ -17,9 +17,9 @@ Gem::Specification.new do |s|
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_development_dependency 'cucumber'
21
- s.add_development_dependency 'rake'
22
- s.add_development_dependency 'rspec'
23
- s.add_development_dependency 'rspec-core'
24
- s.add_development_dependency 'aruba'
20
+ s.add_development_dependency 'rspec', '3.1.0'
21
+ s.add_development_dependency 'rspec-core', '3.1.2'
22
+ s.add_development_dependency 'rspec-its', '1.0.1'
23
+ s.add_development_dependency 'rspec-given', '3.8.0'
24
+ s.add_development_dependency 'pry'
25
25
  end
metadata CHANGED
@@ -1,141 +1,132 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stepladder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
5
- prerelease:
4
+ version: 0.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Joel Helbling
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-15 00:00:00.000000000 Z
11
+ date: 2018-03-12 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: cucumber
14
+ name: rspec
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '='
20
18
  - !ruby/object:Gem::Version
21
- version: '0'
19
+ version: 3.1.0
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - '='
28
25
  - !ruby/object:Gem::Version
29
- version: '0'
26
+ version: 3.1.0
30
27
  - !ruby/object:Gem::Dependency
31
- name: rake
28
+ name: rspec-core
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - '='
36
32
  - !ruby/object:Gem::Version
37
- version: '0'
33
+ version: 3.1.2
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - '='
44
39
  - !ruby/object:Gem::Version
45
- version: '0'
40
+ version: 3.1.2
46
41
  - !ruby/object:Gem::Dependency
47
- name: rspec
42
+ name: rspec-its
48
43
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
44
  requirements:
51
- - - ! '>='
45
+ - - '='
52
46
  - !ruby/object:Gem::Version
53
- version: '0'
47
+ version: 1.0.1
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
51
  requirements:
59
- - - ! '>='
52
+ - - '='
60
53
  - !ruby/object:Gem::Version
61
- version: '0'
54
+ version: 1.0.1
62
55
  - !ruby/object:Gem::Dependency
63
- name: rspec-core
56
+ name: rspec-given
64
57
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
58
  requirements:
67
- - - ! '>='
59
+ - - '='
68
60
  - !ruby/object:Gem::Version
69
- version: '0'
61
+ version: 3.8.0
70
62
  type: :development
71
63
  prerelease: false
72
64
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
65
  requirements:
75
- - - ! '>='
66
+ - - '='
76
67
  - !ruby/object:Gem::Version
77
- version: '0'
68
+ version: 3.8.0
78
69
  - !ruby/object:Gem::Dependency
79
- name: aruba
70
+ name: pry
80
71
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
72
  requirements:
83
- - - ! '>='
73
+ - - ">="
84
74
  - !ruby/object:Gem::Version
85
75
  version: '0'
86
76
  type: :development
87
77
  prerelease: false
88
78
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
79
  requirements:
91
- - - ! '>='
80
+ - - ">="
92
81
  - !ruby/object:Gem::Version
93
82
  version: '0'
94
- description: ! ' Stepladder grew out of experimentation with Ruby fibers, after readings
95
- Dave Thomas'' demo of Ruby fibers, wherein he created a pipeline of fiber processes,
83
+ description: " Stepladder grew out of experimentation with Ruby fibers, after readings
84
+ Dave Thomas' demo of Ruby fibers, wherein he created a pipeline of fiber processes,
96
85
  emulating the style and syntax of the *nix command line. I noticed that, courtesy
97
- of fibers'' extremely low surface area, fiber-to-fiber collaborators could operate
98
- with extremely low coupling. That was the original motivation for creating the framework. '
86
+ of fibers' extremely low surface area, fiber-to-fiber collaborators could operate
87
+ with extremely low coupling. That was the original motivation for creating the framework. "
99
88
  email:
100
89
  - joel@joelhelbling.com
101
90
  executables: []
102
91
  extensions: []
103
92
  extra_rdoc_files: []
104
93
  files:
105
- - .gitignore
94
+ - ".gitignore"
95
+ - ".travis.yml"
106
96
  - Gemfile
107
97
  - README.md
108
98
  - Rakefile
99
+ - docs/stepladder/worker.md
100
+ - lib/stepladder.rb
101
+ - lib/stepladder/dsl.rb
109
102
  - lib/stepladder/version.rb
110
103
  - lib/stepladder/worker.rb
111
104
  - pkg/.gitkeep
105
+ - spec/lib/stepladder/dsl_spec.rb
112
106
  - spec/lib/stepladder/worker_spec.rb
113
107
  - spec/spec_helper.rb
114
108
  - stepladder.gemspec
115
109
  homepage: http://github.com/joelhelbling/stepladder
116
110
  licenses: []
111
+ metadata: {}
117
112
  post_install_message:
118
113
  rdoc_options: []
119
114
  require_paths:
120
115
  - lib
121
116
  required_ruby_version: !ruby/object:Gem::Requirement
122
- none: false
123
117
  requirements:
124
- - - ! '>='
118
+ - - ">="
125
119
  - !ruby/object:Gem::Version
126
120
  version: '0'
127
121
  required_rubygems_version: !ruby/object:Gem::Requirement
128
- none: false
129
122
  requirements:
130
- - - ! '>='
123
+ - - ">="
131
124
  - !ruby/object:Gem::Version
132
125
  version: '0'
133
126
  requirements: []
134
127
  rubyforge_project: stepladder
135
- rubygems_version: 1.8.24
128
+ rubygems_version: 2.7.3
136
129
  signing_key:
137
- specification_version: 3
130
+ specification_version: 4
138
131
  summary: A ruby-fibers-based framework aimed at extremely low coupling.
139
- test_files:
140
- - spec/lib/stepladder/worker_spec.rb
141
- - spec/spec_helper.rb
132
+ test_files: []