reduxco 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+ module Reduxco
2
+ # The current version of the Reduxco gem.
3
+ VERSION = '1.0.0'
4
+ 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