hierarchy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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