stepladder 0.0.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []