reduxco 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +24 -0
- data/README.rdoc +126 -0
- data/Rakefile +19 -0
- data/lib/reduxco.rb +10 -0
- data/lib/reduxco/callable_ref.rb +142 -0
- data/lib/reduxco/context.rb +243 -0
- data/lib/reduxco/context/callable_table.rb +81 -0
- data/lib/reduxco/context/callstack.rb +74 -0
- data/lib/reduxco/reduxer.rb +41 -0
- data/lib/reduxco/version.rb +4 -0
- data/spec/callable_ref_spec.rb +273 -0
- data/spec/callable_table_spec.rb +174 -0
- data/spec/callstack_spec.rb +128 -0
- data/spec/context_spec.rb +619 -0
- data/spec/rdoc_examples_spec.rb +46 -0
- data/spec/reduxer_spec.rb +88 -0
- data/spec/spec_helper.rb +17 -0
- metadata +69 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative '../callable_ref'
|
2
|
+
|
3
|
+
module Reduxco
|
4
|
+
class Context
|
5
|
+
# CallableTable is a 'private' helper class to Context which handles resolving
|
6
|
+
# CallableRef instances to their appropriate callables. This should not be
|
7
|
+
# used directly.
|
8
|
+
class CallableTable
|
9
|
+
|
10
|
+
# The constant returned by +resolve+ when resolution failure occurs.
|
11
|
+
# This constant can be multiply assigned to the same pattern as a
|
12
|
+
# normal resolution, but will assign nil into each value.
|
13
|
+
RESOLUTION_FAILURE = [nil,nil]
|
14
|
+
|
15
|
+
# Instantiate with list of callable maps.
|
16
|
+
def initialize(callable_map_list)
|
17
|
+
@table = Hash[flatten(callable_map_list).sort.reverse]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Resolves the given callref. The return value usually takes advantage of
|
21
|
+
# multiple assignment to dissect the return into the matching callref and
|
22
|
+
# found callable. however one can check for failed resolution simply by
|
23
|
+
# comparing the result to RESOLUTION_FAILURE.
|
24
|
+
#
|
25
|
+
# Note that if resolution fails, each value in the multiple assignment is
|
26
|
+
# given the value nil.
|
27
|
+
def resolve(callref)
|
28
|
+
if( callref.static? )
|
29
|
+
@table.include?(callref) ? [callref, @table[callref]] : RESOLUTION_FAILURE
|
30
|
+
else
|
31
|
+
@table.find(->{RESOLUTION_FAILURE}) {|refkey, callable| callref.include?(refkey)}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
alias_method :[], :resolve
|
35
|
+
|
36
|
+
# Returns true if the call with teh given callref exists.
|
37
|
+
def include?(callref)
|
38
|
+
!resolve(callref).last.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Given a static callref, resolves the next available shadowed callable
|
42
|
+
# above it. If the callref is dynamic, then an exception is thrown.
|
43
|
+
def resolve_super(callref)
|
44
|
+
if( callref.dynamic? )
|
45
|
+
raise ArgumentError, "Cannot resolve the 'super' of a dyanmic CallableReference.", caller
|
46
|
+
else
|
47
|
+
# This is really nice, but runs in O(n):
|
48
|
+
# @table.find(->{RESOLUTION_FAILURE}) {|refkey, callable| refkey.name == callref.name && refkey.depth < callref.depth}
|
49
|
+
# This is more performant on large tables O(1) but has the potential for a lot of recursion depth:
|
50
|
+
# if( callref.depth <= CallableRef::MIN_DEPTH )
|
51
|
+
# RESOLUTION_FAILURE
|
52
|
+
# else
|
53
|
+
# resolution = resolve(callref.pred)
|
54
|
+
# resolution == RESOLUTION_FAILURE ? resolve_super(callref.pred) : resolution
|
55
|
+
# end
|
56
|
+
# So we go for this imperative C-style flat iteration for O(1) and no recursion.
|
57
|
+
ref = callref
|
58
|
+
while( ref.depth > CallableRef::MIN_DEPTH )
|
59
|
+
ref = ref.pred
|
60
|
+
resolution = resolve(ref)
|
61
|
+
return resolution if(resolution != RESOLUTION_FAILURE)
|
62
|
+
end
|
63
|
+
RESOLUTION_FAILURE
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Flattens the given list of independent maps into a flat symbol table.
|
70
|
+
def flatten(callable_map_list)
|
71
|
+
callable_map_list.each_with_object({table:{}, depth:CallableRef::MIN_DEPTH}) do |map, memo|
|
72
|
+
map.each do |name, callable|
|
73
|
+
memo[:table][CallableRef.new(name, memo[:depth])] = callable
|
74
|
+
end
|
75
|
+
memo[:depth] += 1
|
76
|
+
end[:table]
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Reduxco
|
2
|
+
class Context
|
3
|
+
# Defines and implements a callstack interface.
|
4
|
+
#
|
5
|
+
# Callstacks are made of frames, and the top element of the stack is the
|
6
|
+
# current frame.
|
7
|
+
class Callstack
|
8
|
+
|
9
|
+
# Initialize an empty callstack. Optionally takes an array of frames,
|
10
|
+
# reading from top of the stack to the bottom.
|
11
|
+
def initialize(array=[])
|
12
|
+
@stack = array.reverse
|
13
|
+
end
|
14
|
+
|
15
|
+
# Pushes the given frame onto the callstack
|
16
|
+
def push(frame)
|
17
|
+
@stack.push(frame)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Pops the top frame from the callstack and returns it.
|
22
|
+
def pop
|
23
|
+
@stack.pop
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the element at the top of the stack.
|
27
|
+
def top
|
28
|
+
@stack.last
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns true if the callstack contains the given frame
|
32
|
+
def include?(frame)
|
33
|
+
@stack.include?(frame)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns teh callstack depth
|
37
|
+
def depth
|
38
|
+
@stack.length
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a Callstack instance for everything below the top of the stack.
|
42
|
+
def rest
|
43
|
+
self.class.new(@stack[0..-2].reverse)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a copy of this callstack.
|
47
|
+
def dup
|
48
|
+
self.class.new(@stack.dup.reverse)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the callstack in a form that looks like Ruby's caller method,
|
52
|
+
# so that it can be placed in exception backtraces. Typically one wants
|
53
|
+
# the top of the caller-style stack to be the trace to where Context#call was
|
54
|
+
# invoked in a caller, so this may be provided.
|
55
|
+
def to_caller(top=nil)
|
56
|
+
@stack.reverse.map {|frame| "#{self.class.name} frame: #{frame}"}.tap do |cc|
|
57
|
+
cc.unshift top.to_s unless top.nil?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Output the Callstack from top to bottom.
|
62
|
+
def to_s
|
63
|
+
@stack.reverse.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
# Inspect the Callstack, with the top frame first.
|
67
|
+
def inspect
|
68
|
+
@stack.reverse.inspect
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'context'
|
2
|
+
|
3
|
+
module Reduxco
|
4
|
+
# The primary public facing Reduxco class.
|
5
|
+
class Reduxer
|
6
|
+
|
7
|
+
# When given one or more maps of callables, instantiates with the given
|
8
|
+
# callable maps.
|
9
|
+
#
|
10
|
+
# When the first argument is a Context, it instantiates with a new Context
|
11
|
+
# that has the same callable maps.
|
12
|
+
def initialize(*args)
|
13
|
+
case args.first
|
14
|
+
when Context
|
15
|
+
@context = args.first.dup
|
16
|
+
else
|
17
|
+
@context = Context.new(*args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a reference to the enclosing Context. This is typically not
|
22
|
+
# needed, and its use is more often than not related to a client design
|
23
|
+
# mistake.
|
24
|
+
attr_reader :context
|
25
|
+
|
26
|
+
# Retrieves the value of the given refname. Both final and intermediate
|
27
|
+
# values are cached, so multiple calls have low overhead.
|
28
|
+
def call(refname=:app)
|
29
|
+
@context.call(refname)
|
30
|
+
end
|
31
|
+
alias_method :[], :call
|
32
|
+
alias_method :reduce, :call
|
33
|
+
|
34
|
+
# Acts as a copy constructor, giving a new Reduxer instantiated with the
|
35
|
+
# same arguments as this one.
|
36
|
+
def dup
|
37
|
+
self.class.new(@context)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Reduxco::CallableRef do
|
4
|
+
|
5
|
+
describe 'basic properites' do
|
6
|
+
|
7
|
+
it 'should error if the depth is 0 or less.' do
|
8
|
+
->{Reduxco::CallableRef.new(:foo, 0)}.should raise_error(IndexError)
|
9
|
+
->{Reduxco::CallableRef.new(:foo, -100)}.should raise_error(IndexError)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should be dynamic with a symbol name' do
|
13
|
+
ref = Reduxco::CallableRef.new(:foo)
|
14
|
+
ref.should be_dynamic
|
15
|
+
ref.should_not be_static
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should be static with a symbol name and depth' do
|
19
|
+
ref = Reduxco::CallableRef.new(:foo, 3)
|
20
|
+
ref.should be_static
|
21
|
+
ref.should_not be_dynamic
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should give the name' do
|
25
|
+
ref = Reduxco::CallableRef.new(:foo)
|
26
|
+
ref.name.should == :foo
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should give the depth' do
|
30
|
+
ref = Reduxco::CallableRef.new(:foo, 3)
|
31
|
+
ref.depth.should == 3
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should give nil for the depth of a dynamic ref' do
|
35
|
+
ref = Reduxco::CallableRef.new(:foo)
|
36
|
+
ref.depth.should be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should be immutable' do
|
40
|
+
ref = Reduxco::CallableRef.new(:foo)
|
41
|
+
ref.should_not respond_to(:name=)
|
42
|
+
ref.should_not respond_to(:depth=)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should accept non-symbol names' do
|
46
|
+
name = Object.new
|
47
|
+
ref = Reduxco::CallableRef.new(name)
|
48
|
+
ref.name.should == name
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should not change strings to symbols' do
|
52
|
+
ref = Reduxco::CallableRef.new('foo')
|
53
|
+
ref.name.should_not == :foo
|
54
|
+
ref.name.should == 'foo'
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should be a symantecially identical callref when a callref is given as the name' do
|
58
|
+
name = Object.new # Purposefully choose an object that, if duplicated, will give a differing equality.
|
59
|
+
callref = Reduxco::CallableRef.new(name, 3)
|
60
|
+
|
61
|
+
copyref = Reduxco::CallableRef.new(callref)
|
62
|
+
copyref.name.should == callref.name
|
63
|
+
copyref.depth.should == callref.depth
|
64
|
+
copyref.should == callref
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should override a passed callref\'s depth when a depth arg is given' do
|
68
|
+
name = Object.new # Purposefully choose an object that, if duplicated, will give a differing equality.
|
69
|
+
callref = Reduxco::CallableRef.new(name, 3)
|
70
|
+
|
71
|
+
copyref = Reduxco::CallableRef.new(callref, 8)
|
72
|
+
copyref.name.should == callref.name
|
73
|
+
copyref.depth.should == 8
|
74
|
+
copyref.should_not == callref
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'movement' do
|
80
|
+
|
81
|
+
describe 'succ' do
|
82
|
+
|
83
|
+
it 'should return a ref with the same name, but one level deeper' do
|
84
|
+
name = Object.new
|
85
|
+
ref = Reduxco::CallableRef.new(name, 10)
|
86
|
+
|
87
|
+
ref.succ.tap do |r|
|
88
|
+
r.name.should == name
|
89
|
+
r.depth.should == 11
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should raise an exception when dynamic' do
|
94
|
+
->{ Reduxco::CallableRef.new(:foo).succ }.should raise_error
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should alias next to succ' do
|
98
|
+
ref = Reduxco::CallableRef.new('foo')
|
99
|
+
ref.method(:next).should == ref.method(:succ)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'pred' do
|
105
|
+
|
106
|
+
it 'should return a ref with the same name, but one level higher' do
|
107
|
+
name = Object.new
|
108
|
+
ref = Reduxco::CallableRef.new(name, 10)
|
109
|
+
|
110
|
+
ref.pred.tap do |r|
|
111
|
+
r.name.should == name
|
112
|
+
r.depth.should == 9
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should raise an exception when stepping too low' do
|
117
|
+
name = Object.new
|
118
|
+
ref = Reduxco::CallableRef.new(name, 1)
|
119
|
+
|
120
|
+
->{ ref.pred }.should raise_error(IndexError)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'should raise an exception when dynamic' do
|
124
|
+
->{ Reduxco::CallableRef.new(:foo).pred }.should raise_error
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
describe 'equality' do
|
132
|
+
|
133
|
+
it 'should compute a predictable hash based on name' do
|
134
|
+
name = Object.new
|
135
|
+
ref1 = Reduxco::CallableRef.new(name)
|
136
|
+
ref2 = Reduxco::CallableRef.new(name)
|
137
|
+
refz = Reduxco::CallableRef.new(Object.new)
|
138
|
+
|
139
|
+
ref2.hash.should == ref1.hash
|
140
|
+
refz.hash.should_not == ref1.hash
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should compute a predictable hash based on depth' do
|
144
|
+
ref1 = Reduxco::CallableRef.new(:foo, 3)
|
145
|
+
ref2 = Reduxco::CallableRef.new(:foo, 3)
|
146
|
+
refy = Reduxco::CallableRef.new(:foo)
|
147
|
+
refz = Reduxco::CallableRef.new(:foo, 4)
|
148
|
+
|
149
|
+
ref2.hash.should == ref1.hash
|
150
|
+
refy.hash.should_not == ref1.hash
|
151
|
+
refz.hash.should_not == ref1.hash
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'should compute different hashes for a string and symbol name' do
|
155
|
+
Reduxco::CallableRef.new(:foo).hash.should_not == Reduxco::CallableRef.new('foo').hash
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'should compute static refs as included in a same-named dynamic ref' do
|
159
|
+
ref = Reduxco::CallableRef.new(:foo)
|
160
|
+
|
161
|
+
ref1 = Reduxco::CallableRef.new(:foo, 3)
|
162
|
+
ref2 = Reduxco::CallableRef.new(:foo, 4)
|
163
|
+
ref3 = Reduxco::CallableRef.new(:bar, 4)
|
164
|
+
ref4 = Reduxco::CallableRef.new(:foo)
|
165
|
+
|
166
|
+
ref.should include(ref1)
|
167
|
+
ref.should include(ref2)
|
168
|
+
ref.should_not include(ref3)
|
169
|
+
ref.should include(ref4)
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should compute dynamic refs as only equal to dynamic refs with the same name' do
|
173
|
+
ref = Reduxco::CallableRef.new(:foo)
|
174
|
+
|
175
|
+
ref1 = Reduxco::CallableRef.new(:foo)
|
176
|
+
ref2 = Reduxco::CallableRef.new(:foo, 4)
|
177
|
+
|
178
|
+
ref1.should == ref
|
179
|
+
ref2.should_not == ref
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should compute static refs as only equal to refs with the same name and depth' do
|
183
|
+
ref = Reduxco::CallableRef.new(:foo, 4)
|
184
|
+
|
185
|
+
ref1 = Reduxco::CallableRef.new(:foo)
|
186
|
+
ref2 = Reduxco::CallableRef.new(:foo, 3)
|
187
|
+
ref3 = Reduxco::CallableRef.new(:foo, 4)
|
188
|
+
|
189
|
+
ref1.should_not == ref
|
190
|
+
ref2.should_not == ref
|
191
|
+
ref3.should == ref
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'should not consider a string value of a ref equal to the ref' do
|
195
|
+
ref = Reduxco::CallableRef.new(:foo, 4)
|
196
|
+
ref.should_not == ref.to_s
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
describe 'sortability' do
|
202
|
+
|
203
|
+
it 'should not allow sorting of dynamic refs with static ones' do
|
204
|
+
refs = [[:foo,3], [:bar]].map {|args| Reduxco::CallableRef.new(*args)}
|
205
|
+
->{refs.sort}.should raise_error(ArgumentError)
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'should sort equivalently named refs of lower depth above those of higher depths' do
|
209
|
+
depths = [6,1,2,2,9]
|
210
|
+
refs = depths.map {|depth| Reduxco::CallableRef.new(:foo, depth)}
|
211
|
+
|
212
|
+
refs.sort.map {|ref| ref.depth}.should == depths.sort
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'should sort names of same depth when sortable' do
|
216
|
+
names = ['cdr', 'cons', 'car']
|
217
|
+
dyn_refs = names.map {|name| Reduxco::CallableRef.new(name)}
|
218
|
+
stc_refs = names.map {|name| Reduxco::CallableRef.new(name, 3)}
|
219
|
+
|
220
|
+
dyn_refs.sort.map {|ref| ref.name}.should == names.sort
|
221
|
+
stc_refs.sort.map {|ref| ref.name}.should == names.sort
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'should not reject unsortable names (just be ambiguous)' do
|
225
|
+
names = [3, nil, :foo, Object.new]
|
226
|
+
dyn_refs = names.map {|name| Reduxco::CallableRef.new(name)}
|
227
|
+
stc_refs = names.map {|name| Reduxco::CallableRef.new(name, 3)}
|
228
|
+
|
229
|
+
->{ dyn_refs.sort.map {|ref| ref.name} }.should_not raise_error
|
230
|
+
->{ stc_refs.sort.map {|ref| ref.name} }.should_not raise_error
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
describe 'coercion' do
|
236
|
+
|
237
|
+
before(:each) do
|
238
|
+
@dyn_ref = Reduxco::CallableRef.new(:foo)
|
239
|
+
@stc_ref = Reduxco::CallableRef.new(:foo, 3)
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'should convert to array' do
|
243
|
+
@dyn_ref.to_a.should == [@dyn_ref.name, @dyn_ref.depth]
|
244
|
+
@stc_ref.to_a.should == [@stc_ref.name, @stc_ref.depth]
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'should convert to hash' do
|
248
|
+
@dyn_ref.to_h.should == {name: @dyn_ref.name, depth: @dyn_ref.depth}
|
249
|
+
@stc_ref.to_h.should == {name: @stc_ref.name, depth: @stc_ref.depth}
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
describe 'string form' do
|
254
|
+
|
255
|
+
it 'should convert dynamic ref to string without depth' do
|
256
|
+
@dyn_ref.to_s.should include(@stc_ref.name.to_s)
|
257
|
+
@dyn_ref.to_s.should_not include(@stc_ref.depth.to_s)
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'should convert static ref to string' do
|
261
|
+
@stc_ref.to_s.should include(@stc_ref.name.to_s)
|
262
|
+
@stc_ref.to_s.should include(@stc_ref.depth.to_s)
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'should not convert to string a missing args splat like it had one' do
|
266
|
+
Reduxco::CallableRef.new([:foo,3]).to_s.should_not == Reduxco::CallableRef.new(:foo,3).to_s
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|