hierarchy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,25 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ .bundle
21
+ .rvmrc
22
+
23
+ ## PROJECT::DOCUMENTATION
24
+ .yardoc
25
+ doc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -cfs
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source :rubygems
2
+
3
+ # DEPENDENCIES
4
+ gem 'rails', '>= 3.0'
5
+
6
+ # DEVELOPMENT
7
+ gem 'jeweler'
8
+ gem 'yard'
9
+ gem 'RedCloth', require: 'redcloth'
10
+ gem 'pg'
11
+
12
+ # TEST
13
+ gem 'rspec'
data/Gemfile.lock ADDED
@@ -0,0 +1,100 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ RedCloth (4.2.3)
5
+ abstract (1.0.0)
6
+ actionmailer (3.0.1)
7
+ actionpack (= 3.0.1)
8
+ mail (~> 2.2.5)
9
+ actionpack (3.0.1)
10
+ activemodel (= 3.0.1)
11
+ activesupport (= 3.0.1)
12
+ builder (~> 2.1.2)
13
+ erubis (~> 2.6.6)
14
+ i18n (~> 0.4.1)
15
+ rack (~> 1.2.1)
16
+ rack-mount (~> 0.6.12)
17
+ rack-test (~> 0.5.4)
18
+ tzinfo (~> 0.3.23)
19
+ activemodel (3.0.1)
20
+ activesupport (= 3.0.1)
21
+ builder (~> 2.1.2)
22
+ i18n (~> 0.4.1)
23
+ activerecord (3.0.1)
24
+ activemodel (= 3.0.1)
25
+ activesupport (= 3.0.1)
26
+ arel (~> 1.0.0)
27
+ tzinfo (~> 0.3.23)
28
+ activeresource (3.0.1)
29
+ activemodel (= 3.0.1)
30
+ activesupport (= 3.0.1)
31
+ activesupport (3.0.1)
32
+ arel (1.0.1)
33
+ activesupport (~> 3.0.0)
34
+ builder (2.1.2)
35
+ diff-lcs (1.1.2)
36
+ erubis (2.6.6)
37
+ abstract (>= 1.0.0)
38
+ gemcutter (0.6.1)
39
+ git (1.2.5)
40
+ i18n (0.4.2)
41
+ jeweler (1.4.0)
42
+ gemcutter (>= 0.1.0)
43
+ git (>= 1.2.5)
44
+ rubyforge (>= 2.0.0)
45
+ json_pure (1.4.6)
46
+ mail (2.2.9)
47
+ activesupport (>= 2.3.6)
48
+ i18n (~> 0.4.1)
49
+ mime-types (~> 1.16)
50
+ treetop (~> 1.4.8)
51
+ mime-types (1.16)
52
+ pg (0.9.0)
53
+ polyglot (0.3.1)
54
+ rack (1.2.1)
55
+ rack-mount (0.6.13)
56
+ rack (>= 1.0.0)
57
+ rack-test (0.5.6)
58
+ rack (>= 1.0)
59
+ rails (3.0.1)
60
+ actionmailer (= 3.0.1)
61
+ actionpack (= 3.0.1)
62
+ activerecord (= 3.0.1)
63
+ activeresource (= 3.0.1)
64
+ activesupport (= 3.0.1)
65
+ bundler (~> 1.0.0)
66
+ railties (= 3.0.1)
67
+ railties (3.0.1)
68
+ actionpack (= 3.0.1)
69
+ activesupport (= 3.0.1)
70
+ rake (>= 0.8.4)
71
+ thor (~> 0.14.0)
72
+ rake (0.8.7)
73
+ rspec (2.0.1)
74
+ rspec-core (~> 2.0.1)
75
+ rspec-expectations (~> 2.0.1)
76
+ rspec-mocks (~> 2.0.1)
77
+ rspec-core (2.0.1)
78
+ rspec-expectations (2.0.1)
79
+ diff-lcs (>= 1.1.2)
80
+ rspec-mocks (2.0.1)
81
+ rspec-core (~> 2.0.1)
82
+ rspec-expectations (~> 2.0.1)
83
+ rubyforge (2.0.4)
84
+ json_pure (>= 1.1.7)
85
+ thor (0.14.3)
86
+ treetop (1.4.8)
87
+ polyglot (>= 0.3.1)
88
+ tzinfo (0.3.23)
89
+ yard (0.6.1)
90
+
91
+ PLATFORMS
92
+ ruby
93
+
94
+ DEPENDENCIES
95
+ RedCloth
96
+ jeweler
97
+ pg
98
+ rails (>= 3.0)
99
+ rspec
100
+ yard
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Tim Morgan
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.textile ADDED
@@ -0,0 +1,96 @@
1
+ h1. Hierarchy -- Use PostgreSQL @LTREE@ columns in ActiveRecord
2
+
3
+ | *Author* | Tim Morgan |
4
+ | *Version* | 1.0 (Oct 30, 2010) |
5
+ | *License* | Released under the MIT license. |
6
+
7
+ h2. About
8
+
9
+ The @LTREE@ column type is a PostgreSQL-specific type (installation script is
10
+ available in the @contrib@ directory in your PostgreSQL installation) for
11
+ representing hierarchies. It is more efficient than the typical way of
12
+ accomplishing hierarchal structures in SQL, the @parent_id@ column (or similar).
13
+
14
+ This gem lets you use an @LTREE@-utilizing hierarchy in ActiveRecord. Including
15
+ this gem in your project gets you a) monkey-patches* to Arel to support @LTREE@
16
+ columns, and b) a module you can include in your models, providing an abundance
17
+ of methods to help you navigate and manipulate the hierarchy.
18
+
19
+ *Though I don't know for sure, I suspect when Arel 2.0 is supported in Rails, I
20
+ will be able to remove this monkey-patching ugliness.
21
+
22
+ h2. Installation
23
+
24
+ *Important Note:* This gem requires Ruby 1.9 and Rails 3.0.
25
+
26
+ Firstly, add the gem to your Rails project's @Gemfile@:
27
+
28
+ <pre><code>
29
+ gem 'hierarchy'
30
+ </code></pre>
31
+
32
+ Then, run the generator to install the migration:
33
+
34
+ <pre><code>
35
+ rails generate hierarchy
36
+ </code></pre>
37
+
38
+ Note that *this migration must precede any tables using @LTREEs@*, so reorder
39
+ the migration if you have to.
40
+
41
+ h2. Usage
42
+
43
+ Because this gem was hastily extracted from a personal project, it's a little
44
+ constraining in how it can be used. (Sorry.) Currently the gem requires that
45
+ your table schema have a column named @path@ of type @LTREE@, defined as in the
46
+ example below:
47
+
48
+ <pre><code>
49
+ path LTREE NOT NULL DEFAULT ''
50
+ </code></pre>
51
+
52
+ Once you've got that column in your model, feel free to include the @Hierarchy@
53
+ module:
54
+
55
+ <pre><code>
56
+ class Person < ActiveRecord::Base
57
+ include Hierarchy
58
+ end
59
+ </code></pre>
60
+
61
+ You can now define hierarchy by setting a model's @parent@, like so:
62
+
63
+ <pre><code>
64
+ person.parent = mother #=> Sets the `path` column appropriately
65
+ </code></pre>
66
+
67
+ You also have access to a wealth of ways to traverse the hierarchy:
68
+
69
+ <pre><code>
70
+ person.children.where(gender: :male)
71
+ person.top_level?
72
+ Person.treeified #=> returns a traversable tree of all people
73
+ </code></pre>
74
+
75
+ For more information on what you can do, see the {Hierarchy} module
76
+ documentation.
77
+
78
+ h2. Development
79
+
80
+ If you wish to develop for Hierarchy, the first thing you will want to do is get
81
+ specs up and running. This requires a call to @bundle install@ (obviously) and
82
+ setting up your test database.
83
+
84
+ As you can see in the @spec/spec_helper.rb@ file, the specs require that a
85
+ PostgreSQL database named @hierarchy_test@ exist and be owned by a
86
+ @hierarchy_tester@ user. Unfortunately I haven't written a way to configure this
87
+ (though patches are welcome). So, the following commands should suffice to get
88
+ you started:
89
+
90
+ <pre><code>
91
+ createuser hierarchy_tester # answer "no" to all prompts
92
+ createdb -O hierarchy_tester hierarchy_test
93
+ </code></pre>
94
+
95
+ With those steps done you should be able to run @rake spec@ and see the Glorious
96
+ Green.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ begin
3
+ require 'bundler'
4
+ rescue LoadError
5
+ puts "Bundler is not installed; install with `gem install bundler`."
6
+ exit 1
7
+ end
8
+
9
+ Bundler.require :default
10
+
11
+ Jeweler::Tasks.new do |gem|
12
+ gem.name = "hierarchy"
13
+ gem.summary = %Q{Use PostgreSQL LTREE type with ActiveRecord}
14
+ gem.description = %Q{Adds ActiveRecord support for hierarchial data structures using PostgreSQL's LTREE column type.}
15
+ gem.email = "git@timothymorgan.info"
16
+ gem.homepage = "http://github.com/riscfuture/hierarchy"
17
+ gem.authors = [ "Tim Morgan" ]
18
+ gem.required_ruby_version = '>= 1.9'
19
+ gem.add_dependency 'activerecord', '>= 3.0'
20
+ gem.add_dependency 'activesupport', '>= 3.0'
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+
24
+ require 'rspec/core/rake_task'
25
+ RSpec::Core::RakeTask.new
26
+
27
+ YARD::Rake::YardocTask.new('doc') do |doc|
28
+ doc.options << "-m" << "textile"
29
+ doc.options << "--protected"
30
+ doc.options << "-r" << "README.textile"
31
+ doc.options << "-o" << "doc"
32
+ doc.options << "--title" << "Hierarchy Documentation".inspect
33
+
34
+ doc.files = [ 'lib/**/*', 'README.textile' ]
35
+ end
36
+
37
+ task(default: :spec)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/hierarchy.gemspec ADDED
@@ -0,0 +1,67 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{hierarchy}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Tim Morgan"]
12
+ s.date = %q{2010-10-30}
13
+ s.description = %q{Adds ActiveRecord support for hierarchial data structures using PostgreSQL's LTREE column type.}
14
+ s.email = %q{git@timothymorgan.info}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.textile"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ ".rspec",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE",
26
+ "README.textile",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "hierarchy.gemspec",
30
+ "lib/hierarchy.rb",
31
+ "lib/hierarchy/index_path.rb",
32
+ "lib/hierarchy/node.rb",
33
+ "lib/hierarchy_generator.rb",
34
+ "spec/hierarchy_spec.rb",
35
+ "spec/index_path_spec.rb",
36
+ "spec/spec_helper.rb",
37
+ "templates/add_ltree_type.rb"
38
+ ]
39
+ s.homepage = %q{http://github.com/riscfuture/hierarchy}
40
+ s.rdoc_options = ["--charset=UTF-8"]
41
+ s.require_paths = ["lib"]
42
+ s.required_ruby_version = Gem::Requirement.new(">= 1.9")
43
+ s.rubygems_version = %q{1.3.7}
44
+ s.summary = %q{Use PostgreSQL LTREE type with ActiveRecord}
45
+ s.test_files = [
46
+ "spec/hierarchy_spec.rb",
47
+ "spec/index_path_spec.rb",
48
+ "spec/spec_helper.rb"
49
+ ]
50
+
51
+ if s.respond_to? :specification_version then
52
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<activerecord>, [">= 3.0"])
57
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0"])
58
+ else
59
+ s.add_dependency(%q<activerecord>, [">= 3.0"])
60
+ s.add_dependency(%q<activesupport>, [">= 3.0"])
61
+ end
62
+ else
63
+ s.add_dependency(%q<activerecord>, [">= 3.0"])
64
+ s.add_dependency(%q<activesupport>, [">= 3.0"])
65
+ end
66
+ end
67
+
@@ -0,0 +1,59 @@
1
+ module Hierarchy
2
+
3
+ # An array of integers representing an ordered list of IDs. Duck-types an
4
+ # @Array@ in many ways.
5
+
6
+ class IndexPath
7
+ include Enumerable
8
+
9
+ delegate :&, :+, :-, :<<, :==, :[], :[]=, :at, :clear, :collect, :collect!,
10
+ :concat, :count, :delete_at, :delete_if, :drop, :drop_while, :each,
11
+ :each_index, :empty?, :eql?, :fetch, :index, :first, :include?,
12
+ :insert, :join, :keep_if, :last, :length, :map, :map!, :pop, :push,
13
+ :reject, :reject!, :reverse, :reverse!, :rindex, :select, :select!,
14
+ :shift, :size, :slice, :slice!, :take, :take_while, :to_a, :to_ary,
15
+ :unshift, :values_at, :|, to: :indexes
16
+
17
+ # @overload initialize(id, ...)
18
+ # Creates an index path from a list of integer IDs.
19
+ # @param [Fixnum] id An integer ID.
20
+ # @return [IndexPath] A new instance.
21
+ # @raise [ArgumentError] If an invalid ID is given.
22
+
23
+ def initialize(*indexes)
24
+ raise ArgumentError, "IndexPath indexes must be integers" unless indexes.all? { |index| index.kind_of?(Fixnum) }
25
+ @indexes = indexes
26
+ end
27
+
28
+ # Creates an index path from a PostgreSQL @LTREE@ column.
29
+ #
30
+ # @param [String] string An @LTREE@ column value, such as "1.10.22".
31
+ # @return [IndexPath] A corresponding index path.
32
+
33
+ def self.from_ltree(string)
34
+ new(*(string.split('.').map(&:to_i)))
35
+ end
36
+
37
+ # Defines a natural ordering of index paths. Paths with lower IDs at the same
38
+ # index level will come before those with higher IDs at that index level.
39
+ # Lower IDs at shallower index levels come before lower IDs at deeper index
40
+ # levels.
41
+ #
42
+ # @param [IndexPath] other An index path to compare.
43
+ # @return [-1, 0, 1] -1 if this index path comes before the other one, 0 if
44
+ # they are identical, or 1 if this index path comes after the other one.
45
+ # @raise [ArgumentError] If something other than an index path is given.
46
+
47
+ def <=>(other)
48
+ raise ArgumentError, "Can't compare IndexPath and #{other.class.to_s}" unless other.kind_of?(IndexPath)
49
+ indexes <=> other.send(:indexes)
50
+ end
51
+
52
+ # @private
53
+ def inspect() "#<#{self.class.to_s} #{@indexes.inspect}>" end
54
+
55
+ private
56
+
57
+ def indexes() @indexes end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ module Hierarchy
2
+
3
+ # A node in a tree structure. A node can have zero or more {#children}, and
4
+ # has a reverse link back to its parent.
5
+
6
+ class Node
7
+ # @return [Array<Node>] This node's children.
8
+ attr_reader :children
9
+
10
+ # @return [Node, nil] This node's parent, or @nil@ if it is a root node.
11
+ attr_reader :parent
12
+
13
+ # @return The object this node contains.
14
+ attr_accessor :content
15
+
16
+ # Creates a new root node with no children.
17
+ #
18
+ # @param content The content the node will contain.
19
+
20
+ def initialize(content)
21
+ @children = []
22
+ @content = content
23
+ end
24
+
25
+ # Adds a node as a child of this one. Sets the {#parent} of the given node.
26
+ #
27
+ # @param [Node] child The node to add as a child of this node.
28
+
29
+ def <<(child)
30
+ children << child
31
+ child.instance_variable_set :@parent, self
32
+ end
33
+
34
+ # Performs a depth-first traversal using this as the root node of the tree.
35
+ # @yield [node] Each node in depth-first order.
36
+ # @yieldparam [Node] node A child node.
37
+
38
+ def traverse(&block)
39
+ children.each { |child| child.traverse &block }
40
+ block[self]
41
+ end
42
+
43
+ # @private
44
+ def inspect
45
+ str = "#<#{self.class.to_s} #{content.inspect}"
46
+ unless children.empty?
47
+ str << ": [ #{children.map(&:inspect).join(', ')} ]"
48
+ end
49
+ str << ">"
50
+ str
51
+ end
52
+ end
53
+ end
data/lib/hierarchy.rb ADDED
@@ -0,0 +1,166 @@
1
+ # @private
2
+ module Arel
3
+ # @private
4
+ module Sql
5
+ # @private
6
+ module Attributes
7
+ # @private
8
+ def self.for_with_psql(column)
9
+ case column.sql_type
10
+ when 'ltree' then String
11
+ else for_without_psql(column)
12
+ end
13
+ end
14
+ unless singleton_class.method_defined?(:for_without_psql)
15
+ singleton_class.send :alias_method, :for_without_psql, :for
16
+ singleton_class.send :alias_method, :for, :for_with_psql
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ require 'hierarchy_generator'
23
+ require 'hierarchy/index_path'
24
+ require 'hierarchy/node'
25
+
26
+ # Adds a tree structure to a model. This is very similar to @acts_as_nested_set@
27
+ # but uses the PostgreSQL-specific @ltree@ feature for schema storage.
28
+ #
29
+ # Your model must have a @path@ field of type @ltree@. This field will be a
30
+ # period-delimited list of IDs of records above this one in the hierarchy. In
31
+ # addition, you should also consider the following indexes:
32
+ #
33
+ # <pre><code>
34
+ # CREATE INDEX index1 ON table USING gist(path)
35
+ # CREATE INDEX index2 ON table USING btree(path)
36
+ # </code></pre>
37
+ #
38
+ # replacing @table@ with your table and @index1@/@index2@ with appropriate names
39
+ # for these indexes.
40
+ #
41
+ # @example
42
+ # class MyModel < ActiveRecord::Base
43
+ # include Hierarchy
44
+ # end
45
+
46
+ module Hierarchy
47
+ extend ActiveSupport::Concern
48
+
49
+ # @private
50
+ included do |base|
51
+ base.extend ActiveSupport::Memoizable
52
+ base.memoize :index_path, :ancestors
53
+
54
+ base.scope :parent_of, ->(obj) { obj.top_level? ? base.where('false') : base.where(id: obj.index_path.last) }
55
+ base.scope :children_of, ->(obj) { base.where(path: obj.my_path) }
56
+ base.scope :ancestors_of, ->(obj) { base.where(id: obj.index_path.to_a) }
57
+ base.scope :descendants_of, ->(obj) { base.where([ "path <@ ?", obj.my_path ]) }
58
+ base.scope :siblings_of, ->(obj) { base.where(path: obj.path) }
59
+ base.scope :priority_order, base.order("NLEVEL(path) ASC")
60
+
61
+ base.before_save { |obj| obj.path ||= '' }
62
+ end
63
+
64
+ module ClassMethods
65
+
66
+ # @overload treeified
67
+ # @return [Hash<ActiveRecord::Base, Hash<...>>] All models organized
68
+ # into a tree structure. Returns a hash where each key is a tree root,
69
+ # and the values are themselves hashes whose keys are the children of the
70
+ # respective model.
71
+
72
+ def treeified(root=nil, objects=nil)
73
+ path = root ? root.content.my_path : ''
74
+ root ||= Node.new(nil)
75
+ objects ||= order('id ASC').all.sort_by { |o| [ o.index_path, o.id ] }
76
+
77
+ while objects.first and objects.first.path == path
78
+ child = objects.shift
79
+ root << Node.new(child)
80
+ end
81
+
82
+ root.children.each do |child|
83
+ treeified child, objects
84
+ end
85
+
86
+ return root
87
+ end
88
+ end
89
+
90
+ # Methods added to instances of the class this module is included into.
91
+
92
+ module InstanceMethods
93
+ # Sets the object above this one in the hierarchy.
94
+ #
95
+ # @param [ActiveRecord::Base] parent The parent object.
96
+ # @raise [ArgumentError] If @parent@ is an unsaved record with no primary key.
97
+
98
+ def parent=(parent)
99
+ raise ArgumentError, "Parent cannot be a new record" if parent.try(:new_record?)
100
+ self.path = parent.try(:my_path)
101
+ end
102
+
103
+ # Returns an array of ancestors above this object. Note that a) this array
104
+ # is ordered with the most senior ancestor at the beginning of the list, and
105
+ # b) this is an _array_, not a _relation_. For that reason, you can pass
106
+ # any additional scope options to the method.
107
+ #
108
+ # @param [Hash] options Additional finder options.
109
+ # @return [Array] The objects above this one in the hierarchy.
110
+
111
+ def ancestors(options={})
112
+ objects = self.class.ancestors_of(self).scoped(options).group_by(&:id)
113
+ index_path.map { |id| objects[id].first }
114
+ end
115
+
116
+ # @return [ActiveRecord::Relation] The objects below this one in the
117
+ # hierarchy.
118
+
119
+ def descendants
120
+ self.class.descendants_of self
121
+ end
122
+
123
+ # @return [ActiveRecord::Base] The object directly above this one in the
124
+ # hierarchy.
125
+
126
+ def parent
127
+ top_level? ? nil : self.class.parent_of(self).first
128
+ end
129
+
130
+ # @return [ActiveRecord::Relation] The objects directly below this one
131
+ # in the hierarchy.
132
+
133
+ def children
134
+ self.class.children_of self
135
+ end
136
+
137
+ # @return [Array] The objects at the same hierarchical level of this one.
138
+
139
+ def siblings
140
+ self.class.siblings_of(self) - [ self ]
141
+ end
142
+
143
+ # @return [true, false] Whether or not this object has no parents.
144
+
145
+ def top_level?
146
+ path == ''
147
+ end
148
+
149
+ # @return [true, false] Whether or not this object has no children. Makes a
150
+ # database call.
151
+
152
+ def bottom_level?
153
+ children.empty?
154
+ end
155
+
156
+ # @private
157
+ def my_path
158
+ path.blank? ? id.to_s : "#{path}.#{id}"
159
+ end
160
+
161
+ # @private
162
+ def index_path
163
+ IndexPath.from_ltree path
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators'
3
+ require 'rails/generators/migration'
4
+
5
+ # @private
6
+ class HierarchyGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root "#{File.dirname __FILE__}/../templates"
10
+
11
+ def self.next_migration_number(dirname)
12
+ if ActiveRecord::Base.timestamped_migrations then
13
+ Time.now.utc.strftime "%Y%m%d%H%M%S"
14
+ else
15
+ "%.3d" % (current_migration_number(dirname) + 1)
16
+ end
17
+ end
18
+
19
+ def copy_files
20
+ migration_template "add_ltree_type.rb", "db/migrate/add_ltree_type.rb"
21
+ end
22
+ end
@@ -0,0 +1,110 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Hierarchy do
4
+ before :each do
5
+ @objects = Hash.new
6
+ 2.times { |i| @objects[:"grandparent_#{i + 1}"] = Model.create! }
7
+ 2.times do |gp_num|
8
+ 2.times { |i| @objects[:"parent_#{gp_num + 1}_#{i + 1}"] = Model.create!(parent: @objects[:"grandparent_#{gp_num + 1}"]) }
9
+ end
10
+ 2.times do |gp_num|
11
+ 2.times do |p_num|
12
+ 2.times { |i| @objects[:"child_#{gp_num + 1}_#{p_num + 1}_#{i + 1}"] = Model.create!(parent: @objects[:"parent_#{gp_num + 1}_#{p_num + 1}"]) }
13
+ end
14
+ end
15
+ end
16
+
17
+ describe ".parent_of" do
18
+ it "should return the parent object" do
19
+ Model.parent_of(@objects[:parent_1_1]).should == [ @objects[:grandparent_1] ]
20
+ Model.parent_of(@objects[:parent_2_2]).should == [ @objects[:grandparent_2] ]
21
+ end
22
+
23
+ it "should return an empty relation for top-level objects" do
24
+ Model.parent_of(@objects[:grandparent_1]).should be_empty
25
+ Model.parent_of(@objects[:grandparent_2]).should be_empty
26
+ end
27
+ end
28
+
29
+ describe ".children_of" do
30
+ it "should return the direct children of an object" do
31
+ Model.children_of(@objects[:grandparent_1]).should == [ :parent_1_1, :parent_1_2 ].map { |name| @objects[name] }
32
+ Model.children_of(@objects[:parent_2_1]).should == [ :child_2_1_1, :child_2_1_2 ].map { |name| @objects[name] }
33
+ end
34
+
35
+ it "should return an empty relation for leaf objects" do
36
+ Model.children_of(@objects[:child_1_1_1]).should be_empty
37
+ Model.children_of(@objects[:child_2_2_2]).should be_empty
38
+ end
39
+ end
40
+
41
+ describe ".ancestors_of" do
42
+ it "should return all ancestors of an object" do
43
+ Model.ancestors_of(@objects[:child_1_2_1]).should == [ :grandparent_1, :parent_1_2 ].map { |name| @objects[name] }
44
+ Model.ancestors_of(@objects[:parent_2_1]).should == [ @objects[:grandparent_2] ]
45
+ end
46
+
47
+ it "should return an empty relation for top-level objects" do
48
+ Model.ancestors_of(@objects[:grandparent_1]).should be_empty
49
+ end
50
+ end
51
+
52
+ describe ".descendants_of" do
53
+ it "should return all descendants of an object" do
54
+ Model.descendants_of(@objects[:grandparent_2]).should == [ :parent_2_1, :parent_2_2, :child_2_1_1, :child_2_1_2, :child_2_2_1, :child_2_2_2 ].map { |name| @objects[name] }
55
+ Model.descendants_of(@objects[:parent_2_1]).should == [ :child_2_1_1, :child_2_1_2 ].map { |name| @objects[name] }
56
+ end
57
+
58
+ it "should return an empty relation for leaf objects" do
59
+ Model.children_of(@objects[:child_1_1_1]).should be_empty
60
+ Model.children_of(@objects[:child_2_2_2]).should be_empty
61
+ end
62
+ end
63
+
64
+ describe ".siblings_of" do
65
+ it "should return all sibling objects" do
66
+ Model.siblings_of(@objects[:parent_1_1]).should == [ :parent_1_1, :parent_1_2 ].map { |name| @objects[name] }
67
+ end
68
+ end
69
+
70
+ describe "#parent=" do
71
+ it "should raise an error if parent is unsaved" do
72
+ expect { Model.create!.parent = Model.new }.to raise_error(ArgumentError)
73
+ end
74
+
75
+ it "should set the path ltree appropriately" do
76
+ ggp = Model.create!
77
+ gp = Model.create!(path: ggp.id.to_s)
78
+ pa = Model.create!(path: "#{ggp.id}.#{gp.id}")
79
+ path = [ ggp.id, gp.id, pa.id ].join('.')
80
+
81
+ object = Model.new
82
+ parent = Model.create!(path: path)
83
+ object.parent = parent
84
+ object.save!
85
+ object.path.should eql("#{path}.#{parent.id}")
86
+ end
87
+ end
88
+
89
+ describe "#top_level?" do
90
+ it "should return true for a top-level object" do
91
+ Model.create!.should be_top_level
92
+ end
93
+
94
+ it "should return false for an object with a parent" do
95
+ parent = Model.create!
96
+ Model.create!(parent: parent).should_not be_top_level
97
+ end
98
+ end
99
+
100
+ describe "#bottom_level?" do
101
+ it "should return true for an object with no children" do
102
+ Model.create!.should be_bottom_level
103
+ end
104
+ it "should return false for an object with children" do
105
+ parent = Model.create!
106
+ Model.create!(parent: parent)
107
+ parent.should_not be_bottom_level
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,31 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Hierarchy::IndexPath do
4
+ describe ".<=>" do
5
+ def compare(ip1, ip2)
6
+ Hierarchy::IndexPath.new(*ip1) <=> Hierarchy::IndexPath.new(*ip2)
7
+ end
8
+
9
+ it "should return -1 if this index path comes before the given index path" do
10
+ # all except last digit equal
11
+ compare([ 1,2,3 ], [ 1,2,4 ]).should eql(-1)
12
+ # unequal in higher precedence
13
+ compare([ 1,2,3 ], [ 1,3,2 ]).should eql(-1)
14
+ # other is longer
15
+ compare([ 1,2,3 ], [ 1,2,3,4 ]).should eql(-1)
16
+ end
17
+
18
+ it "should return 1 if this index path comes after the given index path" do
19
+ # all except last digit equal
20
+ compare([ 1,2,4 ], [ 1,2,3 ]).should eql(1)
21
+ # unequal in higher precedence
22
+ compare([ 1,3,2 ], [ 1,2,3 ]).should eql(1)
23
+ # this is longer
24
+ compare([ 1,2,3,4 ], [ 1,2,3, ]).should eql(1)
25
+ end
26
+
27
+ it "should return 0 if the index paths are equal" do
28
+ compare([ 1,2,3 ], [ 1,2,3 ]).should eql(0)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ Bundler.require :default, :test
2
+ require 'active_support'
3
+ require 'active_record'
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+
8
+ require 'hierarchy'
9
+
10
+ ActiveRecord::Base.establish_connection(
11
+ adapter: 'postgresql',
12
+ database: 'hierarchy_test',
13
+ username: 'hierarchy_tester'
14
+ )
15
+ system "psql -f `pg_config --sharedir`/contrib/ltree.sql hierarchy_test &>/dev/null"
16
+
17
+ class Model < ActiveRecord::Base
18
+ include Hierarchy
19
+ end
20
+
21
+ RSpec.configure do |config|
22
+ config.before(:each) do
23
+ Model.connection.execute "DROP TABLE IF EXISTS models"
24
+ Model.connection.execute "CREATE TABLE models (id SERIAL PRIMARY KEY, path LTREE NOT NULL DEFAULT '')"
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ class AddLtreeType < ActiveRecord::Migration
2
+ def self.up
3
+ cmd = "psql -f `pg_config --sharedir`/contrib/ltree.sql #{ActiveRecord::Base.connection.instance_variable_get(:@config)[:database]}"
4
+ puts cmd
5
+ result = system(cmd)
6
+ raise "Bad exit" unless result
7
+ end
8
+
9
+ def self.down
10
+ cmd = "psql -f `pg_config --sharedir`/contrib/uninstall_ltree.sql #{ActiveRecord::Base.connection.instance_variable_get(:@config)[:database]}"
11
+ puts cmd
12
+ result = system(cmd)
13
+ raise "Bad exit" unless result
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hierarchy
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Tim Morgan
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-30 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 0
30
+ version: "3.0"
31
+ type: :runtime
32
+ prerelease: false
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: activesupport
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 3
43
+ - 0
44
+ version: "3.0"
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *id002
48
+ description: Adds ActiveRecord support for hierarchial data structures using PostgreSQL's LTREE column type.
49
+ email: git@timothymorgan.info
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - LICENSE
56
+ - README.textile
57
+ files:
58
+ - .document
59
+ - .gitignore
60
+ - .rspec
61
+ - Gemfile
62
+ - Gemfile.lock
63
+ - LICENSE
64
+ - README.textile
65
+ - Rakefile
66
+ - VERSION
67
+ - hierarchy.gemspec
68
+ - lib/hierarchy.rb
69
+ - lib/hierarchy/index_path.rb
70
+ - lib/hierarchy/node.rb
71
+ - lib/hierarchy_generator.rb
72
+ - spec/hierarchy_spec.rb
73
+ - spec/index_path_spec.rb
74
+ - spec/spec_helper.rb
75
+ - templates/add_ltree_type.rb
76
+ has_rdoc: true
77
+ homepage: http://github.com/riscfuture/hierarchy
78
+ licenses: []
79
+
80
+ post_install_message:
81
+ rdoc_options:
82
+ - --charset=UTF-8
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ segments:
91
+ - 1
92
+ - 9
93
+ version: "1.9"
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ requirements: []
103
+
104
+ rubyforge_project:
105
+ rubygems_version: 1.3.7
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: Use PostgreSQL LTREE type with ActiveRecord
109
+ test_files:
110
+ - spec/hierarchy_spec.rb
111
+ - spec/index_path_spec.rb
112
+ - spec/spec_helper.rb