order_tree 0.0.2
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/.DS_Store +0 -0
- data/.gitignore +5 -0
- data/.yardoc/checksums +5 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/Gemfile +4 -0
- data/README.md +26 -0
- data/Rakefile +39 -0
- data/lib/order_tree.rb +6 -0
- data/lib/order_tree/order_tree.rb +319 -0
- data/lib/order_tree/order_tree_node.rb +66 -0
- data/lib/order_tree/unique_proxy.rb +96 -0
- data/lib/order_tree/version.rb +3 -0
- data/order_tree.gemspec +26 -0
- data/spec/order_tree_spec.rb +338 -0
- data/spec/spec_helper.rb +35 -0
- metadata +110 -0
data/.DS_Store
ADDED
Binary file
|
data/.yardoc/checksums
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
lib/order_tree.rb 1587f76284fef215167a4b0970a1411503f6543f
|
2
|
+
lib/order_tree/version.rb 12d621b5c0101d185a48bac5d5ad897d7b83053b
|
3
|
+
lib/order_tree/unique_proxy.rb 632e468875c365d94b069f5c7e4d3d10030ccb55
|
4
|
+
lib/order_tree/order_tree.rb 3dc2a2a576334be3fe9cd19fe999b310fbfd7ea6
|
5
|
+
lib/order_tree/order_store.rb cfef2da2707e61ed0637a48e9a0c1a55d9578b1c
|
Binary file
|
data/.yardoc/proxy_types
ADDED
Binary file
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#Ordered Tree
|
2
|
+
|
3
|
+
This is a nested hash / unbalance tree structure which iterates over all of
|
4
|
+
its available values in insertion order.
|
5
|
+
|
6
|
+
##Like a Normal Hash?
|
7
|
+
|
8
|
+
Exactly like a normal hash. Althought with a standard Ruby Hash it's impossible to
|
9
|
+
know whether or not `hash["a"]["b"]` was inserted before or after `hash["b"]["c"]`
|
10
|
+
because each individual Hash maintains it's own insertion order.
|
11
|
+
|
12
|
+
Now you can know. If you need to. The main thing you gain with this over an
|
13
|
+
Array is the ability to prune or transplant multiple values at the same
|
14
|
+
time by cutting on one of the branches.
|
15
|
+
|
16
|
+
##Caveat
|
17
|
+
|
18
|
+
Each value is actually stored in a proxy object which maintains a unique id.
|
19
|
+
This is necessary so that if you insert three `4`s into the tree you can tell
|
20
|
+
which one came first. This wouldn't actually be necessary in a C implementation,
|
21
|
+
or in one based on WeakRefs, but it works okay here.
|
22
|
+
|
23
|
+
You can generally treat the OrderTree exactly like a nested hash, but be aware
|
24
|
+
that the first and last methods (as well as the #each iterator) actually return
|
25
|
+
an OrderTreeNode and not the actual object stored at that location. (You can
|
26
|
+
get at it by using #orig on the returned value.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
8
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
9
|
+
spec.rspec_opts = "-d"
|
10
|
+
end
|
11
|
+
|
12
|
+
RSpec::Core::RakeTask.new(:spec_with_report) do |spec|
|
13
|
+
spec.fail_on_error = false
|
14
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
15
|
+
spec.rspec_opts = "--format html --out report/test_report.html"
|
16
|
+
end
|
17
|
+
|
18
|
+
task :report do
|
19
|
+
Dir.mkdir "report" unless File.exists? "report"
|
20
|
+
Dir.mkdir "report/profile" unless File.exists? "report/profile"
|
21
|
+
File.open "report/index.html","w" do |f|
|
22
|
+
f.write <<-HTML
|
23
|
+
<html>
|
24
|
+
<body>
|
25
|
+
<h1> Status Report </h1>
|
26
|
+
<a href="coverage/index.html"> Coverage </a>
|
27
|
+
<a href="profile/profile.html"> Speed Profile </a>
|
28
|
+
<a href="test_report.html"> Test Report </a>
|
29
|
+
</body>
|
30
|
+
</html>
|
31
|
+
HTML
|
32
|
+
end
|
33
|
+
ENV["REPORT"] = "1"
|
34
|
+
Rake::Task[:spec_with_report].invoke
|
35
|
+
ENV["REPORT"] = ""
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
data/lib/order_tree.rb
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module OrderTree
|
4
|
+
|
5
|
+
# A unbalance tree / nested hash type structure that implements #each and returns
|
6
|
+
# the values in the order in which they were inserted, regardless of depth
|
7
|
+
# It can mostly be treated as a nested hash - but #each will return a #path
|
8
|
+
# to the values it iterates
|
9
|
+
class OrderTree
|
10
|
+
include Enumerable
|
11
|
+
include ProxyOperator
|
12
|
+
|
13
|
+
class PathNotFound < StandardError; end
|
14
|
+
|
15
|
+
attr_accessor :first, :root
|
16
|
+
attr_reader :last
|
17
|
+
|
18
|
+
# Create a new OrderTree
|
19
|
+
# @param [Hash] constructor - a hash literal for the initial values
|
20
|
+
# @param [OrderTree] OrderTree - the root tree object.
|
21
|
+
# @note The order of insertion might not be what you would expect for multi-
|
22
|
+
# level hash literals. The most deeply nested values will be inserted FIRST.
|
23
|
+
def initialize(constructor = {}, root = nil)
|
24
|
+
@_delegate_hash = {}
|
25
|
+
self.root = root || self
|
26
|
+
constructor.each_with_object(self) do |(k,v),memo|
|
27
|
+
memo[k] = v
|
28
|
+
end
|
29
|
+
self.default = OrderTreeNode.new(nil,self) if self.root
|
30
|
+
_delegate_hash.default = self.default
|
31
|
+
end
|
32
|
+
|
33
|
+
def last= obj
|
34
|
+
if @last.nil?
|
35
|
+
@first = obj
|
36
|
+
@last = obj
|
37
|
+
else
|
38
|
+
@last = obj
|
39
|
+
end
|
40
|
+
end
|
41
|
+
protected :last=
|
42
|
+
|
43
|
+
# Set the default value for the tree
|
44
|
+
# This place the default object behind a UniqueProxy. The default
|
45
|
+
# is not remembered within the order
|
46
|
+
# @param [Object] obj
|
47
|
+
def default= obj
|
48
|
+
unless proxy? obj
|
49
|
+
obj = OrderTreeNode.new(obj,self)
|
50
|
+
end
|
51
|
+
@default = obj
|
52
|
+
_delegate_hash.default = @default
|
53
|
+
_delegate_hash.each_value do |v|
|
54
|
+
if v.is_a? OrderTree
|
55
|
+
v.default= obj
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_reader :default
|
61
|
+
|
62
|
+
# Yields each path, value pair of the OrderTree in the order in which it was
|
63
|
+
# inserted
|
64
|
+
# @return [Enumerator]
|
65
|
+
# @yield [path, value] yields the path (as an array) to a value
|
66
|
+
# @yieldparam [Array] path the path as an array
|
67
|
+
# @yieldparam [Object] value the original object stored in the OrderTree
|
68
|
+
def each_pair
|
69
|
+
return enum_for(:each_pair) unless block_given?
|
70
|
+
self.each do |c|
|
71
|
+
yield c.path, c.orig
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Array] the results of calling {#each}.to_a
|
76
|
+
def order
|
77
|
+
each_pair.to_a
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Enumerator] collection of OrderTreeNode objects in insertion order
|
81
|
+
def each
|
82
|
+
return enum_for(:each) unless block_given?
|
83
|
+
c = root.first
|
84
|
+
while c
|
85
|
+
yield c
|
86
|
+
c = c.next
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Enumerator] collection of paths in insertion order
|
91
|
+
def each_path
|
92
|
+
return enum_for(:each_path) unless block_given?
|
93
|
+
self.each do |v|
|
94
|
+
yield v.path
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [Enumerator] collection of leaf values in insertion order
|
99
|
+
def each_leaf
|
100
|
+
return enum_for(:each_leaf) unless block_given?
|
101
|
+
self.each do |v|
|
102
|
+
unless v.is_a? OrderTree
|
103
|
+
yield v.orig
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Enumerator] collection of values in insertion order, including subnodes
|
109
|
+
def each_value
|
110
|
+
return enum_for(:each_value) unless block_given?
|
111
|
+
self.each do |v|
|
112
|
+
yield v.orig
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Finds the first occurence of an object in the tree
|
117
|
+
# @param [Object] val the value to search for this is a O(n) operation
|
118
|
+
# @param [Block] block if both a block and val are passed, the
|
119
|
+
# block will be evalueated first, then the value
|
120
|
+
# @return [Array, false, true] path to value, false if not found, or true if value == self
|
121
|
+
# @yield [value] use the block to perform more complicated tests
|
122
|
+
# @yieldreturn [Boolean]
|
123
|
+
# @raises [ArgumentError] if neither a val nor block is given.
|
124
|
+
# @note You cannot search for a nil value by passing nil as the value. You must
|
125
|
+
# pass a block that compares for nil
|
126
|
+
# @note This methods does NOT guarantee that you will recieve the result
|
127
|
+
# in inserted order
|
128
|
+
def path val = nil, &block
|
129
|
+
begin
|
130
|
+
path! val, &block
|
131
|
+
rescue PathNotFound
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Finds the first occurence of a specific insertion in the tree
|
137
|
+
# @param [OrderTreeNode] val the proxy object to find
|
138
|
+
def strict_path val = nil
|
139
|
+
begin
|
140
|
+
strict_path! val
|
141
|
+
rescue PathNotFound
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Raises an exception if it can't determine the path
|
147
|
+
# @see {#path}
|
148
|
+
def path! val = nil, &block
|
149
|
+
raise ArgumentError, "requires search value or block" if val.nil? and block.nil?
|
150
|
+
__path(val, false, [], &block)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Raises an exception if it can't find the object
|
154
|
+
# @see {#strict_path}
|
155
|
+
def strict_path! val
|
156
|
+
unless ProxyOperator.proxy? val
|
157
|
+
raise ArgumentError, "OrderTreeNode expected for strict_path"
|
158
|
+
end
|
159
|
+
__path(val, true)
|
160
|
+
end
|
161
|
+
|
162
|
+
# @private
|
163
|
+
def __path val = nil, strict = false, key_path = [], &block
|
164
|
+
op = strict ? :equal? : :==
|
165
|
+
return true if (yield(val) unless block.nil?) or self.__send__ op, val
|
166
|
+
_delegate_hash.each do |k,v|
|
167
|
+
if (yield v unless block.nil?) or v.__send__ op, val
|
168
|
+
key_path << k
|
169
|
+
break
|
170
|
+
elsif v.respond_to? :__path, true
|
171
|
+
begin
|
172
|
+
v.__path(val, strict, key_path, &block)
|
173
|
+
key_path.unshift(k)
|
174
|
+
break
|
175
|
+
rescue PathNotFound
|
176
|
+
# try the next one
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
raise PathNotFound, "Couldn't find path to #{val} in #{self.to_s}" if key_path.empty?
|
181
|
+
key_path
|
182
|
+
end
|
183
|
+
private :__path
|
184
|
+
|
185
|
+
# @private
|
186
|
+
# @api Delegate
|
187
|
+
def _delegate_hash
|
188
|
+
@_delegate_hash
|
189
|
+
end
|
190
|
+
private :_delegate_hash
|
191
|
+
|
192
|
+
def to_s
|
193
|
+
"#<#{self.class}:#{'0x%x' % self.__id__ << 1}>"
|
194
|
+
end
|
195
|
+
|
196
|
+
def inspect
|
197
|
+
_delegate_hash.inspect
|
198
|
+
end
|
199
|
+
|
200
|
+
# @param [OrderTree] other
|
201
|
+
# @return [true] if self and other do not share all leaves or were inserted in a different order
|
202
|
+
def != other
|
203
|
+
!(self == other)
|
204
|
+
end
|
205
|
+
|
206
|
+
# @param [OrderTree] other
|
207
|
+
# @return [true] if self and other share all of their leaves and were inserted in the same order
|
208
|
+
def == other
|
209
|
+
return false if other.class != self.class
|
210
|
+
ov,sv = other.each_value.to_a, self.each_value.to_a
|
211
|
+
t_arr = ov.size > sv.size ? (ov.zip(sv)) : (sv.zip(ov))
|
212
|
+
t_arr.each do |sv, ov|
|
213
|
+
return false if sv.nil? ^ ov.nil?
|
214
|
+
if [ov,sv].map { |v| v.respond_to? :_delegate_hash, true}.all?
|
215
|
+
return false unless ov.send(:_delegate_hash) == sv.send(:_delegate_hash)
|
216
|
+
elsif ov != sv
|
217
|
+
return false
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# @param [OrderTree] other
|
223
|
+
# @return [true] if self and other contains the same key/path pairs as leaf nodes, regardless of their
|
224
|
+
# order of insertion
|
225
|
+
def contents_equal? other
|
226
|
+
return false if other.class != self.class
|
227
|
+
ov,sv = other.each_leaf.to_a.sort, self.each_leaf.to_a.sort
|
228
|
+
t_arr = ov.size > sv.size ? (ov.zip(sv)) : (sv.zip(ov))
|
229
|
+
t_arr.each do |sv,ov|
|
230
|
+
return false unless sv == ov
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns the UniqueProxy at path.
|
235
|
+
# @param [Array] path the path to return
|
236
|
+
# @return [OrdeTreeNode] either OrderTreeNode default or the OrderTreeNode at path
|
237
|
+
def at *paths
|
238
|
+
t = self
|
239
|
+
paths.each do |p|
|
240
|
+
if t.respond_to? :at # if it's a branch nodejkeep looking
|
241
|
+
t = (t.__send__ :_delegate_hash)[p]
|
242
|
+
else
|
243
|
+
#other wise resturn the default
|
244
|
+
return self.root.default
|
245
|
+
end
|
246
|
+
end
|
247
|
+
t
|
248
|
+
end
|
249
|
+
|
250
|
+
# Return the object stored at path
|
251
|
+
# @param [Array] path you may specify the path as either an array, or
|
252
|
+
# by using the nested hash syntax
|
253
|
+
# @example
|
254
|
+
# obj = OrderTree.new( :first => { :second => { :third => 3 }})
|
255
|
+
# obj[:first, :second, :third] #=> 3
|
256
|
+
# obj[:first][:second][:third] #=> 3
|
257
|
+
def [] *paths
|
258
|
+
t = self.at *paths
|
259
|
+
t.orig
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
# @private
|
264
|
+
def _find_delegate_hash *paths
|
265
|
+
under = self
|
266
|
+
paths.each do |k|
|
267
|
+
under = under.instance_eval do
|
268
|
+
unless self.respond_to? :_delegate_hash, true
|
269
|
+
raise NoMethodError, "Can't reifiy tree leaf on access to #{paths}"
|
270
|
+
end
|
271
|
+
h = self.send :_delegate_hash
|
272
|
+
if h.has_key? k and k != paths.last
|
273
|
+
h[k]
|
274
|
+
else
|
275
|
+
break h
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
under
|
280
|
+
end
|
281
|
+
private :_find_delegate_hash
|
282
|
+
|
283
|
+
# Prune branch from the tree
|
284
|
+
# @param [Array] path
|
285
|
+
def delete *paths
|
286
|
+
under = _find_delegate_hash *paths
|
287
|
+
if under.has_key? paths.last
|
288
|
+
under[paths.last].remove
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Stores the value at path
|
293
|
+
# @param [Array] path
|
294
|
+
# @param [Object] value
|
295
|
+
def []= *paths, value
|
296
|
+
under = _find_delegate_hash *paths
|
297
|
+
|
298
|
+
if value.kind_of? Hash or value.kind_of? OrderTree
|
299
|
+
value = OrderTree.new(value, @root)
|
300
|
+
value.default= self.root.default
|
301
|
+
end
|
302
|
+
|
303
|
+
if under.has_key? paths.last # i am overwriting a path
|
304
|
+
under[paths.last].remove
|
305
|
+
end
|
306
|
+
|
307
|
+
under[paths.last] = OrderTreeNode.new(value, self)
|
308
|
+
under[paths.last].prev = root.last if root.last
|
309
|
+
root.last.next = under[paths.last] if root.last
|
310
|
+
root.last = under[paths.last]
|
311
|
+
|
312
|
+
#@order.push under[paths.last]
|
313
|
+
|
314
|
+
#puts "insertion of '#{value}' in #{self.to_s} -> #{@order.to_s} (id #{under[paths.last].unique_id})"
|
315
|
+
value
|
316
|
+
end
|
317
|
+
alias :store :[]=
|
318
|
+
end
|
319
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module OrderTree
|
2
|
+
class OrderTreeNode < UniqueProxy
|
3
|
+
attr_accessor :next, :prev
|
4
|
+
attr_accessor :tree
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def initialize obj, tree
|
8
|
+
super(obj)
|
9
|
+
@tree = tree
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove
|
13
|
+
prev_node = self.prev
|
14
|
+
next_node = self.next
|
15
|
+
self.next.prev = prev_node if self.next
|
16
|
+
self.prev.next = next_node if self.prev
|
17
|
+
|
18
|
+
if self.tree.root.first.equal? self
|
19
|
+
self.tree.root.first = self.next
|
20
|
+
end
|
21
|
+
|
22
|
+
# try this so that the node can remove
|
23
|
+
# itself fromt he tree
|
24
|
+
my_path = self.path
|
25
|
+
self.tree.instance_eval do
|
26
|
+
_delegate_hash.delete my_path.last
|
27
|
+
end
|
28
|
+
@path = nil
|
29
|
+
@tree = nil
|
30
|
+
@next = nil
|
31
|
+
@prev = nil
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def before other
|
36
|
+
(self <=> other) == -1 ? true : false
|
37
|
+
end
|
38
|
+
|
39
|
+
def after other
|
40
|
+
(self <=> other) == 1 ? true : false
|
41
|
+
end
|
42
|
+
|
43
|
+
def <=> other
|
44
|
+
if self.equal? other
|
45
|
+
return 0
|
46
|
+
else
|
47
|
+
p, n = self.prev, self.next
|
48
|
+
while p or n
|
49
|
+
return 1 if p.equal? other
|
50
|
+
return -1 if n.equal? other
|
51
|
+
p = p.prev if p
|
52
|
+
n = n.next if n
|
53
|
+
end
|
54
|
+
end
|
55
|
+
raise ::ArgumentError, "Cannot compare #{self} and #{other} because they are not in the same tree"
|
56
|
+
end
|
57
|
+
|
58
|
+
def path
|
59
|
+
@path || self.path!
|
60
|
+
end
|
61
|
+
|
62
|
+
def path!
|
63
|
+
@path = self.tree.root.strict_path! self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module OrderTree
|
4
|
+
|
5
|
+
# Mixin module that provides a proxy convience functions
|
6
|
+
module ProxyOperator
|
7
|
+
#@param [Object] obj object to test
|
8
|
+
#@return [Boolean] is object in fact a proxy?
|
9
|
+
def proxy? obj
|
10
|
+
!!(obj.instance_eval { @is_proxy })
|
11
|
+
rescue false
|
12
|
+
end
|
13
|
+
module_function :proxy?
|
14
|
+
|
15
|
+
#@param [Object] obj object to proxy
|
16
|
+
#@return [UniqueProxy] create a unique proxy over obj
|
17
|
+
def proxy obj
|
18
|
+
UniqueProxy.new obj
|
19
|
+
rescue false
|
20
|
+
end
|
21
|
+
module_function :proxy
|
22
|
+
end
|
23
|
+
|
24
|
+
# Simple Proxy for distinguishing between the insertions of two identical
|
25
|
+
# objects in an order tree. Assign a unique ID to any object passed through
|
26
|
+
# the proxy, so you can always find the same object, even if you move it
|
27
|
+
# around in the tree. It also enables you to tell the differen between two
|
28
|
+
# different insertions of the same singleton object.
|
29
|
+
class UniqueProxy < BasicObject
|
30
|
+
class << self
|
31
|
+
attr_accessor :verbose_inspect
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [Object] obj - the proxy target
|
35
|
+
def initialize obj
|
36
|
+
@is_proxy = true
|
37
|
+
@obj = obj
|
38
|
+
@uuid ||= ::SecureRandom.uuid
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String] the unique ID of the proxy
|
42
|
+
def unique_id
|
43
|
+
@uuid
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String] a string describing the proxy if UniqueProxy.verbose_inspect is not false
|
47
|
+
# otherwise calls #inspect on the proxied object
|
48
|
+
def inspect
|
49
|
+
if UniqueProxy.verbose_inspect
|
50
|
+
"#<#{UniqueProxy}::#{@uuid} => #{@obj.inspect}>"
|
51
|
+
else
|
52
|
+
@obj.inspect
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String] a eval-able string to create a new proxy over this proxied object
|
57
|
+
def to_s
|
58
|
+
if UniqueProxy.verbose_inspect
|
59
|
+
"proxy(#{@obj.to_s})"
|
60
|
+
else
|
61
|
+
@obj.to_s
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Is true only if the other object has the same unique_id as self
|
66
|
+
def equal? other
|
67
|
+
(@uuid == other.unique_id) rescue false
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Object] the unproxied target
|
71
|
+
def orig
|
72
|
+
@obj
|
73
|
+
end
|
74
|
+
|
75
|
+
# Dispatches methods calls to proxy target
|
76
|
+
def method_missing(method, *args, &block)
|
77
|
+
@obj.__send__ method, *args, &block
|
78
|
+
end
|
79
|
+
|
80
|
+
# @private
|
81
|
+
def !
|
82
|
+
!@obj
|
83
|
+
end
|
84
|
+
|
85
|
+
# @private
|
86
|
+
def == arg
|
87
|
+
@obj == arg
|
88
|
+
end
|
89
|
+
|
90
|
+
# @private
|
91
|
+
def != arg
|
92
|
+
@obj != arg
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
data/order_tree.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "order_tree/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "order_tree"
|
7
|
+
s.version = OrderTree::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Stephen Prater"]
|
10
|
+
s.email = ["stephenp@agrussell.com"]
|
11
|
+
s.homepage = "http://github.com/stephenprater/order_tree"
|
12
|
+
s.summary = %q{An unbalanced tree / nested hash which remember insertion order}
|
13
|
+
s.description = %q{Use OrderTree when you need both insertion order access and nested hash path style access}
|
14
|
+
|
15
|
+
s.rubyforge_project = "order_tree"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_development_dependency('rspec')
|
23
|
+
s.add_development_dependency('simplecov')
|
24
|
+
s.add_development_dependency('ruby-prof')
|
25
|
+
s.add_development_dependency('ruby-debug19')
|
26
|
+
end
|
@@ -0,0 +1,338 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
require 'order_tree'
|
6
|
+
|
7
|
+
describe OrderTree::UniqueProxy do
|
8
|
+
before :all do
|
9
|
+
Object.send :include, OrderTree::ProxyOperator
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can tell apart things that are the same" do
|
13
|
+
(4 == 4).should eq true
|
14
|
+
(4.equal? 4).should eq true #because fixnums are really the same object
|
15
|
+
a = OrderTree::UniqueProxy.new(4)
|
16
|
+
b = OrderTree::UniqueProxy.new(4)
|
17
|
+
c = OrderTree::UniqueProxy.new(5)
|
18
|
+
(a == b).should eq true
|
19
|
+
(!c).should eq false
|
20
|
+
(c != a).should eq true
|
21
|
+
(a.equal? b).should eq false
|
22
|
+
(a.orig.equal? b.orig).should eq true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "can retrieve the unique id" do
|
26
|
+
a = OrderTree::UniqueProxy.new(4)
|
27
|
+
b = OrderTree::UniqueProxy.new(4)
|
28
|
+
a.unique_id.should_not eq b.unique_id
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can identify a proxy" do
|
32
|
+
a = OrderTree::UniqueProxy.new(5)
|
33
|
+
proxy(5).should eq proxy(5)
|
34
|
+
(proxy(5).equal? proxy(5)).should be_false
|
35
|
+
(proxy? a).should be_true
|
36
|
+
(proxy? 5).should be_false
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can help you inspect and to_s proxies" do
|
40
|
+
proxy(5).to_s.should eq "5"
|
41
|
+
proxy(5).inspect.should eq "5"
|
42
|
+
OrderTree::UniqueProxy.verbose_inspect = true
|
43
|
+
p = proxy(5)
|
44
|
+
p.inspect.to_s.should match(/#<UniqueProxy:(.*?)\s=>\s5>/)
|
45
|
+
p.to_s.should == "proxy(5)"
|
46
|
+
OrderTree::UniqueProxy.verbose_inspect = false
|
47
|
+
p2 = eval(p.to_s)
|
48
|
+
p2.should eq p
|
49
|
+
(p2.equal? p).should be_false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe OrderTree::OrderTree do
|
54
|
+
before :all do
|
55
|
+
@testhash = {
|
56
|
+
:from => {
|
57
|
+
:a => {
|
58
|
+
:b => 4,
|
59
|
+
:c => 4,
|
60
|
+
}
|
61
|
+
},
|
62
|
+
:to => {
|
63
|
+
:d => 4,
|
64
|
+
:e => 4,
|
65
|
+
:to_to => {
|
66
|
+
:f => 5,
|
67
|
+
:g => 6,
|
68
|
+
:h => 7,
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
@testhash_insertion = {
|
74
|
+
:to => {
|
75
|
+
:d => 4,
|
76
|
+
:e => 4,
|
77
|
+
:to_to => {
|
78
|
+
:f => 5,
|
79
|
+
:g => 6,
|
80
|
+
:h => 7,
|
81
|
+
}
|
82
|
+
},
|
83
|
+
:from => {
|
84
|
+
:a => {
|
85
|
+
:b => 4,
|
86
|
+
:c => 4,
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
@order = [[:from, :a, :b],
|
92
|
+
[:from, :a, :c],
|
93
|
+
[:from, :a],
|
94
|
+
[:from],
|
95
|
+
[:to, :d],
|
96
|
+
[:to, :e],
|
97
|
+
[:to, :to_to, :f],
|
98
|
+
[:to, :to_to, :g],
|
99
|
+
[:to, :to_to, :h],
|
100
|
+
[:to, :to_to],
|
101
|
+
[:to]]
|
102
|
+
end
|
103
|
+
|
104
|
+
it "initializes with a hash" do
|
105
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
106
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "can retrieve based on path or nest" do
|
110
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
111
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
112
|
+
[ot, ot2].map do |t|
|
113
|
+
t[:from][:a][:c].should eq 4
|
114
|
+
t[:from, :a, :c].should eq 4
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "can set based on path or nest" do
|
119
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
120
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
121
|
+
[ot, ot2].map do |t|
|
122
|
+
t[:from][:a][:d] = 4
|
123
|
+
t[:from, :a, :d].should eq 4
|
124
|
+
t[:from, :a, :e] = 6
|
125
|
+
t[:from][:a][:e].should eq 6
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "remember the order" do
|
130
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
131
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
132
|
+
ot.each_path.to_a.should eq @order
|
133
|
+
ot2.each_path.to_a.should_not eq @order
|
134
|
+
end
|
135
|
+
|
136
|
+
it "does not reify the hash on access" do
|
137
|
+
ot = OrderTree::OrderTree.new
|
138
|
+
lambda do
|
139
|
+
ot[:a, :b, :c] = 4
|
140
|
+
end.should raise_error NoMethodError
|
141
|
+
end
|
142
|
+
|
143
|
+
it "remembers the order after initialize" do
|
144
|
+
ot = OrderTree::OrderTree.new
|
145
|
+
order_paths = [[:a],
|
146
|
+
[:a, :a1],
|
147
|
+
[:a, :a2],
|
148
|
+
[:b],
|
149
|
+
[:b, :c],
|
150
|
+
[:b, :c, :d]]
|
151
|
+
order_paths.map do |v|
|
152
|
+
if [[:a], [:b], [:b, :c]].include? v
|
153
|
+
ot[*v] = {}
|
154
|
+
else
|
155
|
+
ot[*v] = 4
|
156
|
+
end
|
157
|
+
end
|
158
|
+
ot.each_path.to_a.should eq order_paths
|
159
|
+
end
|
160
|
+
|
161
|
+
it "can retrieve each pair" do
|
162
|
+
ot = OrderTree::OrderTree.new @testhash
|
163
|
+
|
164
|
+
ot.each_pair.with_index do |(p,v),i|
|
165
|
+
p.should eq @order[i]
|
166
|
+
ot[*p].should eq v
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it "can overwrite nodes" do
|
171
|
+
ot = OrderTree::OrderTree.new @testhash
|
172
|
+
ot[:from, :a, :c] = 'overwritten'
|
173
|
+
|
174
|
+
new_pairs = ot.each_pair.to_a
|
175
|
+
p,v = new_pairs.last
|
176
|
+
p.should eq [:from, :a, :c]
|
177
|
+
v.should eq 'overwritten'
|
178
|
+
|
179
|
+
ot[:from, :a, :c] = 'overwritten again'
|
180
|
+
|
181
|
+
p.should eq [:from, :a, :c]
|
182
|
+
ot[:from, :a, :c].should eq 'overwritten again'
|
183
|
+
|
184
|
+
new_pairs = ot.each_pair.to_a
|
185
|
+
p,v = new_pairs.first
|
186
|
+
p.should eq [:from, :a, :b]
|
187
|
+
v.should eq 4
|
188
|
+
|
189
|
+
p,v = new_pairs[3]
|
190
|
+
p.should eq [:to, :d]
|
191
|
+
v.should eq 4
|
192
|
+
end
|
193
|
+
|
194
|
+
it "does == comparison" do
|
195
|
+
ot = OrderTree::OrderTree.new @testhash
|
196
|
+
ot2 = OrderTree::OrderTree.new @testhash
|
197
|
+
|
198
|
+
ot.first.should eq ot2.first #because underlying objects are compared
|
199
|
+
(ot == ot2).should be_true #each order and == on the object
|
200
|
+
ot.equal?(ot2).should be_false #we're comparing the proxies here
|
201
|
+
|
202
|
+
(ot.first.equal? ot2.first).should be_false
|
203
|
+
end
|
204
|
+
|
205
|
+
it "does != comparison" do
|
206
|
+
ot = OrderTree::OrderTree.new @testhash
|
207
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
208
|
+
|
209
|
+
(ot != ot2).should be_true
|
210
|
+
end
|
211
|
+
|
212
|
+
it "does leaf/node equality with contents_equal?" do
|
213
|
+
ot = OrderTree::OrderTree.new @testhash
|
214
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
215
|
+
(ot.contents_equal? ot2).should be_true
|
216
|
+
end
|
217
|
+
|
218
|
+
it "overwriting a key moves it to the end of the order" do
|
219
|
+
ot = OrderTree::OrderTree.new
|
220
|
+
ot[:a] = 4
|
221
|
+
ot[:b] = 4
|
222
|
+
ot.each_path.to_a.should eq [[:a], [:b]]
|
223
|
+
ot[:a] = 5
|
224
|
+
ot.each_path.to_a.should eq [[:b], [:a]]
|
225
|
+
end
|
226
|
+
|
227
|
+
it "overwriting a nested keys moves to the end of the order" do
|
228
|
+
ot = OrderTree::OrderTree.new( {:a => { :b => 4}, :c => 5})
|
229
|
+
ot.each_path.to_a.should eq [[:a, :b], [:a], [:c]]
|
230
|
+
ot[:a,:b] = 5
|
231
|
+
ot.each_path.to_a.should eq [[:a], [:c], [:a, :b]]
|
232
|
+
end
|
233
|
+
|
234
|
+
it "does not double proxy the default" do
|
235
|
+
ot = OrderTree::OrderTree.new @testhash
|
236
|
+
(proxy? ot.default).should be_true
|
237
|
+
(proxy? ot[:foobar]).should be_false
|
238
|
+
(proxy? ot[:to, :to_to, :no_key]).should be_false
|
239
|
+
end
|
240
|
+
|
241
|
+
it "returns a default when the key doesn't exist" do
|
242
|
+
ot = OrderTree::OrderTree.new @testhash
|
243
|
+
ot.default = "foo"
|
244
|
+
ot[:to, :foo].should eq "foo"
|
245
|
+
|
246
|
+
#copies it to nested levels
|
247
|
+
ot.default = "bar"
|
248
|
+
ot[:to, :foo].should eq "bar"
|
249
|
+
|
250
|
+
ot[:to, :to_to, :no_key].should eq "bar"
|
251
|
+
|
252
|
+
# does't matter how deep i look
|
253
|
+
ot[:foo, :bar, :foo, :monkey].should eq "bar"
|
254
|
+
end
|
255
|
+
|
256
|
+
it "can find the path for value" do
|
257
|
+
ot = OrderTree::OrderTree.new @testhash
|
258
|
+
ot.path(7).should eq [:to, :to_to, :h]
|
259
|
+
ot.path(8).should be_nil
|
260
|
+
|
261
|
+
lambda do
|
262
|
+
ot.path!(7).should eq [:to, :to_to, :h]
|
263
|
+
ot.path!(8)
|
264
|
+
end.should raise_error OrderTree::OrderTree::PathNotFound
|
265
|
+
end
|
266
|
+
|
267
|
+
it "can prune the tree" do
|
268
|
+
ot = OrderTree::OrderTree.new @testhash
|
269
|
+
ot.default = "bob"
|
270
|
+
ot.delete :from, :a, :b
|
271
|
+
ot[:from, :a, :b].should eq "bob"
|
272
|
+
|
273
|
+
to_to = ot.at :to, :to_to
|
274
|
+
to_to.remove
|
275
|
+
|
276
|
+
ot[:to, :to_to].should eq "bob"
|
277
|
+
end
|
278
|
+
|
279
|
+
it "can find the path for a node object" do
|
280
|
+
ot = OrderTree::OrderTree.new @testhash
|
281
|
+
lambda do
|
282
|
+
ot.strict_path(7)
|
283
|
+
end.should raise_error ArgumentError
|
284
|
+
|
285
|
+
seven_node = ot.at *ot.path(7)
|
286
|
+
ot.strict_path(seven_node).should eq [:to, :to_to, :h]
|
287
|
+
|
288
|
+
seven_node.remove
|
289
|
+
ot.strict_path(seven_node).should be_nil
|
290
|
+
#this is the internal call that it uses - it's just here for completeness
|
291
|
+
lambda do
|
292
|
+
ot.strict_path!(seven_node)
|
293
|
+
end.should raise_error OrderTree::OrderTree::PathNotFound
|
294
|
+
end
|
295
|
+
|
296
|
+
it "can run enumerable methods which depend on <=>" do
|
297
|
+
ot = OrderTree::OrderTree.new @testhash
|
298
|
+
ot.max.should eq ot.last
|
299
|
+
ot.min.should eq ot.first
|
300
|
+
ot.sort.should eq ot.each_value.to_a
|
301
|
+
|
302
|
+
# roundabout
|
303
|
+
ot.max.should eq ot[*ot.strict_path!(ot.last)]
|
304
|
+
ot.min.should eq ot[*ot.strict_path!(ot.first)]
|
305
|
+
end
|
306
|
+
|
307
|
+
it "can tell you about insertion order, natch" do
|
308
|
+
ot = OrderTree::OrderTree.new @testhash
|
309
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
310
|
+
|
311
|
+
(ot.at(:from, :a, :c).before(ot.at(:from, :a, :b))).should be_false
|
312
|
+
(ot.at(:from, :a, :b).before(ot.at(:to, :d))).should be_true
|
313
|
+
|
314
|
+
(ot.at(:from, :a, :c).after(ot.at(:from, :a, :b))).should be_true
|
315
|
+
(ot.at(:to, :e).after(ot.at(:from, :a, :b))).should be_true
|
316
|
+
|
317
|
+
#this probably is only possible if you're doing this.
|
318
|
+
(ot.at(:from, :a, :b) <=> ot.at(:from, :a, :b)).should eq 0
|
319
|
+
end
|
320
|
+
|
321
|
+
it "can't compare nodes across trees" do
|
322
|
+
ot = OrderTree::OrderTree.new @testhash
|
323
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
324
|
+
|
325
|
+
lambda do
|
326
|
+
ot.at(:from, :a, :c).before(ot2.at(:from, :a, :b))
|
327
|
+
end.should raise_error ArgumentError
|
328
|
+
end
|
329
|
+
|
330
|
+
it "can run regular enumerable methods" do
|
331
|
+
# i'm not going to try all of these, just the one i know
|
332
|
+
# i didn't define.
|
333
|
+
ot = OrderTree::OrderTree.new @testhash
|
334
|
+
ot.each_cons(3).with_index do |v,idx|
|
335
|
+
v.collect { |e| e.path }.should eq @order[idx..idx+2]
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
if ENV["REPORT"] == "1" then
|
2
|
+
require 'simplecov'
|
3
|
+
require 'ruby-prof'
|
4
|
+
require 'ruby-debug'
|
5
|
+
|
6
|
+
SimpleCov.start do
|
7
|
+
add_filter "spec.rb"
|
8
|
+
coverage_dir "report/coverage"
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.before :suite do |example|
|
13
|
+
STDOUT << '|'
|
14
|
+
RubyProf.start
|
15
|
+
end
|
16
|
+
|
17
|
+
config.around :each do |example|
|
18
|
+
STDOUT << '.'
|
19
|
+
RubyProf.resume do
|
20
|
+
example.run
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
config.after :suite do
|
25
|
+
result = RubyProf.stop
|
26
|
+
result.eliminate_methods!([/RSpec::Matchers#.*?/])
|
27
|
+
printer = RubyProf::MultiPrinter.new(result)
|
28
|
+
printer.print(:path => 'report/profile', :profile => "profile")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Requires supporting files with custom matchers and macros, etc,
|
34
|
+
# in ./support/ and its subdirectories.
|
35
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: order_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Stephen Prater
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-07-25 00:00:00.000000000 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
requirement: &3691310 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *3691310
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: simplecov
|
28
|
+
requirement: &3691090 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *3691090
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: ruby-prof
|
39
|
+
requirement: &3690870 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *3690870
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: ruby-debug19
|
50
|
+
requirement: &3690650 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *3690650
|
59
|
+
description: Use OrderTree when you need both insertion order access and nested hash
|
60
|
+
path style access
|
61
|
+
email:
|
62
|
+
- stephenp@agrussell.com
|
63
|
+
executables: []
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files: []
|
66
|
+
files:
|
67
|
+
- .DS_Store
|
68
|
+
- .gitignore
|
69
|
+
- .yardoc/checksums
|
70
|
+
- .yardoc/objects/root.dat
|
71
|
+
- .yardoc/proxy_types
|
72
|
+
- Gemfile
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- lib/order_tree.rb
|
76
|
+
- lib/order_tree/order_tree.rb
|
77
|
+
- lib/order_tree/order_tree_node.rb
|
78
|
+
- lib/order_tree/unique_proxy.rb
|
79
|
+
- lib/order_tree/version.rb
|
80
|
+
- order_tree.gemspec
|
81
|
+
- spec/order_tree_spec.rb
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
has_rdoc: true
|
84
|
+
homepage: http://github.com/stephenprater/order_tree
|
85
|
+
licenses: []
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project: order_tree
|
104
|
+
rubygems_version: 1.5.2
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: An unbalanced tree / nested hash which remember insertion order
|
108
|
+
test_files:
|
109
|
+
- spec/order_tree_spec.rb
|
110
|
+
- spec/spec_helper.rb
|