reduxco 1.0.0 → 1.0.1
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 +7 -0
- data/LICENSE.txt +1 -1
- data/README.rdoc +338 -37
- data/lib/reduxco/context.rb +18 -2
- data/lib/reduxco/context/callstack.rb +7 -0
- data/lib/reduxco/version.rb +1 -1
- data/spec/callstack_spec.rb +7 -0
- data/spec/context_spec.rb +14 -0
- data/spec/rdoc_examples_spec.rb +92 -1
- data/spec/spec_helper.rb +6 -1
- metadata +10 -14
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 66dee8a1deb9af6c20d475fb99e8e7548c5d2188
|
4
|
+
data.tar.gz: c0ddc9771da228833b264c798a3d532491e306fa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6804b82d9cefbe4d5a6439e1dbaf225230a89732544dbfc68ffcb45501f616c13ad2195fd66d57fd6e23ab2e76f3db08670b13624ef835120bea781578a2875e
|
7
|
+
data.tar.gz: 4298d0af571f7c98ab7018664791792b8da6f778b82169759d1d423361a35f683d71b7a25cd9d5561a1f0897a1bc44e9154aaea7d69b93f82c1f3f7cceb2e9e2
|
data/LICENSE.txt
CHANGED
data/README.rdoc
CHANGED
@@ -1,24 +1,221 @@
|
|
1
|
+
= Introduction to Reduxco
|
2
|
+
|
3
|
+
== What is Reduxco?
|
4
|
+
|
5
|
+
In a sentence, Reduxco is a general purpose graph reduction engine that is
|
6
|
+
perfect for pipelines and calculation engines.
|
7
|
+
|
8
|
+
At its core, Reduxco allows you to define interchangeable graph nodes and easily
|
9
|
+
evalute them, producing one or more results. Key features include:
|
10
|
+
* Interchangeable, modular nodes,
|
11
|
+
* Self-organizing graph,
|
12
|
+
* Lazy evaluation of nodes,
|
13
|
+
* Node value caching,
|
14
|
+
* Node Overriding and inheritance,
|
15
|
+
* Helpers for nesting, and
|
16
|
+
* Graph Introspection and Reflection.
|
17
|
+
|
18
|
+
== Why would I use Reduxco?
|
19
|
+
|
20
|
+
The prototypical example of graph reduction is in the reduction of expression
|
21
|
+
graphs, whereby each node represents one part of a larger calculation, and the
|
22
|
+
final result is the final product of the calculation.
|
23
|
+
|
24
|
+
But what Reduxco is best at are pipelines.
|
25
|
+
|
26
|
+
Consider Rack Middleware: it is a list of interchangeable black-box like
|
27
|
+
"nodes" that form a response generation pipeline.
|
28
|
+
Now consider this: it often becomes necessary to communicate intermediate
|
29
|
+
information between various components. This requires a sideband channel
|
30
|
+
(e.g. the rack env) to store this data. This has a couple of problems, including
|
31
|
+
a lack of uniform access to data (i.e. did it come from the pipeline or the
|
32
|
+
side-band data?). Even worse, a mixup in the order of middleware can cause
|
33
|
+
errors due to missing dependencies in the sideband channel.
|
34
|
+
|
35
|
+
Reduxco solves these problems for you, by self organizing the dependency graph
|
36
|
+
thanks to a lazy evaluation model that gets the value of each node as it is
|
37
|
+
needed.
|
38
|
+
|
39
|
+
== Where is Reduxco Being Used?
|
40
|
+
|
41
|
+
Reduxco is used to build the request/response pipelines for the Whitepages APIs;
|
42
|
+
it handles everything from taking of the initial request parameters, all the
|
43
|
+
way through generating and returning the final formatted output.
|
44
|
+
|
45
|
+
It's modularity and customizability are used to great effect, allowing us to
|
46
|
+
easily refactor or replace nodes to drastically change the result of a an API
|
47
|
+
call, without having to worry about its effect on the rest of the work the
|
48
|
+
request does.
|
49
|
+
|
50
|
+
== How do I use Reduxco?
|
51
|
+
|
52
|
+
At its core, Reduxco allows you to build a named set of "callables" (i.e. any
|
53
|
+
object that responds to the <code>call</code> method, such as a Proc), and
|
54
|
+
evaluate the values from one or more nodes.
|
55
|
+
|
56
|
+
To give the rich set of features Reduxco offers, callables must take a single
|
57
|
+
argument to their <code>call</code> method which contains a Reduxco::Context. The
|
58
|
+
Context is essentially a little runtime, responsible for dynamically routing
|
59
|
+
calls to dependencies, and caching the results.
|
60
|
+
|
61
|
+
Lastly, Reduxco pipelines are created via the Reduxco::Reduxer class.
|
62
|
+
|
63
|
+
Thus, as a contrived simpleexample, let's do a simple expression graph reduction on
|
64
|
+
the equation <code>(x+y) * (x+y)</code>, with the values of x and y generated
|
65
|
+
at random:
|
66
|
+
|
67
|
+
callables = {
|
68
|
+
x: ->(c){ rand(10) },
|
69
|
+
y: ->(c){ rand(10) },
|
70
|
+
sum: ->(c){ c[:x] + c[:y] },
|
71
|
+
result: ->(c){ c[:sum] * c[:sum] },
|
72
|
+
app: ->(c){ "For x=#{c[:x]} and y=#{c[:y]}, the result is #{c[:result]}." }
|
73
|
+
}
|
74
|
+
|
75
|
+
pipeline = Reduxco::Reduxer.new(callables)
|
76
|
+
output = pipeline.reduce
|
77
|
+
output.should == "For x=3 and y=6, the result is 81."
|
78
|
+
|
79
|
+
This example starts by defining named callables as Ruby blocks, and then creating
|
80
|
+
a Reduxco pipeline out of it. The call to <code>reduce</code> invokes calculation
|
81
|
+
of the <code>:app</code> node, which cascades into evaluation of the required
|
82
|
+
portions of the graph. Note that the result is the square of the sum,
|
83
|
+
demonstrating that the <code>:x</code> and <code>:y</code> nodes are only
|
84
|
+
evaluated once and cached.
|
85
|
+
|
86
|
+
= Example
|
87
|
+
|
88
|
+
This example demonstrates some more advanced features of Reduxco, such as
|
89
|
+
instrospection, yielding values, flow helpers, and overriding.
|
90
|
+
|
91
|
+
Consider the basic structure for an application with error
|
92
|
+
handling, and that has the following requirements:
|
93
|
+
* For the default gut implementation, we generate a random number.
|
94
|
+
* If that number is even, then we raise a RuntimeError.
|
95
|
+
* If that number is odd, we return the number.
|
96
|
+
|
97
|
+
Furthermore, the consumer of the pipeline must define its own error handling
|
98
|
+
strategy as so:
|
99
|
+
* The error handling layer should catch the error and substitute the error
|
100
|
+
message string as the result.
|
101
|
+
|
102
|
+
Lastly, to write tests against this, we nee to override the value to be an even
|
103
|
+
for one test, and an odd for the next test.
|
104
|
+
|
105
|
+
The finished code looks like this
|
106
|
+
|
107
|
+
# The base callables, probably served up from a factory.
|
108
|
+
base_callables = {
|
109
|
+
app: ->(c) do
|
110
|
+
c.inside(:error_handler) do
|
111
|
+
c[:value].even? ? raise(RuntimeError, "Even!") : c[:value]
|
112
|
+
end
|
113
|
+
end,
|
114
|
+
|
115
|
+
error_handler: ->(c) do
|
116
|
+
begin
|
117
|
+
c.yield
|
118
|
+
rescue => error
|
119
|
+
c.call(:onfailure){error} if c.include?(:onfailure)
|
120
|
+
end
|
121
|
+
end,
|
122
|
+
|
123
|
+
value: ->(c){ rand(100) },
|
124
|
+
}
|
125
|
+
|
126
|
+
# The contect specific eror handler implementation.
|
127
|
+
handler_callables = {
|
128
|
+
onfailure: ->(c){ c.yield.message }
|
129
|
+
}
|
130
|
+
|
131
|
+
# Test callables; overrieds the value to be an even value.
|
132
|
+
even_test_callables = {
|
133
|
+
value: ->(c){ 8 }
|
134
|
+
}
|
135
|
+
|
136
|
+
# Test callables: overrieds the value to be an odd value.
|
137
|
+
odd_test_callables = {
|
138
|
+
value: ->(c){ 13 }
|
139
|
+
}
|
140
|
+
|
141
|
+
# Test evens
|
142
|
+
pipeline = Reduxco::Reduxer.new(base_callables, handler_callables, even_test_callables)
|
143
|
+
pipeline.reduce.should == 'Even!'
|
144
|
+
|
145
|
+
# Test odds
|
146
|
+
pipeline = Reduxco::Reduxer.new(base_callables, handler_callables, odd_test_callables)
|
147
|
+
pipeline.reduce.should == 13
|
148
|
+
|
149
|
+
# Invoke with random result
|
150
|
+
pipeline = Reduxco::Reduxer.new(base_callables, handler_callables)
|
151
|
+
random_result = pipeline.reduce
|
152
|
+
|
153
|
+
There are a few features to note about this code, with each explained in more
|
154
|
+
detail below:
|
155
|
+
* When multiple callable maps are given during pipeline instantiation, the
|
156
|
+
Reduxco::Context dispatches the the right-most map with the needed callable. Not shown
|
157
|
+
here is the ability to call <code>c.super</code> to get the value for a given
|
158
|
+
callable in the next highest map.
|
159
|
+
* The error handler demonstrates the use of introspection via the Reduxco::Context#include?
|
160
|
+
method, which checks that a given name is available.
|
161
|
+
Not shown are several other inspection methods, including introspection
|
162
|
+
as to which nodes are evaluated.
|
163
|
+
* The error handler utilizes the Reduxco::Context#yield method, which
|
164
|
+
yields the value of the block provided on the associated Reduxco::Context#call
|
165
|
+
method, in this case the error passed to the
|
166
|
+
<code>:onfailure</code> callable when invoked in the error handler.
|
167
|
+
* The use of the convenience method Reduxco::Context#inside, which although is the
|
168
|
+
same as Reduxco::Context#call, expresses the meaning of the code better.
|
169
|
+
|
1
170
|
= Overview
|
2
171
|
|
3
|
-
Reduxco is a
|
4
|
-
|
5
|
-
|
172
|
+
Reduxco is a graph reduction engine on steroids. It allows the creation of
|
173
|
+
maps of callables to create a self-organizing, lazy evaluated pipeline.
|
174
|
+
|
175
|
+
The main two classes are the Reduxco::Reduxer class, which is used to instantiate
|
176
|
+
pipelines, and the Reduxco::Context class, which is coordinates communication
|
177
|
+
between the nodes.
|
6
178
|
|
7
|
-
|
8
|
-
intermediate calculations that have to be reused later, but unlike Rack Middleware,
|
9
|
-
Reduxco is self organizing based on the dependencies used by each piece.
|
179
|
+
== Callable
|
10
180
|
|
11
|
-
|
181
|
+
They key building block of Reduxco pipelines are named "callables", which become
|
182
|
+
the implementation logic for each node in the graph.
|
12
183
|
|
13
|
-
|
184
|
+
There is no specific callable class. To be a callable, an object need comply
|
185
|
+
with the following two rules:
|
186
|
+
1. The object must respond to the <code>call</code> method.
|
187
|
+
2. The <code>call</code> method must take a single argument, which is a
|
188
|
+
Reduxco::Context instance.
|
14
189
|
|
15
|
-
|
190
|
+
Most of the time, the standard Ruby Proc object is all that is necessary, but
|
191
|
+
there are many clever reasons why one may substitute in a specialty object
|
192
|
+
in its place.
|
16
193
|
|
17
|
-
|
18
|
-
custom class instances), and register them with a Reduxco::Context. Callables can then
|
19
|
-
used their Reduco::Context handle to refer to the callables they can depend on.
|
194
|
+
== Reduxco::Reduxer
|
20
195
|
|
21
|
-
|
196
|
+
The Reduxco::Reduxer class is used to instantiate new pipelines, and to get
|
197
|
+
values from the pipeline.
|
198
|
+
|
199
|
+
=== Pipeline Creation
|
200
|
+
|
201
|
+
Reduxco::Reduxer instances are created by passing one or more maps of callables
|
202
|
+
during instantiation.
|
203
|
+
|
204
|
+
Callable maps are usually Hash instance, whose keys are Symbol instances, and
|
205
|
+
values meet the requirements of callables.
|
206
|
+
|
207
|
+
If more than one map is provieded to the initializer, callables are resolved
|
208
|
+
to the right-most argument that defines it. This provides a mechanism for
|
209
|
+
overriding callables created by a factory to customize for your layer. See
|
210
|
+
the section on overriding below.
|
211
|
+
|
212
|
+
=== Pipeline Invocation
|
213
|
+
|
214
|
+
The resulting Pipeline instance is considered an immutable object whose values
|
215
|
+
are lazily evaluated as necessary and then cached. These values are extracted
|
216
|
+
via the Reduxco::Reduxer#reduce method.
|
217
|
+
|
218
|
+
For example, a simple pipeline can be instantiated with the following code:
|
22
219
|
|
23
220
|
map = {
|
24
221
|
sum: ->(c){ c[:x] + c[:y] },
|
@@ -26,24 +223,84 @@ For example, the addition of two numbers could be done as follows:
|
|
26
223
|
y: ->(c){ 5 }
|
27
224
|
}
|
28
225
|
|
29
|
-
|
226
|
+
pipeline = Reduxco::Reduxer.new(map)
|
227
|
+
sum = pipeline.reduce(:sum)
|
30
228
|
sum.should == 8
|
31
229
|
|
32
|
-
Note that
|
33
|
-
|
34
|
-
|
230
|
+
Note that while Reduxco::Reduxer#reduce can take any named callable as an
|
231
|
+
argument, it by default attempts to reduce the value of the <code>:app</code>
|
232
|
+
callable when called with no argument.
|
233
|
+
|
234
|
+
Thus, most practical pipelines define an <code>:app</code> callable and simply
|
235
|
+
call Reduxco::Reduxer#reduce without an explicit argument:
|
236
|
+
|
237
|
+
pipeline = Reduxco::Reduxer.new(app: ->(c){ "Hello World" })
|
238
|
+
result = pipeline.reduce
|
239
|
+
result.should == "Hello World"
|
240
|
+
|
241
|
+
== Reduxco::Context
|
242
|
+
|
243
|
+
The Reduxco::Context object is the workhorse of the pipeline. It is responsible
|
244
|
+
for communication between the node including invoking the correct callables as
|
245
|
+
necessary and caching their results.
|
35
246
|
|
36
|
-
|
247
|
+
The Reduxco::Context instance is passed into each node's callable when it is
|
248
|
+
invoked, allowing for a plethora of communication and helper methods to be
|
249
|
+
used by the callables. An overview of this functionality is presented below.
|
250
|
+
|
251
|
+
== Basic Calling
|
252
|
+
|
253
|
+
The most common use of the Reduxco::Context object is to retrieve values of
|
254
|
+
other callables. This is accomplished via one of two methods:
|
255
|
+
* Reduxco::Context#[], which is the preferred way to call due to readability.
|
256
|
+
* Reduxco::Context#call, which behaves exactly the same, but has the option of
|
257
|
+
taking a block that can be evaluated by the called callable (explained below)
|
258
|
+
|
259
|
+
Don't forget that callables are only evaluated once and then cached, so multiple
|
260
|
+
retrievals of complex computations are as efficient as possible.
|
261
|
+
|
262
|
+
== Yielding Values
|
263
|
+
|
264
|
+
Sometimes one needs to push values into a callable when it is called. A good
|
265
|
+
example of this are error handling hooks, which are invoked when an error is
|
266
|
+
caught, and must be passed the error for processing.
|
267
|
+
|
268
|
+
Reduxco::Context#yield provides functionality to do this, but allowing the
|
269
|
+
callable to execute the block passed into the associated Reduxco::Context#call
|
270
|
+
method, and retrieve its value.
|
271
|
+
|
272
|
+
For example, consider the following pipeline:
|
273
|
+
|
274
|
+
callables = {
|
275
|
+
app: ->(c){ c.call(:foo) {3+20} },
|
276
|
+
foo: ->(c){ c.yield + 100 }
|
277
|
+
}
|
278
|
+
|
279
|
+
pipeline = Reduxco::Reduxer.new(callables)
|
280
|
+
pipeline.reduce.should == 123
|
37
281
|
|
38
|
-
Of course, any object responding to <code>call</code> can be used as the values in
|
39
|
-
the map, so one could just as easily define a class with an instance method of
|
40
|
-
<code>call</code> on it instead of using Proc objects.
|
41
282
|
|
42
283
|
== Overriding and Super
|
43
284
|
|
44
|
-
|
45
|
-
|
46
|
-
|
285
|
+
=== Dynamic Dispatch
|
286
|
+
|
287
|
+
Resolution of callables for a node is done via a dynamic dispatch methodology
|
288
|
+
that is not all that different than dynamic method dispatch in object oriented
|
289
|
+
dynamic languages like Ruby.
|
290
|
+
|
291
|
+
The Reduxco::Context looks at its stack of callable maps, and tests each map
|
292
|
+
until if finds a matching callable. It then selects that callable, and
|
293
|
+
retrieves the associated value from the cache, evaluating the callable itself
|
294
|
+
if necessary.
|
295
|
+
|
296
|
+
=== Override
|
297
|
+
|
298
|
+
This dynamic dispatching can be used to override the callable for a node with
|
299
|
+
a new one at instantiation (thus shadowing the previous definition). This is
|
300
|
+
especially useful when the primary callables may be generated by a factory, but
|
301
|
+
some pipeline customization is needed at the client layer.
|
302
|
+
|
303
|
+
The following code shows a concise example of overriding:
|
47
304
|
|
48
305
|
map1 = {
|
49
306
|
message: ->(c){ 'Hello From Map 1' }
|
@@ -56,8 +313,16 @@ For example:
|
|
56
313
|
msg = Reduxco::Reduxer.new(map1, map2).reduce(:message)
|
57
314
|
msg.should == 'Hello From Map 2'
|
58
315
|
|
59
|
-
|
60
|
-
|
316
|
+
=== Super
|
317
|
+
|
318
|
+
As mentioned earlier, the dynamic dispatch model used by Reduxco acts a bit like
|
319
|
+
a dynamic object oriented language. The logical extension of this is to allow
|
320
|
+
for a shadowing callable to execute the callable it shadows. This is easily
|
321
|
+
done via the Reduxco::Context#super method, which tells the dynamic dispatcher
|
322
|
+
to call the callable for the same node name, starting with the map "above" you
|
323
|
+
in the stack.
|
324
|
+
|
325
|
+
The following example shows a call to super in the override:
|
61
326
|
|
62
327
|
map1 = {
|
63
328
|
message: ->(c){ 'Hello From Map 1' }
|
@@ -67,17 +332,19 @@ Context#super. For example:
|
|
67
332
|
message: ->(c){ c.super + ' and Hello From Map 2' }
|
68
333
|
}
|
69
334
|
|
70
|
-
msg = Reduxco::
|
335
|
+
msg = Reduxco::Reduxer.new(map1, map2).reduce(:message)
|
71
336
|
msg.should == 'Hello From Map 1 and Hello From Map 2'
|
72
337
|
|
73
|
-
==
|
338
|
+
== Instrospection
|
74
339
|
|
75
340
|
There are several introspection methods for making assertions about the
|
76
|
-
Reduxco::Context.
|
77
|
-
environment before proceeding.
|
341
|
+
Reduxco::Context. These are usually used by callables to inspect their
|
342
|
+
environment before proceeding down an execution path.
|
78
343
|
|
344
|
+
|
345
|
+
The primary introspection methods are as follows:
|
79
346
|
[Reduxco::Context#include?] Allows you to inspect if the Reduxco::Context
|
80
|
-
can resolve a given
|
347
|
+
can resolve a given node name if called.
|
81
348
|
[Reduxco::Context#completed?] Allows you to inspect if the callable associated
|
82
349
|
with a given block name has already been called;
|
83
350
|
useful for assertions about weak dependencies.
|
@@ -86,21 +353,55 @@ environment before proceeding.
|
|
86
353
|
|
87
354
|
== Before, After, and Inside
|
88
355
|
|
356
|
+
While not strictly necessary, it is often useful to control call flow with
|
357
|
+
a method call that is more expressive than <code>call</code>. In other words,
|
358
|
+
while these methods are trivially implementable with just <code>call</code>,
|
359
|
+
it is often more desireable for your code to more directly express your intent
|
360
|
+
as an author to help with readability and maintainability of your code.
|
361
|
+
|
362
|
+
Academically, the key characteristic in common to Reduxco::Context#before,
|
363
|
+
Reduxco::Context#after and Reduxco::Context#insid, is that they each allow for
|
364
|
+
easy expression of ordered flow control, but with the return value being that
|
365
|
+
of the callable initially called.
|
366
|
+
|
367
|
+
As a practical example, Reduxco::Context#inside is often used to insert a
|
368
|
+
genericized error handling node into the app node, as in the following code
|
369
|
+
listing:
|
370
|
+
|
371
|
+
callables = {
|
372
|
+
app: ->(c) do
|
373
|
+
c.inside(:error_handler) do
|
374
|
+
c[:result]
|
375
|
+
end
|
376
|
+
end,
|
377
|
+
|
378
|
+
error_handler: ->(c) do
|
379
|
+
begin
|
380
|
+
c.yield
|
381
|
+
rescue => error
|
382
|
+
c.call(:onfailure){error} if c.include?(:onfailure)
|
383
|
+
raise error
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
result: ->(c) do
|
388
|
+
# do something
|
389
|
+
end
|
390
|
+
}
|
391
|
+
|
89
392
|
= Contact
|
90
393
|
|
91
394
|
Jeff Reinecke <jreinecke@whitepages.com>
|
92
395
|
|
93
|
-
= Roadmap
|
94
|
-
|
95
|
-
TBD
|
96
|
-
|
97
396
|
= History
|
98
397
|
|
99
|
-
[1.0.0 - 2013-Apr
|
398
|
+
[1.0.0 - 2013-Apr-18] Initial Release.
|
399
|
+
[1.0.1 - 2013-Apr-18] Fixed a bug where calling c.yield in a block given to a
|
400
|
+
call would give a stack overflow.
|
100
401
|
|
101
402
|
= License
|
102
403
|
|
103
|
-
Copyright (c)
|
404
|
+
Copyright (c) 2013, WhitePages, Inc.
|
104
405
|
All rights reserved.
|
105
406
|
|
106
407
|
Redistribution and use in source and binary forms, with or without
|
data/lib/reduxco/context.rb
CHANGED
@@ -70,6 +70,7 @@ module Reduxco
|
|
70
70
|
@cache = {}
|
71
71
|
|
72
72
|
@block_association_cache = {}
|
73
|
+
@yield_frame_depth = 0
|
73
74
|
end
|
74
75
|
|
75
76
|
# Given a refname, call it for this context and return the result.
|
@@ -129,11 +130,19 @@ module Reduxco
|
|
129
130
|
|
130
131
|
# Yields to the block given to a #Context.call
|
131
132
|
def yield(*args)
|
132
|
-
block = block_for_frame(
|
133
|
+
block = block_for_frame(yield_frame)
|
133
134
|
if( block.nil? )
|
134
135
|
raise LocalJumpError, "No block given to yield to.", caller
|
135
136
|
else
|
136
|
-
|
137
|
+
begin
|
138
|
+
# If the block call has a context yield inside, resolve that one frame up.
|
139
|
+
# This turns out to be what we usually want as we are forwarding a
|
140
|
+
# yielded value in the call.
|
141
|
+
@yield_frame_depth += 1
|
142
|
+
block.call(*args)
|
143
|
+
ensure
|
144
|
+
@yield_frame_depth -= 1
|
145
|
+
end
|
137
146
|
end
|
138
147
|
end
|
139
148
|
|
@@ -147,6 +156,10 @@ module Reduxco
|
|
147
156
|
@callstack.top
|
148
157
|
end
|
149
158
|
|
159
|
+
def yield_frame
|
160
|
+
@callstack.peek(@yield_frame_depth)
|
161
|
+
end
|
162
|
+
|
150
163
|
# Returns a true value if the given refname is defined in this context.
|
151
164
|
#
|
152
165
|
# If given a CallableRef, it returns a true value if the reference is
|
@@ -221,6 +234,9 @@ module Reduxco
|
|
221
234
|
raise CyclicalError, "Cyclical dependency on #{frame.inspect} in #{@callstack.rest.top.inspect}", callstack.to_caller(caller[1])
|
222
235
|
end
|
223
236
|
|
237
|
+
# On a new call we start resolving yield blocks at the current frame, so reset to zero.
|
238
|
+
@yield_frame_depth = 0
|
239
|
+
|
224
240
|
# Recall from cache, or build if necessary.
|
225
241
|
unless( @cache.include?(frame) )
|
226
242
|
@block_association_cache[frame] = block
|
@@ -28,6 +28,13 @@ module Reduxco
|
|
28
28
|
@stack.last
|
29
29
|
end
|
30
30
|
|
31
|
+
# Returns the element at a given depth from the top of the stack.
|
32
|
+
#
|
33
|
+
# A depth of zero corresponds to the top of the stack.
|
34
|
+
def peek(depth)
|
35
|
+
@stack[-depth - 1]
|
36
|
+
end
|
37
|
+
|
31
38
|
# Returns true if the callstack contains the given frame
|
32
39
|
def include?(frame)
|
33
40
|
@stack.include?(frame)
|
data/lib/reduxco/version.rb
CHANGED
data/spec/callstack_spec.rb
CHANGED
@@ -24,6 +24,13 @@ describe Reduxco::Context::Callstack do
|
|
24
24
|
@stack.top.should == :top
|
25
25
|
end
|
26
26
|
|
27
|
+
it 'should allow peeking into the stack' do
|
28
|
+
@stack.peek(0).should == @stack.top
|
29
|
+
@stack.peek(@stack.depth).should be_nil
|
30
|
+
@stack.peek(1).should == :middle
|
31
|
+
@stack.peek(2).should == :bottom
|
32
|
+
end
|
33
|
+
|
27
34
|
it 'should pop' do
|
28
35
|
frame = @stack.pop
|
29
36
|
|
data/spec/context_spec.rb
CHANGED
@@ -610,6 +610,20 @@ describe Reduxco::Context do
|
|
610
610
|
context.call(:app).should == 25
|
611
611
|
end
|
612
612
|
|
613
|
+
it 'should allow forwarded yielded values to be handed down' do
|
614
|
+
$debug = true
|
615
|
+
value = []
|
616
|
+
|
617
|
+
context = Reduxco::Context.new(
|
618
|
+
app: ->(c){ c.call(:outter){ value } },
|
619
|
+
outter: ->(c){ c.call(:middle){ c.yield } },
|
620
|
+
middle: ->(c){ c.call(:inner){ c.yield } },
|
621
|
+
inner: ->(c){ c.yield + [:inner] }
|
622
|
+
)
|
623
|
+
|
624
|
+
context.call(:app).should == [:inner]
|
625
|
+
end
|
626
|
+
|
613
627
|
end
|
614
628
|
|
615
629
|
end
|
data/spec/rdoc_examples_spec.rb
CHANGED
@@ -4,6 +4,80 @@ describe 'RDoc Examples' do
|
|
4
4
|
|
5
5
|
describe 'README.rdoc' do
|
6
6
|
|
7
|
+
it 'should meet the math example' do
|
8
|
+
srand(5) # For test predictability
|
9
|
+
|
10
|
+
###
|
11
|
+
|
12
|
+
callables = {
|
13
|
+
x: ->(c){ rand(10) },
|
14
|
+
y: ->(c){ rand(10) },
|
15
|
+
sum: ->(c){ c[:x] + c[:y] },
|
16
|
+
result: ->(c){ c[:sum] * c[:sum] },
|
17
|
+
app: ->(c){ "For x=#{c[:x]} and y=#{c[:y]}, the result is #{c[:result]}." }
|
18
|
+
}
|
19
|
+
|
20
|
+
pipeline = Reduxco::Reduxer.new(callables)
|
21
|
+
output = pipeline.reduce
|
22
|
+
output.should == "For x=3 and y=6, the result is 81."
|
23
|
+
|
24
|
+
###
|
25
|
+
|
26
|
+
pipeline.reduce(:sum).should == 9
|
27
|
+
|
28
|
+
###
|
29
|
+
|
30
|
+
srand(Time.now.to_i) # And now randomize better again.
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should meet the error handling exampmle' do
|
34
|
+
# The base callables, probably served up from a factory.
|
35
|
+
base_callables = {
|
36
|
+
app: ->(c) do
|
37
|
+
c.inside(:error_handler) do
|
38
|
+
c[:value].even? ? raise(RuntimeError, "Even!") : c[:value]
|
39
|
+
end
|
40
|
+
end,
|
41
|
+
|
42
|
+
error_handler: ->(c) do
|
43
|
+
begin
|
44
|
+
c.yield
|
45
|
+
rescue => error
|
46
|
+
c.call(:onfailure){error} if c.include?(:onfailure)
|
47
|
+
end
|
48
|
+
end,
|
49
|
+
|
50
|
+
value: ->(c){ rand(100) },
|
51
|
+
}
|
52
|
+
|
53
|
+
# The contect specific eror handler implementation.
|
54
|
+
handler_callables = {
|
55
|
+
onfailure: ->(c){ c.yield.message }
|
56
|
+
}
|
57
|
+
|
58
|
+
# Test callables; overrieds the value to be an even value.
|
59
|
+
even_test_callables = {
|
60
|
+
value: ->(c){ 8 }
|
61
|
+
}
|
62
|
+
|
63
|
+
# Test callables: overrieds the value to be an odd value.
|
64
|
+
odd_test_callables = {
|
65
|
+
value: ->(c){ 13 }
|
66
|
+
}
|
67
|
+
|
68
|
+
# Test evens
|
69
|
+
pipeline = Reduxco::Reduxer.new(base_callables, handler_callables, even_test_callables)
|
70
|
+
pipeline.reduce.should == 'Even!'
|
71
|
+
|
72
|
+
# Test odds
|
73
|
+
pipeline = Reduxco::Reduxer.new(base_callables, handler_callables, odd_test_callables)
|
74
|
+
pipeline.reduce.should == 13
|
75
|
+
|
76
|
+
# Invoke with random result
|
77
|
+
pipeline = Reduxco::Reduxer.new(base_callables, handler_callables)
|
78
|
+
random_result = pipeline.reduce
|
79
|
+
end
|
80
|
+
|
7
81
|
it 'should obey basic context use' do
|
8
82
|
map = {
|
9
83
|
sum: ->(c){ c[:x] + c[:y] },
|
@@ -11,10 +85,27 @@ describe 'RDoc Examples' do
|
|
11
85
|
y: ->(c){ 5 }
|
12
86
|
}
|
13
87
|
|
14
|
-
|
88
|
+
pipeline = Reduxco::Reduxer.new(map)
|
89
|
+
sum = pipeline.reduce(:sum)
|
15
90
|
sum.should == 8
|
16
91
|
end
|
17
92
|
|
93
|
+
it 'should have a basic app pipeline example' do
|
94
|
+
pipeline = Reduxco::Reduxer.new(app: ->(c){ "Hello World" })
|
95
|
+
result = pipeline.reduce
|
96
|
+
result.should == "Hello World"
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should have a simple yield example' do
|
100
|
+
callables = {
|
101
|
+
app: ->(c){ c.call(:foo) {3+20} },
|
102
|
+
foo: ->(c){ c.yield + 100 }
|
103
|
+
}
|
104
|
+
|
105
|
+
pipeline = Reduxco::Reduxer.new(callables)
|
106
|
+
pipeline.reduce.should == 123
|
107
|
+
end
|
108
|
+
|
18
109
|
it 'should override/shadow' do
|
19
110
|
map1 = {
|
20
111
|
message: ->(c){ 'Hello From Map 1' }
|
data/spec/spec_helper.rb
CHANGED
@@ -7,7 +7,11 @@ require 'reduxco'
|
|
7
7
|
|
8
8
|
# Require the debugger, if present.
|
9
9
|
begin
|
10
|
-
|
10
|
+
if( RUBY_VERSION < '2.0.0' )
|
11
|
+
require 'debugger'
|
12
|
+
else
|
13
|
+
require 'byebug'
|
14
|
+
end
|
11
15
|
rescue LoadError
|
12
16
|
module Kernel
|
13
17
|
def debugger(*args, &block)
|
@@ -15,3 +19,4 @@ rescue LoadError
|
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
22
|
+
|
metadata
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reduxco
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.1
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Jeff Reinecke
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-04
|
11
|
+
date: 2013-12-04 00:00:00.000000000 Z
|
13
12
|
dependencies: []
|
14
|
-
description:
|
15
|
-
those
|
16
|
-
|
13
|
+
description: |-
|
14
|
+
Reduxco is a general purpose graph reduction calculation engine for those
|
17
15
|
non-linear dependency flows that normal pipelines and Rack Middleware-like
|
18
|
-
|
19
|
-
architectures can''t do cleanly.'
|
16
|
+
architectures can't do cleanly.
|
20
17
|
email:
|
21
18
|
- jreinecke@whitepages.com
|
22
19
|
executables: []
|
@@ -44,26 +41,25 @@ files:
|
|
44
41
|
homepage: https://github.com/whitepages/reduxco
|
45
42
|
licenses:
|
46
43
|
- BSD
|
44
|
+
metadata: {}
|
47
45
|
post_install_message:
|
48
46
|
rdoc_options: []
|
49
47
|
require_paths:
|
50
48
|
- lib
|
51
49
|
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
-
none: false
|
53
50
|
requirements:
|
54
|
-
- -
|
51
|
+
- - '>='
|
55
52
|
- !ruby/object:Gem::Version
|
56
53
|
version: '0'
|
57
54
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
-
none: false
|
59
55
|
requirements:
|
60
|
-
- -
|
56
|
+
- - '>='
|
61
57
|
- !ruby/object:Gem::Version
|
62
58
|
version: '0'
|
63
59
|
requirements: []
|
64
60
|
rubyforge_project:
|
65
|
-
rubygems_version: 1.
|
61
|
+
rubygems_version: 2.1.11
|
66
62
|
signing_key:
|
67
|
-
specification_version:
|
63
|
+
specification_version: 4
|
68
64
|
summary: A graph reduction calculation engine.
|
69
65
|
test_files: []
|