DanaDanger-hyrarchy 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 The Indianapolis Star
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,69 @@
1
+ = Hyrarchy
2
+
3
+ Hyrarchy (Hybrid hieRarchy) is a gem and Rails plugin for working with hierarchic data in ActiveRecord. Your models gain methods for finding an instance's parent, children, ancestors, descendants, and depth, as well as a named scope for finding root nodes.
4
+
5
+ To use Hyrarchy in your Rails app, copy the plugin from the gem into your app's vendors/plugins directory. (The plugin is just a two-liner that loads and activates the gem.)
6
+
7
+ To use Hyrarchy in one of your models, add the following line to the class:
8
+
9
+ class Comment < ActiveRecord::Base
10
+ is_hierarchic
11
+ end
12
+
13
+ Then add the hierarchic columns to the model's database table:
14
+
15
+ class MakeCommentsHierarchic < ActiveRecord::Migration
16
+ def self.up
17
+ add_hierarchy :comments
18
+ end
19
+
20
+ def self.down
21
+ remove_hierarchy :comments
22
+ end
23
+ end
24
+
25
+ Or you can put it in the same migration as the table's creation:
26
+
27
+ class CreateCommentsTable < ActiveRecord::Migration
28
+ def self.up
29
+ create_table :comments do |t|
30
+ t.integer :author_id
31
+ t.text :body
32
+ end
33
+ add_hierarchy :comments
34
+ end
35
+
36
+ def self.down
37
+ drop_table :comments
38
+ end
39
+ end
40
+
41
+ == Performance
42
+
43
+ On MySQL, Hyrarchy scales to at least one million nodes with insertion and access times below 100ms. On SQLite, times are below 200ms.
44
+
45
+ == Database Compatibility
46
+
47
+ Hyrarchy has been tested on MySQL 5 and SQLite 3.
48
+
49
+ == Replacing awesome_nested_set
50
+
51
+ Hyrarchy is designed to be an almost-drop-in replacement for awesome_nested_set. All of awesome_nested_set's methods are implemented by Hyrarchy, but you'll need to replace calls to acts_as_nested_set with is_hierarchic. You'll also need to replace awesome_nested_set's database columns with Hyrarchy's, which you can do with an option to the add_hierarchy migration method:
52
+
53
+ add_hierarchy :comments, :convert => :awesome_nested_set
54
+
55
+ The convert option will modify the table structure but it won't rebuild the hierarchy information. You can rebuild it by calling rebuild! on your hierarchic model class:
56
+
57
+ Comment.rebuild!
58
+
59
+ The same option can be used with remove_hierarchy for the down half of a migration.
60
+
61
+ Hyrarchy doesn't yet support awesome_nested_set's scoping feature or its view helper.
62
+
63
+ == Implementation Details
64
+
65
+ Under the hood, Hyrarchy uses a combination of an adjacency list and a rational nested set. The nested set uses a technique developed by (I think) Vadim Tropashko, in which the left and right values are generated using Farey sequences. This makes it possible to insert new records without adjusting the left and right values of any other records. It also makes it possible to do many operations (like determining a record's depth in the tree) without accessing the database. For operations where rational nested sets perform poorly (such as finding a node's immediate descendants), the adjacency list is used.
66
+
67
+ == Credits and Copyright
68
+
69
+ Heavily based on works by Vadim Tropashko and Wim Lewis. Implemented by Dana Danger. Tolerated by VivaZoya. Copyright (c) 2008 The Indianapolis Star, released under the MIT license. See LICENSE for details.
data/lib/hyrarchy.rb ADDED
@@ -0,0 +1,317 @@
1
+ require 'hyrarchy/encoded_path'
2
+ require 'hyrarchy/collection_proxy'
3
+ require 'hyrarchy/awesome_nested_set_compatibility'
4
+
5
+ module Hyrarchy
6
+ # Fudge factor to account for imprecision with floating point approximations
7
+ # of a node's left and right fractions.
8
+ FLOAT_FUDGE_FACTOR = 0.0000000000001 # :nodoc:
9
+
10
+ # Mixes Hyrarchy into ActiveRecord.
11
+ def self.activate!
12
+ ActiveRecord::Base.extend IsHierarchic
13
+ ActiveRecord::Migration.extend Migrations
14
+ end
15
+
16
+ # These methods are available in ActiveRecord migrations for adding and
17
+ # removing columns and indexes required by Hyrarchy.
18
+ module Migrations
19
+ def add_hierarchy(table, options = {})
20
+ convert = options.delete(:convert)
21
+ unless options.empty?
22
+ raise(ArgumentError, "unknown keys: #{options.keys.join(', ')}")
23
+ end
24
+
25
+ case convert
26
+ when :awesome_nested_set
27
+ remove_column table, :lft
28
+ remove_column table, :rgt
29
+ when '', nil
30
+ else
31
+ raise(ArgumentError, "don't know how to convert hierarchy from #{convert}")
32
+ end
33
+
34
+ add_column table, :lft, :float
35
+ add_column table, :rgt, :float
36
+ add_column table, :lft_numer, :integer
37
+ add_column table, :lft_denom, :integer
38
+ add_column table, :parent_id, :integer unless convert == :awesome_nested_set
39
+ add_index table, :lft
40
+ add_index table, [:lft_numer, :lft_denom], :unique => true
41
+ add_index table, :parent_id
42
+ end
43
+
44
+ def remove_hierarchy(table, options = {})
45
+ convert = options.delete(:convert)
46
+ unless options.empty?
47
+ raise(ArgumentError, "unknown keys: #{options.keys.join(', ')}")
48
+ end
49
+
50
+ remove_column table, :lft
51
+ remove_column table, :rgt
52
+ remove_column table, :lft_numer
53
+ remove_column table, :lft_denom
54
+ remove_column table, :parent_id, :integer unless convert == :awesome_nested_set
55
+
56
+ case convert
57
+ when :awesome_nested_set
58
+ add_column table, :lft, :integer
59
+ add_column table, :rgt, :integer
60
+ when '', nil
61
+ else
62
+ raise(ArgumentError, "don't know how to convert hierarchy to #{convert}")
63
+ end
64
+ end
65
+ end
66
+
67
+ module IsHierarchic
68
+ # Declares that a model represents hierarchic data. Adds a has_many
69
+ # association for instances' children, and a named scope for the model's
70
+ # root nodes (called +roots+).
71
+ def is_hierarchic
72
+ extend ClassMethods
73
+ include InstanceMethods
74
+
75
+ has_many :children,
76
+ :foreign_key => 'parent_id',
77
+ :order => 'rgt DESC, lft',
78
+ :class_name => self.to_s,
79
+ :dependent => :destroy
80
+
81
+ before_save :set_encoded_paths
82
+ before_save :set_parent_id
83
+ after_destroy :mark_path_free
84
+
85
+ named_scope :roots,
86
+ :conditions => { :parent_id => nil },
87
+ :order => 'rgt DESC, lft'
88
+ end
89
+ end
90
+
91
+ # These private methods are available to model classes that have been
92
+ # declared is_hierarchic. They're used internally and aren't intended to be
93
+ # used by application developers.
94
+ module ClassMethods # :nodoc:
95
+ include Hyrarchy::AwesomeNestedSetCompatibility::ClassMethods
96
+
97
+ private
98
+
99
+ # Returns an array of unused child paths beneath +parent_path+.
100
+ def free_child_paths(parent_path)
101
+ @@free_child_paths ||= {}
102
+ @@free_child_paths[parent_path] ||= []
103
+ end
104
+
105
+ # Stores +path+ in the arrays of free child paths.
106
+ def child_path_is_free(path)
107
+ parent_path = path.parent(false)
108
+ free_child_paths(parent_path) << path
109
+ free_child_paths(parent_path).sort!
110
+ end
111
+
112
+ # Removes all paths from the array of free child paths for +parent_path+.
113
+ def reset_free_child_paths(parent_path)
114
+ free_child_paths(parent_path).clear
115
+ end
116
+
117
+ # Removes all paths from the array of free child paths.
118
+ def reset_all_free_child_paths
119
+ @@free_child_paths = {}
120
+ end
121
+
122
+ # Finds the first unused child path beneath +parent_path+.
123
+ def next_child_encoded_path(parent_path)
124
+ p = free_child_paths(parent_path).shift || parent_path.first_child
125
+ while true do
126
+ if exists?(:lft_numer => p.numerator, :lft_denom => p.denominator)
127
+ p = parent_path.mediant(p)
128
+ else
129
+ if free_child_paths(parent_path).empty?
130
+ child_path_is_free(parent_path.mediant(p))
131
+ end
132
+ return p
133
+ end
134
+ end
135
+ end
136
+
137
+ # Returns the node with the specified encoded path.
138
+ def find_by_encoded_path(p)
139
+ find(:first, :conditions => {
140
+ :lft_numer => p.numerator,
141
+ :lft_denom => p.denominator
142
+ })
143
+ end
144
+ end
145
+
146
+ # These methods are available to instances of models that have been declared
147
+ # is_hierarchic.
148
+ module InstanceMethods
149
+ include Hyrarchy::AwesomeNestedSetCompatibility::InstanceMethods
150
+
151
+ # Returns this node's parent, or +nil+ if this is a root node.
152
+ def parent
153
+ return @new_parent if @new_parent
154
+ p = encoded_path.parent
155
+ return nil if p.nil?
156
+ self.class.send(:find_by_encoded_path, p)
157
+ end
158
+
159
+ # Sets this node's parent. To make this node a root node, set its parent to
160
+ # +nil+.
161
+ def parent=(other)
162
+ @make_root = false
163
+ if other.nil?
164
+ @new_parent = nil
165
+ @make_root = true
166
+ elsif encoded_path && other.encoded_path == (encoded_path.parent rescue nil)
167
+ @new_parent = nil
168
+ else
169
+ @new_parent = other
170
+ end
171
+ other
172
+ end
173
+
174
+ # Returns an array of this node's descendants: its children, grandchildren,
175
+ # and so on. The array returned by this method is a named scope.
176
+ def descendants
177
+ cached[:descendants] ||=
178
+ self_and_descendants.scoped :conditions => "id <> #{id}"
179
+ end
180
+
181
+ # Returns an array of this node's ancestors--its parent, grandparent, and
182
+ # so on--ordered from parent to root. The array returned by this method is
183
+ # a has_many association, so you can do things like this:
184
+ #
185
+ # node.ancestors.find(:all, :conditions => { ... })
186
+ #
187
+ def ancestors(with_self = false)
188
+ cache_key = with_self ? :self_and_ancestors : :ancestors
189
+ return cached[cache_key] if cached[cache_key]
190
+
191
+ paths = []
192
+ path = with_self ? encoded_path : encoded_path.parent
193
+ while path do
194
+ paths << path
195
+ path = path.parent
196
+ end
197
+
198
+ cached[cache_key] = CollectionProxy.new(
199
+ self,
200
+ cache_key,
201
+ :conditions => paths.empty? ? "id <> id" : [
202
+ paths.collect {|p| "(lft_numer = ? AND lft_denom = ?)"}.join(" OR "),
203
+ *(paths.collect {|p| [p.numerator, p.denominator]}.flatten)
204
+ ],
205
+ :order => 'rgt DESC, lft'
206
+ )
207
+ end
208
+
209
+ # Returns the root node related to this node, or nil if this node is a root
210
+ # node.
211
+ def root
212
+ return cached[:root] if cached[:root]
213
+
214
+ path = encoded_path.parent
215
+ while path do
216
+ parent = path.parent
217
+ break if parent.nil?
218
+ path = parent
219
+ end
220
+
221
+ if path
222
+ self.class.send :find_by_encoded_path, path
223
+ else
224
+ nil
225
+ end
226
+ end
227
+
228
+ # Returns the number of nodes between this one and the top of the tree.
229
+ def depth
230
+ encoded_path.depth - 1
231
+ end
232
+
233
+ # Overrides ActiveRecord's reload method to clear cached scopes and ad hoc
234
+ # associations.
235
+ def reload(options = nil) # :nodoc:
236
+ @cached = {}
237
+ super
238
+ end
239
+
240
+ protected
241
+
242
+ # Sets the node's encoded path, updating all relevant database columns to
243
+ # match.
244
+ def encoded_path=(r) # :nodoc:
245
+ @cached = {}
246
+ if r.nil?
247
+ self.lft_numer = nil
248
+ self.lft_denom = nil
249
+ self.lft = nil
250
+ self.rgt = nil
251
+ else
252
+ self.lft_numer = r.numerator
253
+ self.lft_denom = r.denominator
254
+ self.lft = r.to_f
255
+ self.rgt = encoded_path.next_farey_fraction.to_f
256
+ end
257
+ r
258
+ end
259
+
260
+ # Returns the node's encoded path (its rational left value).
261
+ def encoded_path # :nodoc:
262
+ return nil if lft_numer.nil? || lft_denom.nil?
263
+ Hyrarchy::EncodedPath(lft_numer, lft_denom)
264
+ end
265
+
266
+ # Returns a hash for caching scopes and ad hoc associations.
267
+ def cached # :nodoc:
268
+ @cached ||= {}
269
+ end
270
+
271
+ private
272
+
273
+ # before_save callback to ensure that this node's encoded path is a child
274
+ # of its parent, and that its descendants' paths are updated if this node
275
+ # has moved.
276
+ def set_encoded_paths # :nodoc:
277
+ p = nil
278
+ self.lft_numer = self.lft_denom = nil if @make_root
279
+
280
+ if @new_parent.nil?
281
+ if lft_numer.nil? || lft_denom.nil?
282
+ p = Hyrarchy::EncodedPath::ROOT
283
+ end
284
+ else
285
+ p = @new_parent.encoded_path
286
+ end
287
+
288
+ if p
289
+ new_path = self.class.send(:next_child_encoded_path, p)
290
+ if encoded_path != new_path
291
+ self.class.send(:reset_free_child_paths, encoded_path)
292
+ self.encoded_path = new_path
293
+ children.each do |c|
294
+ c.parent = self
295
+ c.save!
296
+ end
297
+ end
298
+ end
299
+
300
+ true
301
+ end
302
+
303
+ # before_save callback to ensure that this node's parent_id attribute
304
+ # agrees with its encoded path.
305
+ def set_parent_id # :nodoc:
306
+ parent = self.class.send(:find_by_encoded_path, encoded_path.parent(false))
307
+ self.parent_id = parent ? parent.id : nil
308
+ true
309
+ end
310
+
311
+ # after_destroy callback to add this node's encoded path to its parent's
312
+ # list of available child paths.
313
+ def mark_path_free # :nodoc:
314
+ self.class.send(:child_path_is_free, encoded_path)
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,295 @@
1
+ module Hyrarchy
2
+ module AwesomeNestedSetCompatibility
3
+ module ClassMethods
4
+ # Returns the first root node.
5
+ def root
6
+ roots.first
7
+ end
8
+
9
+ # Returns true if the model's left and right values are valid, and all
10
+ # root nodes have no ancestors.
11
+ def valid?
12
+ left_and_rights_valid? && all_roots_valid?
13
+ end
14
+
15
+ # Returns true if the model's left and right values match the parent_id
16
+ # attributes.
17
+ def left_and_rights_valid?
18
+ # Load all nodes and index them by ID so we can leave the database
19
+ # alone.
20
+ nodes = connection.select_all("SELECT id, lft_numer, lft_denom, parent_id FROM #{quoted_table_name}")
21
+ nodes_by_id = {}
22
+ nodes.each do |node|
23
+ node['id'] = node['id'].to_i
24
+ node['encoded_path'] = Hyrarchy::EncodedPath(node['lft_numer'].to_i, node['lft_denom'].to_i)
25
+ node['parent_id'] = node['parent_id'] ? node['parent_id'].to_i : nil
26
+ nodes_by_id[node['id']] = node
27
+ end
28
+ # Check to see if the structure defined by the nodes' encoded paths
29
+ # matches the structure defined by their parent_id attributes.
30
+ nodes.all? do |node|
31
+ if node['parent_id'].nil?
32
+ node['encoded_path'].parent == nil rescue false
33
+ else
34
+ parent = nodes_by_id[node['parent_id']]
35
+ parent && node['encoded_path'].parent == parent['encoded_path']
36
+ end
37
+ end
38
+ end
39
+
40
+ # Always returns true. This method exists solely for compatibility with
41
+ # awesome_nested_set; the test it performs doesn't apply to Hyrarchy.
42
+ def no_duplicates_for_columns?
43
+ true
44
+ end
45
+
46
+ # Returns true if all roots have no ancestors.
47
+ def all_roots_valid?
48
+ each_root_valid?(roots)
49
+ end
50
+
51
+ # Returns true if all of the nodes in +roots_to_validate+ have no
52
+ # ancestors.
53
+ def each_root_valid?(roots_to_validate)
54
+ roots_to_validate.all? {|r| r.root?}
55
+ end
56
+
57
+ # Rebuilds the model's hierarchy attributes based on the parent_id
58
+ # attributes.
59
+ def rebuild!
60
+ return true if (valid? rescue false)
61
+
62
+ update_all("lft = id, rgt = id, lft_numer = id, lft_denom = id")
63
+ reset_all_free_child_paths
64
+ paths_by_id = {}
65
+ order_by = columns_hash['created_at'] ? :created_at : :id
66
+
67
+ nodes = roots :order => order_by
68
+ until nodes.empty? do
69
+ nodes.each do |node|
70
+ parent_path = paths_by_id[node.parent_id] || Hyrarchy::EncodedPath::ROOT
71
+ node.send(:encoded_path=, next_child_encoded_path(parent_path))
72
+ node.send(:create_or_update_without_callbacks) || raise(RecordNotSaved)
73
+ paths_by_id[node.id] = node.send(:encoded_path)
74
+ end
75
+ node_ids = nodes.collect {|n| n.id}
76
+ nodes = find(:all, :conditions => { :parent_id => node_ids }, :order => order_by)
77
+ end
78
+ end
79
+ end
80
+
81
+ module InstanceMethods
82
+ # Returns this node's left value. Records that haven't yet been saved
83
+ # won't have left values.
84
+ def left
85
+ encoded_path
86
+ end
87
+
88
+ # Returns this node's left value. Records that haven't yet been saved
89
+ # won't have right values.
90
+ def right
91
+ encoded_path && encoded_path.next_farey_fraction
92
+ end
93
+
94
+ # Returns true if this is a root node.
95
+ def root?
96
+ (encoded_path.nil? || depth == 0 || @make_root) && !@new_parent
97
+ end
98
+
99
+ # Returns true if this node has no children.
100
+ def leaf?
101
+ children.empty?
102
+ end
103
+
104
+ # Returns true if this node is a child of another node.
105
+ def child?
106
+ !root?
107
+ end
108
+
109
+ # Compares two nodes by their left values.
110
+ def <=>(x)
111
+ x.left <=> left
112
+ end
113
+
114
+ # Returns an array containing this node and its ancestors, starting with
115
+ # this node and ending with its root. The array returned by this method
116
+ # is a has_many association, so you can do things like this:
117
+ #
118
+ # node.self_and_ancestors.find(:all, :conditions => { ... })
119
+ #
120
+ def self_and_ancestors
121
+ ancestors(true)
122
+ end
123
+
124
+ # Returns an array containing this node and its siblings. The array
125
+ # returned by this method is a has_many association, so you can do things
126
+ # like this:
127
+ #
128
+ # node.self_and_siblings.find(:all, :conditions => { ... })
129
+ #
130
+ def self_and_siblings
131
+ siblings(true)
132
+ end
133
+
134
+ # Returns an array containing this node's siblings. The array returned by
135
+ # this method is a has_many association, so you can do things like this:
136
+ #
137
+ # node.siblings.find(:all, :conditions => { ... })
138
+ #
139
+ def siblings(with_self = false)
140
+ cache_key = with_self ? :self_and_siblings : :siblings
141
+ return cached[cache_key] if cached[cache_key]
142
+
143
+ if with_self
144
+ conditions = { :parent_id => parent_id }
145
+ else
146
+ conditions = ["parent_id #{parent_id.nil? ? 'IS' : '='} ? AND id <> ?",
147
+ parent_id, id]
148
+ end
149
+
150
+ cached[cache_key] = self.class.scoped(
151
+ :conditions => conditions,
152
+ :order => 'rgt DESC, lft'
153
+ )
154
+ end
155
+
156
+ # Returns an array containing this node's childless descendants. The
157
+ # array returned by this method is a named scope.
158
+ def leaves
159
+ cached[:leaves] ||= descendants.scoped :conditions => "NOT EXISTS (
160
+ SELECT * FROM #{self.class.quoted_table_name} tt
161
+ WHERE tt.parent_id = #{self.class.quoted_table_name}.id
162
+ )"
163
+ end
164
+
165
+ # Alias for depth.
166
+ def level
167
+ depth
168
+ end
169
+
170
+ # Returns an array of this node and its descendants: its children,
171
+ # grandchildren, and so on. The array returned by this method is a
172
+ # has_many association, so you can do things like this:
173
+ #
174
+ # node.self_and_descendants.find(:all, :conditions => { ... })
175
+ #
176
+ def self_and_descendants
177
+ cached[:self_and_descendants] ||= CollectionProxy.new(
178
+ self,
179
+ :descendants,
180
+ :conditions => { :lft => (lft - FLOAT_FUDGE_FACTOR)..(rgt + FLOAT_FUDGE_FACTOR) },
181
+ :order => 'rgt DESC, lft',
182
+ # The query conditions intentionally load extra records that aren't
183
+ # descendants to account for floating point imprecision. This
184
+ # procedure removes the extra records.
185
+ :after => Proc.new do |records|
186
+ r = encoded_path.next_farey_fraction
187
+ records.delete_if do |n|
188
+ n.encoded_path < encoded_path || n.encoded_path >= r
189
+ end
190
+ end,
191
+ # The regular count method doesn't work because of the fudge factor
192
+ # in the conditions. This procedure uses the length of the records
193
+ # array if it's been loaded. Otherwise it does a raw SQL query (to
194
+ # avoid the expense of instantiating a bunch of ActiveRecord objects)
195
+ # and prunes the results in the same manner as the :after procedure.
196
+ :count => Proc.new do
197
+ if descendants.loaded?
198
+ descendants.length
199
+ else
200
+ rows = self.class.connection.select_all("
201
+ SELECT lft_numer, lft_denom
202
+ FROM #{self.class.quoted_table_name}
203
+ WHERE #{descendants.conditions}")
204
+ r = encoded_path.next_farey_fraction
205
+ rows.delete_if do |row|
206
+ p = Hyrarchy::EncodedPath(
207
+ row['lft_numer'].to_i,
208
+ row['lft_denom'].to_i)
209
+ p < encoded_path || p >= r
210
+ end
211
+ rows.length
212
+ end
213
+ end
214
+ )
215
+ end
216
+
217
+ # Returns true if this node is a descendant of +other+.
218
+ def is_descendant_of?(other)
219
+ left > other.left && left <= other.right
220
+ end
221
+
222
+ # Returns true if this node is a descendant of +other+, or if this node
223
+ # is +other+.
224
+ def is_or_is_descendant_of?(other)
225
+ left >= other.left && left <= other.right
226
+ end
227
+
228
+ # Returns true if this node is an ancestor of +other+.
229
+ def is_ancestor_of?(other)
230
+ other.left > left && other.left <= right
231
+ end
232
+
233
+ # Returns true if this node is an ancestor of +other+, or if this node is
234
+ # +other+.
235
+ def is_or_is_ancestor_of?(other)
236
+ other.left >= left && other.left <= right
237
+ end
238
+
239
+ # Always returns true. This method exists solely for compatibility with
240
+ # awesome_nested_set; Hyrarchy doesn't support scoping (but maybe it will
241
+ # some day).
242
+ def same_scope?(other)
243
+ true
244
+ end
245
+
246
+ def left_sibling # :nodoc:
247
+ raise NotImplementedError, "awesome_nested_set's left_sibling method isn't implemented in this version of Hyrarchy"
248
+ end
249
+
250
+ def right_sibling # :nodoc:
251
+ raise NotImplementedError, "awesome_nested_set's right_sibling method isn't implemented in this version of Hyrarchy"
252
+ end
253
+
254
+ def move_left # :nodoc:
255
+ raise NotImplementedError, "awesome_nested_set's move_left method isn't implemented in this version of Hyrarchy"
256
+ end
257
+
258
+ def move_right # :nodoc:
259
+ raise NotImplementedError, "awesome_nested_set's move_right method isn't implemented in this version of Hyrarchy"
260
+ end
261
+
262
+ def move_to_left_of(other) # :nodoc:
263
+ raise NotImplementedError, "awesome_nested_set's move_to_left_of method isn't implemented in this version of Hyrarchy"
264
+ end
265
+
266
+ def move_to_right_of(other) # :nodoc:
267
+ raise NotImplementedError, "awesome_nested_set's move_to_right_of method isn't implemented in this version of Hyrarchy"
268
+ end
269
+
270
+ # Sets this node's parent to +node+ and calls save!.
271
+ def move_to_child_of(node)
272
+ node = self.class.find(node)
273
+ self.parent = node
274
+ save!
275
+ end
276
+
277
+ # Makes this node a root node and calls save!.
278
+ def move_to_root
279
+ self.parent = nil
280
+ save!
281
+ end
282
+
283
+ def move_possible?(target) # :nodoc:
284
+ raise NotImplementedError, "awesome_nested_set's move_possible? method isn't implemented in this version of Hyrarchy"
285
+ end
286
+
287
+ # Returns a textual representation of this node and its descendants.
288
+ def to_text
289
+ self_and_descendants.map do |node|
290
+ "#{'*'*(node.depth+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
291
+ end.join("\n")
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,65 @@
1
+ module Hyrarchy
2
+ # This is a shameful hack to create has_many associations with no foreign key
3
+ # and an option for running a post-processing procedure on the array of
4
+ # records. Hyrarchy uses this class to provide the features of a has_many
5
+ # association on a node's ancestors and descendants arrays.
6
+ class CollectionProxy < ActiveRecord::Associations::HasManyAssociation # :nodoc:
7
+ def initialize(owner, name, options = {})
8
+ @after = options.delete(:after)
9
+ @count = options.delete(:count)
10
+ reflection = ActiveRecord::Base.create_reflection(
11
+ :has_many, name, options.merge(:class_name => owner.class.to_s), owner.class)
12
+ super(owner, reflection)
13
+ end
14
+
15
+ # This is ripped right from the construct_sql method in HasManyAssociation,
16
+ # but the foreign key condition has been removed.
17
+ def construct_sql
18
+ if @reflection.options[:finder_sql]
19
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
20
+ else
21
+ @finder_sql = conditions
22
+ end
23
+
24
+ if @reflection.options[:counter_sql]
25
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
26
+ elsif @reflection.options[:finder_sql]
27
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
28
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
29
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
30
+ else
31
+ @counter_sql = @finder_sql
32
+ end
33
+ end
34
+
35
+ # Overrides find to run the association's +after+ procedure on the results.
36
+ def find(*args)
37
+ records = super
38
+ @after.call(records) if @after
39
+ records
40
+ end
41
+
42
+ # Overrides count to run the association's +count+ procedure, with caching.
43
+ def count
44
+ if @count
45
+ if @count.respond_to?(:call)
46
+ @count = @count.call
47
+ else
48
+ @count
49
+ end
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ # Overrides find_target to run the association's +after+ procedure on the
58
+ # results.
59
+ def find_target
60
+ records = super
61
+ @after.call(records) if @after
62
+ records
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,101 @@
1
+ require 'rational'
2
+
3
+ module Hyrarchy
4
+ # Returns a new path with numerator +n+ and denominator +d+, which will be
5
+ # reduced if possible. Paths must be in the interval [0,1]. This method
6
+ # correlates to the Rational(n, d) method.
7
+ def self.EncodedPath(n, d) # :nodoc:
8
+ r = EncodedPath.reduce n, d
9
+ raise(RangeError, "paths must be in the interval [0,1]") if r < 0 || r > 1
10
+ r
11
+ end
12
+
13
+ # An encoded path is a rational number that represents a node's position in
14
+ # the tree. By using rational numbers instead of integers, new nodes can be
15
+ # inserted arbitrarily without having to adjust the left and right values of
16
+ # any other nodes. Farey sequences are used to prevent denominators from
17
+ # growing exponentially and quickly exhausting the database's integer range.
18
+ # For more information, see "Nested Intervals with Farey Fractions" by Vadim
19
+ # Tropashko: http://arxiv.org/html/cs.DB/0401014
20
+ class EncodedPath < Rational # :nodoc:
21
+ # Path of the uppermost node in the tree. The node at this path has no
22
+ # siblings, and all nodes descend from it.
23
+ ROOT = Hyrarchy::EncodedPath(0, 1)
24
+
25
+ # Returns the path of the parent of the node at this path. If +root_is_nil+
26
+ # is true (the default) and the parent is the root node, returns nil.
27
+ def parent(root_is_nil = true)
28
+ r = next_farey_fraction
29
+ p = Hyrarchy::EncodedPath(
30
+ numerator - r.numerator,
31
+ denominator - r.denominator)
32
+ (root_is_nil && p == ROOT) ? nil : p
33
+ end
34
+
35
+ # Returns the depth of the node at this path, starting from the root node.
36
+ # Paths in the uppermost layer (considered "root nodes" by the ActiveRecord
37
+ # methods) have a depth of one.
38
+ def depth
39
+ n = self
40
+ depth = 0
41
+ while n != ROOT
42
+ n = n.parent(false)
43
+ depth += 1
44
+ end
45
+ depth
46
+ end
47
+
48
+ # Returns the path of the first child of the node at this path.
49
+ def first_child
50
+ mediant(next_farey_fraction)
51
+ end
52
+
53
+ # Returns the path of the sibling immediately after the node at this path.
54
+ def next_sibling
55
+ parent(false).mediant(self)
56
+ end
57
+
58
+ # Finds the mediant of this fraction and +other+.
59
+ def mediant(other)
60
+ Hyrarchy::EncodedPath(
61
+ numerator + other.numerator,
62
+ denominator + other.denominator)
63
+ end
64
+
65
+ # Returns the fraction immediately after this one in the Farey sequence
66
+ # whose order is this fraction's denominator. This is the find-neighbors
67
+ # algorithm from "Rounding rational numbers using Farey/Cauchy sequence" by
68
+ # Wim Lewis: http://www.hhhh.org/wiml/proj/farey
69
+ def next_farey_fraction
70
+ # Handle the special case of the last fraction.
71
+ return nil if self == Rational(1, 1)
72
+ # Compute the modular multiplicative inverses of the numerator and
73
+ # denominator using an iterative extended Euclidean algorithm. These
74
+ # inverses are the denominator and negative numerator of the fraction
75
+ # preceding this one, modulo the numerator and denominator of this
76
+ # fraction.
77
+ a, b = [numerator, denominator]
78
+ x, lastx, y, lasty = [0, 1, 1, 0]
79
+ while b != 0
80
+ a, b, q = [b, a % b, a / b]
81
+ x, lastx = [lastx - q * x, x]
82
+ y, lasty = [lasty - q * y, y]
83
+ end
84
+ qL, pL = [lastx, -lasty]
85
+ # Find the numerator and denominator of the fraction following this one
86
+ # using the mediant relationship between it, this fraction, and the
87
+ # preceding fraction. The modulo ambiguity is resolved by brute force,
88
+ # which is probably not the smartest way to do it, but it's fast enough.
89
+ i = 0
90
+ while true do
91
+ a = pL + numerator * i
92
+ b = qL + denominator * i
93
+ if (numerator * b - denominator * a == 1) &&
94
+ (Rational(numerator - a, denominator - b).denominator <= denominator)
95
+ return Hyrarchy::EncodedPath(numerator - a, denominator - b)
96
+ end
97
+ i += 1
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,2 @@
1
+ require 'hyrarchy'
2
+ Hyrarchy.activate!
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ gem 'sqlite3-ruby'
3
+ require 'activerecord'
4
+ require 'yaml'
5
+
6
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
7
+ require 'hyrarchy'
8
+ Hyrarchy.activate!
9
+
10
+ db_specs = YAML.load_file(File.join(File.dirname(__FILE__), 'database.yml'))
11
+ which_spec = ENV['DB'] || 'mysql'
12
+ ActiveRecord::Base.establish_connection(db_specs[which_spec])
13
+
14
+ class CreateNodesTable < ActiveRecord::Migration
15
+ def self.up
16
+ create_table :nodes do |t|
17
+ t.string :name, :null => false
18
+ end
19
+ add_hierarchy :nodes
20
+ end
21
+
22
+ def self.down
23
+ drop_table :nodes
24
+ end
25
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,10 @@
1
+ mysql:
2
+ adapter: mysql
3
+ host: localhost
4
+ database: hyrarchy_test
5
+ username: root
6
+ password:
7
+
8
+ sqlite:
9
+ adapter: sqlite3
10
+ database: spec/test.sqlite3
@@ -0,0 +1,184 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Hyrarchy do
4
+ describe "(functionality)" do
5
+ before(:all) do
6
+ Node.delete_all
7
+
8
+ @roots = [
9
+ Node.create!(:name => 'root 0'),
10
+ Node.create!(:name => 'root 1'),
11
+ Node.create!(:name => 'root 2')
12
+ ]
13
+ @layer1 = [
14
+ Node.create!(:name => '1.0', :parent => @roots[1]),
15
+ Node.create!(:name => '1.1', :parent => @roots[1]),
16
+ Node.create!(:name => '1.2', :parent => @roots[1])
17
+ ]
18
+ @layer2 = [
19
+ Node.create!(:name => '1.0.0', :parent => @layer1[0]),
20
+ Node.create!(:name => '1.0.1', :parent => @layer1[0]),
21
+ Node.create!(:name => '1.1.0', :parent => @layer1[1]),
22
+ Node.create!(:name => '1.1.1', :parent => @layer1[1]),
23
+ Node.create!(:name => '1.2.0', :parent => @layer1[2]),
24
+ Node.create!(:name => '1.2.1', :parent => @layer1[2])
25
+ ]
26
+
27
+ @roots.collect! {|n| Node.find(n.id)}
28
+ @layer1.collect! {|n| Node.find(n.id)}
29
+ @layer2.collect! {|n| Node.find(n.id)}
30
+ end
31
+
32
+ it "should find its parent" do
33
+ @layer2[0].parent.should == @layer1[0]
34
+ @layer2[1].parent.should == @layer1[0]
35
+ @layer2[2].parent.should == @layer1[1]
36
+ @layer2[3].parent.should == @layer1[1]
37
+ @layer2[4].parent.should == @layer1[2]
38
+ @layer2[5].parent.should == @layer1[2]
39
+ @layer1.each {|n| n.parent.should == @roots[1]}
40
+ @roots.each {|n| n.parent.should == nil}
41
+ end
42
+
43
+ it "should find its descendants" do
44
+ returned_descendants = @roots[1].descendants
45
+ returned_descendants.sort! {|a,b| a.name <=> b.name}
46
+ actual_descendants = @layer1 + @layer2
47
+ actual_descendants.sort! {|a,b| a.name <=> b.name}
48
+ returned_descendants.should == actual_descendants
49
+ @roots[0].descendants.should be_empty
50
+ @roots[2].descendants.should be_empty
51
+ end
52
+
53
+ it "should find its children" do
54
+ @roots[0].children.should be_empty
55
+ @roots[1].children.should == @layer1
56
+ @roots[2].children.should be_empty
57
+ @layer1[0].children.should == [@layer2[0], @layer2[1]]
58
+ @layer1[1].children.should == [@layer2[2], @layer2[3]]
59
+ @layer1[2].children.should == [@layer2[4], @layer2[5]]
60
+ @layer2.each {|n| n.children.should be_empty}
61
+ end
62
+
63
+ it "should find its ancestors" do
64
+ @layer2[0].ancestors.should == [@layer1[0], @roots[1]]
65
+ @layer2[1].ancestors.should == [@layer1[0], @roots[1]]
66
+ @layer2[2].ancestors.should == [@layer1[1], @roots[1]]
67
+ @layer2[3].ancestors.should == [@layer1[1], @roots[1]]
68
+ @layer2[4].ancestors.should == [@layer1[2], @roots[1]]
69
+ @layer2[5].ancestors.should == [@layer1[2], @roots[1]]
70
+ @layer1.each {|n| n.ancestors.should == [@roots[1]]}
71
+ @roots.each {|n| n.ancestors.should be_empty}
72
+ end
73
+
74
+ it "should find all root nodes" do
75
+ Node.roots.should == @roots
76
+ end
77
+ end
78
+
79
+ describe "(data integrity)" do
80
+ before(:each) do
81
+ Node.delete_all
82
+
83
+ @roots = [
84
+ Node.create!(:name => 'root 0'),
85
+ Node.create!(:name => 'root 1'),
86
+ Node.create!(:name => 'root 2')
87
+ ]
88
+ @layer1 = [
89
+ Node.create!(:name => '1.0', :parent => @roots[1]),
90
+ Node.create!(:name => '1.1', :parent => @roots[1]),
91
+ Node.create!(:name => '1.2', :parent => @roots[1])
92
+ ]
93
+ @layer2 = [
94
+ Node.create!(:name => '1.0.0', :parent => @layer1[0]),
95
+ Node.create!(:name => '1.0.1', :parent => @layer1[0]),
96
+ Node.create!(:name => '1.1.0', :parent => @layer1[1]),
97
+ Node.create!(:name => '1.1.1', :parent => @layer1[1]),
98
+ Node.create!(:name => '1.2.0', :parent => @layer1[2]),
99
+ Node.create!(:name => '1.2.1', :parent => @layer1[2])
100
+ ]
101
+
102
+ @roots.collect! {|n| Node.find(n.id)}
103
+ @layer1.collect! {|n| Node.find(n.id)}
104
+ @layer2.collect! {|n| Node.find(n.id)}
105
+ end
106
+
107
+ it "should keep its descendants if it's moved to a different parent" do
108
+ @roots[1].parent = @roots[2]
109
+ @roots[1].save!
110
+
111
+ returned_descendants = @roots[2].descendants
112
+ returned_descendants.sort! {|a,b| a.name <=> b.name}
113
+ actual_descendants = @layer1 + @layer2 + [@roots[1]]
114
+ actual_descendants.sort! {|a,b| a.name <=> b.name}
115
+ returned_descendants.should == actual_descendants
116
+ @roots[0].descendants.should be_empty
117
+
118
+ actual_descendants.delete(@roots[1])
119
+ returned_descendants = @roots[1].descendants
120
+ returned_descendants.sort! {|a,b| a.name <=> b.name}
121
+ returned_descendants.should == actual_descendants
122
+ end
123
+
124
+ it "should destroy its descendants if it's destroyed" do
125
+ @roots[1].destroy
126
+ (@layer1 + @layer2).each do |node|
127
+ lambda { Node.find(node.id) }.should raise_error(ActiveRecord::RecordNotFound)
128
+ end
129
+ end
130
+ end
131
+
132
+ describe "(performance)" do
133
+ SAMPLE_SIZE = 15000
134
+ LAYERS = 10
135
+ TIME_SPEC = ENV['DB'] == 'sqlite' ? 0.2 : 0.1
136
+
137
+ def test_times(times)
138
+ (times.mean + 3 * times.stddev).should satisfy {|n| n < TIME_SPEC}
139
+ slope, offset = linear_regression(times)
140
+ (slope * 1_000_000 + offset).should satisfy {|n| n < TIME_SPEC}
141
+ end
142
+
143
+ unless ENV['SKIP_PERFORMANCE']
144
+ it "should scale with constant insertion and access times < #{(TIME_SPEC * 1000).to_i}ms" do
145
+ Node.connection.execute("TRUNCATE TABLE #{Node.quoted_table_name}") rescue Node.delete_all
146
+ insertion_times = NArray.float(SAMPLE_SIZE)
147
+ parent_times = NArray.float(SAMPLE_SIZE)
148
+ children_times = NArray.float(SAMPLE_SIZE)
149
+ ancestors_times = NArray.float(SAMPLE_SIZE)
150
+ descendants_times = NArray.float(SAMPLE_SIZE)
151
+
152
+ i = -1
153
+ layer = []
154
+ (SAMPLE_SIZE / LAYERS).times do |j|
155
+ insertion_times[i+=1] = measure_time { layer << Node.create!(:name => j.to_s) }
156
+ end
157
+ (LAYERS-1).times do
158
+ new_layer = []
159
+ (SAMPLE_SIZE / LAYERS).times do |j|
160
+ parent = layer[rand(layer.length)]
161
+ insertion_times[i+=1] = measure_time { new_layer << Node.create!(:name => j.to_s, :parent => parent) }
162
+ end
163
+ layer = new_layer
164
+ end
165
+
166
+ ids = Node.connection.select_all("SELECT id FROM #{Node.quoted_table_name}")
167
+ ids.collect! {|row| row["id"].to_i}
168
+ SAMPLE_SIZE.times do |i|
169
+ node = Node.find(ids[rand(ids.length)])
170
+ parent_times[i] = measure_time { node.parent }
171
+ children_times[i] = measure_time { node.children }
172
+ ancestors_times[i] = measure_time { node.ancestors }
173
+ descendants_times[i] = measure_time { node.descendants }
174
+ end
175
+
176
+ test_times(insertion_times)
177
+ test_times(parent_times)
178
+ test_times(children_times)
179
+ test_times(ancestors_times)
180
+ test_times(descendants_times)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ gem 'sqlite3-ruby'
3
+ require 'spec'
4
+ require 'activerecord'
5
+ require 'yaml'
6
+ require 'narray'
7
+
8
+ # Load and activate Hyrarchy.
9
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
10
+ require 'hyrarchy'
11
+ Hyrarchy.activate!
12
+
13
+ # Set up a logger.
14
+ log_path = File.join(File.dirname(__FILE__), 'log')
15
+ File.unlink(log_path) rescue nil
16
+ ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(log_path)
17
+ ActiveRecord::Base.logger.add 0, "\n"
18
+
19
+ # Connect to the test database.
20
+ db_specs = YAML.load_file(File.join(File.dirname(__FILE__), 'database.yml'))
21
+ which_spec = ENV['DB'] || 'mysql'
22
+ ActiveRecord::Base.establish_connection(db_specs[which_spec])
23
+
24
+ # Create a model class for testing.
25
+ class Node < ActiveRecord::Base
26
+ is_hierarchic
27
+ connection.execute("TRUNCATE TABLE #{quoted_table_name}") rescue delete_all
28
+ def inspect; name end
29
+ end
30
+
31
+ # Runs a block and returns how long it took in seconds (with subsecond
32
+ # precision).
33
+ def measure_time(&block)
34
+ start_time = Time.now
35
+ yield
36
+ Time.now - start_time
37
+ end
38
+
39
+ # Calculates the slope and offset of a data set.
40
+ def linear_regression(data)
41
+ sxx = sxy = sx = sy = 0
42
+ data.length.times do |x|
43
+ y = data[x]
44
+ sxy += x*y
45
+ sxx += x*x
46
+ sx += x
47
+ sy += y
48
+ end
49
+ slope = (data.length * sxy - sx * sy) / (data.length * sxx - sx * sx)
50
+ offset = (sy - slope * sx) / data.length
51
+ [slope, offset]
52
+ end
@@ -0,0 +1,31 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class EncodedPathTests < Test::Unit::TestCase
4
+ def setup
5
+ @path = Hyrarchy::EncodedPath(5, 7)
6
+ end
7
+
8
+ def test_next_farey_fraction
9
+ assert_equal(Rational(3, 4), @path.send(:next_farey_fraction))
10
+ end
11
+
12
+ def test_mediant
13
+ assert_equal(Rational(15, 20), @path.send(:mediant, Rational(10, 13)))
14
+ end
15
+
16
+ def test_parent
17
+ assert_equal(Rational(2, 3), @path.parent)
18
+ end
19
+
20
+ def test_depth
21
+ assert_equal(3, @path.depth)
22
+ end
23
+
24
+ def test_first_child
25
+ assert_equal(Rational(8, 11), @path.first_child)
26
+ end
27
+
28
+ def test_next_sibling
29
+ assert_equal(Rational(7, 10), @path.next_sibling)
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+ require 'test/unit'
4
+
5
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
6
+ require 'hyrarchy'
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: DanaDanger-hyrarchy
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Dana Danger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-15 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email:
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - LICENSE
25
+ files:
26
+ - lib/hyrarchy.rb
27
+ - lib/hyrarchy/collection_proxy.rb
28
+ - lib/hyrarchy/encoded_path.rb
29
+ - lib/hyrarchy/awesome_nested_set_compatibility.rb
30
+ - rails_plugin/init.rb
31
+ - README.rdoc
32
+ - spec/create_nodes_table.rb
33
+ - spec/database.yml
34
+ - spec/hyrarchy_spec.rb
35
+ - spec/spec_helper.rb
36
+ - test/encoded_path_test.rb
37
+ - test/test_helper.rb
38
+ - LICENSE
39
+ has_rdoc: true
40
+ homepage: http://github.com/DanaDanger/hyrarchy
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --all
44
+ - --inline-source
45
+ - --line-numbers
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.2.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: A gem and Rails plugin for working with hierarchic data.
67
+ test_files:
68
+ - spec/create_nodes_table.rb
69
+ - spec/database.yml
70
+ - spec/hyrarchy_spec.rb
71
+ - spec/spec_helper.rb
72
+ - test/encoded_path_test.rb
73
+ - test/test_helper.rb