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.
@@ -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