reduxco 1.0.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.
- 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
|