xylem 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1f36a0a448ea0f87bbb5bb18b7ebb4f090e8a724
4
+ data.tar.gz: 74c92b1a1d23c3524480c3c97521f5f1bf3395b7
5
+ SHA512:
6
+ metadata.gz: 25b9ac95f3a6c51d2abc5195269a7f6bab89376dffb32034db0a9aad18cf52974083ac5666515cdd0ebb6d93b77f68c80d1253ff256a4900e271ab99f6aec9b3
7
+ data.tar.gz: 9406bb0aec116ecee01b8f7dfdd3c5e3f190580eba20afb2f8e3a2a0fe57b81b4a23aa1d2d68889ed92fdd7adf6f2e982d55cf9429d223bfb7f9c9e57ad962c1
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.vagrant/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /gemfiles/*.gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
12
+ *.bundle
13
+ *.so
14
+ *.o
15
+ *.a
16
+ mkmf.log
data/.travis.yml ADDED
@@ -0,0 +1,35 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.3.0
5
+ - 2.2.4
6
+ - 2.1.8
7
+ - 2.0.0
8
+ - 1.9.3
9
+ gemfile:
10
+ - gemfiles/ar_4.0.gemfile
11
+ - gemfiles/ar_4.1.gemfile
12
+ - gemfiles/ar_4.2.gemfile
13
+ - gemfiles/ar_5beta.gemfile
14
+ before_script:
15
+ - psql -c 'create database xylem_test;' -U postgres
16
+ addons:
17
+ apt:
18
+ sources:
19
+ - debian-sid
20
+ packages:
21
+ - sqlite3
22
+ env:
23
+ matrix:
24
+ - DB=sqlite
25
+ - DB=postgres
26
+ global:
27
+ secure: "PSK+NqEOpz6P3lDxQ/dIYg6F2FQrdtrkiE72hbL2S0uJ+BJMGp/Uw3h23ihUmlJAPqjNjtxyoslKYLiCiUje/6ID7DhxL8q2Yerqtf0mnMnHrX2mmSkrXrLva9RkMEbpXlHUVT7eZyEZKaw/3ZL917XPo2/M/dX8e0zwtUjhfOEejzXvWuW53A+B155ghpKnB9krbI71CXzK9fsPGR9fi4Mb7+eyZ24Qi3snrRADz9gQbpCG6rLs1j0E052NnLCttkMYU0d5+K9VI+eyDnWboiPThDY/iCw7arTkE6HPjnPbtZp2/GyiOQMpGxV8pzBrtAiy167wUzzhdhtKhLUvl0RUKyRY811N9EJAXEoXydf+r/SHCYNNrTaKKf/dMuTJT1YKFiqthlFS3lFT4f3JduRoPT0YsvPliAWDSdIG1XOINg5DnrNtVgKdwfLjb4klUK4MAw2cwF2M0iKbaNaj9AAduMctRxJyfY91+uCMOnnj4KXNXDzcUjLqXyZPd2qPBWf9wU1lL7mll/B2UDHuAaKqVDe18yut4LeszN0JFClZdGNlSdYXfj/1+C5b80hd5f8ckzlZ5ZYkksOTPDcByev1uvkDThSixru+LI8kYyTENgRAptMSHrUFmGn4XINELOvJi9dsjq9nhSC3DP30dCcddQalcxcSZQI8EeWOVb4="
28
+ matrix:
29
+ exclude:
30
+ - rvm: 1.9.3
31
+ gemfile: gemfiles/ar_5beta.gemfile
32
+ - rvm: 2.0.0
33
+ gemfile: gemfiles/ar_5beta.gemfile
34
+ - rvm: 2.1.8
35
+ gemfile: gemfiles/ar_5beta.gemfile
data/Appraisals ADDED
@@ -0,0 +1,15 @@
1
+ appraise 'ar-4.0' do
2
+ gem 'activerecord', '~> 4.0'
3
+ end
4
+
5
+ appraise 'ar-4.1' do
6
+ gem 'activerecord', '~> 4.1'
7
+ end
8
+
9
+ appraise 'ar-4.2' do
10
+ gem 'activerecord', '~> 4.2'
11
+ end
12
+
13
+ appraise 'ar-5beta' do
14
+ gem 'activerecord', '5.0.0.beta1'
15
+ end
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'codeclimate-test-reporter', group: :test, require: nil
4
+ gem 'appraisal'
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Xylem
2
+ [![Build Status](https://travis-ci.org/oesgalha/xylem.svg)](https://travis-ci.org/oesgalha/xylem)
3
+ [![Code Climate](https://codeclimate.com/github/oesgalha/xylem/badges/gpa.svg)](https://codeclimate.com/github/oesgalha/xylem)
4
+ [![Test Coverage](https://codeclimate.com/github/oesgalha/xylem/badges/coverage.svg)](https://codeclimate.com/github/oesgalha/xylem/coverage)
5
+ [![Dependency Status](https://gemnasium.com/oesgalha/xylem.svg)](https://gemnasium.com/oesgalha/xylem)
6
+
7
+ Xylem provides a simple way to store and retrieve hierarchical data in ActiveRecord.
8
+
9
+ ## What
10
+
11
+ Xylem uses the Adjacency List approach to store hierarchical data, and use [recursive CTEs](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression) to query through it.
12
+
13
+ That means that the storage strategy is simple: in order to map an ActiveRecord Model to a tree-like structure with parents and children, it's needed to add only one column to the table which contains a node's parent id. If the node is a root node that column will have a null value (or `nil`). With that, the insertion and removal of nodes is a simple process and thus it should be simple to recover a tree in a corrupted state and guarantee data consistency.
14
+
15
+ Also queries that traverse the tree (such as get ancestors or descendants of a node) are made in one single recursive SQL statement.
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'xylem', github: 'oesgalha/xylem'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ $ bundle
28
+
29
+ Now add a `parent_id` column to your ActiveRecord::Base model.
30
+ Let's suppose that you want to add the tree behavior to a model called `Menu` with a database table called `menus`.
31
+
32
+ You could invoke the rails generator tool like this:
33
+ ```
34
+ rails g migration add_parent_to_menus parent:references
35
+ ```
36
+
37
+ Or you could create the migration by hand, with something like that:
38
+ ```ruby
39
+ class AddParentToMenus < ActiveRecord::Migration
40
+ def change
41
+ add_reference :menus, :parent, index: true, foreign_key: true
42
+ end
43
+ end
44
+ ```
45
+
46
+ Some notes regarding migration:
47
+ * Add an index is optional, but recommended for performance sake.
48
+ * Add a foreign key is optional, but it's recommended to guarantee data consistency
49
+ * The `foreign_key: true` option is available in [rails 4.2.1](https://github.com/rails/rails/blob/v4.2.1/activerecord/CHANGELOG.md) or newer
50
+
51
+ Now you need to enable the tree behavior in or model by adding the `acts_as_tree` directive in it.
52
+ Let's suppose again that you're dealing with a model called `Menu`, you should add the following:
53
+
54
+ ```ruby
55
+ class Menu < ActiveRecord::Base
56
+ acts_as_tree
57
+ end
58
+ ```
59
+
60
+ And you are ready to go! Check the config options in the Usage section below.
61
+
62
+ ## Usage
63
+
64
+ ### acts_as_tree options
65
+
66
+ * :counter_cache => The name of the column that will cache the node children count. In order to use this, you need to create an integer column with the same name (ex: `counter_cache: :children_count`).
67
+ * :touch => If `true`, when you updated or destroy a node, it's ancestors will be touched: `updated_at` column is updated with the current time. (the default value is `false`)
68
+ * :dependent => Controls what happens with the children of a deleted node. Choose one of the following options (the default value is :destroy)
69
+ * :destroy children are also destroyed.
70
+ * :delete_all delete children direct in the database (this will skip callbacks)
71
+ * :nullify set the children's parent_id to NULL (nil), therefore turning them into new roots (this will also skip callbacks)
72
+ * :restrict_with_exception an exception is raised if there is an attempt to destroy a record with children
73
+ * :restrict_with_error a validation error is added to the record if there is an attempt to destroy it and it has children
74
+
75
+ ### Class methods
76
+
77
+ Xylem adds the following class methods to a class with the `acts_as_tree` directive:
78
+
79
+ * `root`: returns the first root of the tree
80
+ * `roots`: returns all the roots from the tree
81
+ * `leaves`: returns all the leaves from the tree
82
+
83
+ ### Instance methods
84
+
85
+ Xylem adds the following class methods to a class with the `acts_as_tree` directive:
86
+
87
+ * `ancestors`: returns the ancestors from root to the parent of the node. Ordered from the root to the parent.
88
+ * `self_and_ancestors` returns the ancestors from the root to the node itself. Ordered from the root to the node.
89
+ * `descendants` returns all the descendants of the given node. Ordered by depth from the closer to the node to the more distant ones.
90
+ * `self_and_descendants` returns the node and all it's descendants. Ordered by depth with the node first and the descendants later.
91
+ * `root` returns the current node root.
92
+ * `siblings` returns the siblings from the node, excluding itself.
93
+ * `self_and_siblings` returns the node and it's siblings.
94
+ * `children` returns the direct children of the node.
95
+ * `self_and_children` returns the node and it's direct children.
96
+ * `parent` returns the node parent.
97
+ * `root?` returns true if the current node is a root, false otherwise.
98
+ * `leaf?` returns true if the current node is a leaf, false otherwise.
99
+
100
+ ### Examples
101
+
102
+ ```ruby
103
+ class Menu < ActiveRecord::Base
104
+ acts_as_tree
105
+ end
106
+
107
+ root = Menu.create!(name: 'root menu')
108
+ child1 = root.children.create!('option 1')
109
+ child2 = Menu.create!(name: 'option 2', parent: root)
110
+ subchild = Menu.create!(name: 'suboption 1', parent: child2)
111
+
112
+ Menu.roots # => [ root ]
113
+ Menu.leaves # => [ child1, subchild ]
114
+
115
+ root.root? # => true
116
+ child1.leaf? # => true
117
+
118
+ child2.self_and_descendants # => [ child2, subchild ]
119
+
120
+ child1.root # => root
121
+ child1.self_and_ancestors # => [ root, child1 ]
122
+
123
+ child1.self_and_siblings # => [ child1, child2 ]
124
+ ```
125
+
126
+ ## Dependencies
127
+
128
+ * ActiveRecord 4+
129
+
130
+ This gem relies on the `Recursive CTE` feature which was introduced to SQL in the 1999 revision.
131
+ So you need a database management system that implements it. This gem is tested against PostgreSQL and SQLite, those started to support the recursive CTE in the following versions:
132
+ * PostgreSQL 8.4+
133
+ * SQLite 3.8.3+
134
+
135
+ It seems that MySQL still has no support to it. If you use MySQL you can look for other gems to help you deal with models organized in trees, check the Alternatives section bellow.
136
+
137
+ This gem is tested against ruby 1.9.3 and newer only.
138
+
139
+ ## Alternatives
140
+
141
+ There are other gems that allow you to treat models like trees, with different approaches.
142
+ Here are some other gems you could use to achieve the same objective:
143
+
144
+ * [acts_as_tree](https://github.com/amerine/acts_as_tree)
145
+ * [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set)
146
+ * [ancestry](https://github.com/stefankroes/ancestry)
147
+
148
+ ## Contributing
149
+
150
+ 1. Fork it ( https://github.com/oesgalha/xylem/fork )
151
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
152
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
153
+ 4. Push to the branch (`git push origin my-new-feature`)
154
+ 5. Create a new Pull Request
155
+
156
+ ## License
157
+
158
+ Copyright (c) 2015-2016 Oscar Esgalha
159
+
160
+ MIT License
161
+
162
+ Permission is hereby granted, free of charge, to any person obtaining
163
+ a copy of this software and associated documentation files (the
164
+ "Software"), to deal in the Software without restriction, including
165
+ without limitation the rights to use, copy, modify, merge, publish,
166
+ distribute, sublicense, and/or sell copies of the Software, and to
167
+ permit persons to whom the Software is furnished to do so, subject to
168
+ the following conditions:
169
+
170
+ The above copyright notice and this permission notice shall be
171
+ included in all copies or substantial portions of the Software.
172
+
173
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
174
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
175
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
176
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
177
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
178
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
179
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ task :test do
4
+ $LOAD_PATH.unshift('lib', 'test')
5
+ require './test/xylem_tests.rb'
6
+ end
7
+
8
+ task default: :test
data/Vagrantfile ADDED
@@ -0,0 +1,15 @@
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5
+ VAGRANTFILE_API_VERSION = "2"
6
+
7
+ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8
+ config.vm.box = 'ubuntu/trusty64'
9
+ config.vm.network :private_network, ip: "192.168.33.78"
10
+ config.vm.synced_folder '.', '/vagrant', nfs: true
11
+ config.ssh.forward_agent = true
12
+ config.vm.define "xylem_develop" do |xylem_develop|
13
+ end
14
+ config.vm.provision :shell, path: 'bootstrap.sh', keep_color: true
15
+ end
@@ -0,0 +1,32 @@
1
+ [
2
+ {
3
+ "name": "insertion",
4
+ "ips": 412.7091261183738,
5
+ "stddev": 51
6
+ },
7
+ {
8
+ "name": "parent",
9
+ "ips": 633.857043437574,
10
+ "stddev": 85
11
+ },
12
+ {
13
+ "name": "children",
14
+ "ips": 8.660125142509473,
15
+ "stddev": 2
16
+ },
17
+ {
18
+ "name": "roots",
19
+ "ips": 1029.8556860775493,
20
+ "stddev": 136
21
+ },
22
+ {
23
+ "name": "ancestors",
24
+ "ips": 251.68388495529973,
25
+ "stddev": 33
26
+ },
27
+ {
28
+ "name": "descendants",
29
+ "ips": 0.19643176694334188,
30
+ "stddev": 0
31
+ }
32
+ ]
@@ -0,0 +1,32 @@
1
+ [
2
+ {
3
+ "name": "insertion",
4
+ "ips": 529.3829399001362,
5
+ "stddev": 68
6
+ },
7
+ {
8
+ "name": "parent",
9
+ "ips": 798.8413756182359,
10
+ "stddev": 112
11
+ },
12
+ {
13
+ "name": "children",
14
+ "ips": 19.534339432833036,
15
+ "stddev": 2
16
+ },
17
+ {
18
+ "name": "roots",
19
+ "ips": 1306.76576446662,
20
+ "stddev": 254
21
+ },
22
+ {
23
+ "name": "ancestors",
24
+ "ips": 501.12099359692917,
25
+ "stddev": 112
26
+ },
27
+ {
28
+ "name": "descendants",
29
+ "ips": 14.539749441549349,
30
+ "stddev": 2
31
+ }
32
+ ]
@@ -0,0 +1,32 @@
1
+ [
2
+ {
3
+ "name": "insertion",
4
+ "ips": 7.721853007625269,
5
+ "stddev": 2
6
+ },
7
+ {
8
+ "name": "parent",
9
+ "ips": 569.4524509694182,
10
+ "stddev": 191
11
+ },
12
+ {
13
+ "name": "children",
14
+ "ips": 154.39379306573127,
15
+ "stddev": 34
16
+ },
17
+ {
18
+ "name": "roots",
19
+ "ips": 820.8230612972056,
20
+ "stddev": 119
21
+ },
22
+ {
23
+ "name": "ancestors",
24
+ "ips": 346.3686578783685,
25
+ "stddev": 70
26
+ },
27
+ {
28
+ "name": "descendants",
29
+ "ips": 52.86002694684352,
30
+ "stddev": 7
31
+ }
32
+ ]
@@ -0,0 +1,85 @@
1
+ require 'bundler/inline'
2
+
3
+ BENCH_GEM = ENV['BENCH_GEM']
4
+
5
+ unless ['xylem', 'acts_as_tree', 'awesome_nested_set', 'ancestry'].include?(BENCH_GEM)
6
+ fail 'Please provide a environment variable BENCH_GEM with one o the following values: [xylem, acts_as_tree, awesome_nested_set, ancestry]'
7
+ end
8
+
9
+ gemfile(true) do
10
+ source 'https://rubygems.org'
11
+ gem 'activerecord', require: 'active_record'
12
+ gem 'pg'
13
+ gem 'benchmark-ips'
14
+
15
+ case BENCH_GEM
16
+ when 'xylem'
17
+ gem 'xylem', path: '..'
18
+ when 'acts_as_tree'
19
+ gem 'acts_as_tree'
20
+ when 'awesome_nested_set'
21
+ gem 'activesupport', require: 'active_support/core_ext/module/delegation'
22
+ gem 'awesome_nested_set'
23
+ when 'ancestry'
24
+ gem 'ancestry'
25
+ end
26
+ end
27
+
28
+ ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'xylem_test', username: 'postgres')
29
+ ActiveRecord::Base.connection.execute('DROP SCHEMA public CASCADE; CREATE SCHEMA public;')
30
+
31
+ ActiveRecord::Base.send(:include, ActsAsTree) if BENCH_GEM == 'acts_as_tree'
32
+
33
+ class Comment < ActiveRecord::Base
34
+ acts_as_tree if ['xylem', 'acts_as_tree'].include?(BENCH_GEM)
35
+ acts_as_nested_set if BENCH_GEM == 'awesome_nested_set'
36
+ has_ancestry if BENCH_GEM == 'ancestry'
37
+
38
+ connection.create_table table_name, force: true do |t|
39
+ t.string :payload
40
+ t.integer :parent_id, index: true if BENCH_GEM != 'ancestry'
41
+ t.integer :lft, index: true if BENCH_GEM == 'awesome_nested_set'
42
+ t.integer :rgt, index: true if BENCH_GEM == 'awesome_nested_set'
43
+ t.string :ancestry, index: true if BENCH_GEM == 'ancestry'
44
+ end
45
+ end
46
+
47
+ def recursive_child(par, depth)
48
+ unless depth == 0
49
+ 5.times do
50
+ recursive_child(Comment.create!(payload: SecureRandom.hex(128), parent: par), depth - 1)
51
+ end
52
+ end
53
+ end
54
+
55
+ puts 'INSERTING TEST DATA...'
56
+
57
+ recursive_child(nil, 5)
58
+
59
+ @comment1 = Comment.roots.first
60
+ @comment11 = @comment1.children.sample
61
+ @comment111 = @comment11.children.sample
62
+ @comment1111 = @comment111.children.sample
63
+ @comment11111 = @comment1111.children.sample
64
+
65
+ Benchmark.ips do |x|
66
+ x.report('insertion') do |times|
67
+ times.times { Comment.create!(parent: @comment111) }
68
+ end
69
+ x.report('parent') do |times|
70
+ times.times { @comment11111.reload.parent }
71
+ end
72
+ x.report('children') do |times|
73
+ times.times { @comment111.reload.children.to_a }
74
+ end
75
+ x.report('roots') do |times|
76
+ times.times { Comment.roots.to_a }
77
+ end
78
+ x.report('ancestors') do |times|
79
+ times.times { @comment11111.reload.ancestors.to_a }
80
+ end
81
+ x.report('descendants') do |times|
82
+ times.times { @comment1.reload.descendants.to_a }
83
+ end
84
+ x.json!("#{ENV['BENCH_GEM']}.json")
85
+ end
data/bench/xylem.json ADDED
@@ -0,0 +1,32 @@
1
+ [
2
+ {
3
+ "name": "insertion",
4
+ "ips": 600.964555391788,
5
+ "stddev": 34
6
+ },
7
+ {
8
+ "name": "parent",
9
+ "ips": 1070.2536522884402,
10
+ "stddev": 120
11
+ },
12
+ {
13
+ "name": "children",
14
+ "ips": 11.90851991649796,
15
+ "stddev": 2
16
+ },
17
+ {
18
+ "name": "roots",
19
+ "ips": 1713.3724759018537,
20
+ "stddev": 210
21
+ },
22
+ {
23
+ "name": "ancestors",
24
+ "ips": 511.49023662770406,
25
+ "stddev": 61
26
+ },
27
+ {
28
+ "name": "descendants",
29
+ "ips": 12.000439734178782,
30
+ "stddev": 2
31
+ }
32
+ ]
data/bootstrap.sh ADDED
@@ -0,0 +1,32 @@
1
+ # Shell file taken at: https://github.com/rails/rails-dev-box
2
+ function install {
3
+ echo installing $1
4
+ shift
5
+ apt-get -y install "$@" >/dev/null 2>&1
6
+ }
7
+
8
+ echo updating package information
9
+ apt-add-repository -y ppa:brightbox/ruby-ng >/dev/null 2>&1
10
+ apt-get -y update >/dev/null 2>&1
11
+
12
+ install 'development tools' build-essential
13
+
14
+ install Ruby ruby2.2 ruby2.2-dev
15
+ update-alternatives --set ruby /usr/bin/ruby2.2 >/dev/null 2>&1
16
+ update-alternatives --set gem /usr/bin/gem2.2 >/dev/null 2>&1
17
+
18
+ echo installing Bundler
19
+ gem install bundler -N >/dev/null 2>&1
20
+
21
+ install Git git
22
+ install SQLite sqlite3 libsqlite3-dev
23
+
24
+ install PostgreSQL postgresql postgresql-contrib libpq-dev
25
+ sudo -u postgres createdb -O postgres xylem_test
26
+
27
+ install 'Nokogiri dependencies' libxml2 libxml2-dev libxslt1-dev
28
+
29
+ # Needed for docs generation.
30
+ update-locale LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8
31
+
32
+ echo 'ready to roll out'
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "appraisal"
7
+ gem "activerecord", "~> 4.0"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "appraisal"
7
+ gem "activerecord", "~> 4.1"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "appraisal"
7
+ gem "activerecord", "~> 4.2"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "appraisal"
7
+ gem "activerecord", "5.0.0.beta1"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,3 @@
1
+ module Xylem
2
+ VERSION = '0.0.1'
3
+ end
data/lib/xylem.rb ADDED
@@ -0,0 +1,96 @@
1
+ require 'active_record'
2
+
3
+ module Xylem
4
+ module InstanceMethods
5
+ def ancestors
6
+ _xylem_query(:id, parent_id, :id, :parent_id, :desc)
7
+ end
8
+
9
+ def self_and_ancestors
10
+ _xylem_query(:id, id, :id, :parent_id, :desc)
11
+ end
12
+
13
+ def descendants
14
+ _xylem_query(:parent_id, id, :parent_id, :id, :asc)
15
+ end
16
+
17
+ def self_and_descendants
18
+ _xylem_query(:id, id, :parent_id, :id, :asc)
19
+ end
20
+
21
+ def root
22
+ ancestors.first
23
+ end
24
+
25
+ def siblings
26
+ self.class.where(parent_id: parent_id).where.not(id: id)
27
+ end
28
+
29
+ def self_and_siblings
30
+ self.class.where(parent_id: parent_id)
31
+ end
32
+
33
+ def self_and_children
34
+ [self] + children
35
+ end
36
+
37
+ def root?
38
+ parent.nil?
39
+ end
40
+
41
+ def leaf?
42
+ children.size == 0
43
+ end
44
+
45
+ private
46
+
47
+ def _xylem_query(where_col, where_val, join_lft_col, join_rgt_col, order_stmt)
48
+ rcte = Arel::Table.new(:recusive_cte)
49
+ table = self.class.arel_table
50
+ i_select = table.project([table[Arel.star], Arel::Nodes::As.new(1, Arel::Nodes::SqlLiteral.new('level'))]).where(table[where_col].eq(where_val))
51
+ r_select = table.project([table[Arel.star], Arel::Nodes::SqlLiteral.new('level + 1')]).join(rcte).on(table[join_lft_col].eq(rcte[join_rgt_col]))
52
+ as_stmt = Arel::Nodes::As.new(rcte, i_select.union(:all, r_select))
53
+ self.class.from(Arel::Nodes::TableAlias.new(rcte.project(Arel.star).with(:recursive, as_stmt), self.class.table_name).to_sql).order(level: order_stmt)
54
+ end
55
+ end
56
+
57
+ module ClassMethods
58
+ def root
59
+ roots.first
60
+ end
61
+
62
+ def roots
63
+ where(parent: nil)
64
+ end
65
+
66
+ def leaves
67
+ t = arel_table
68
+ where(t[:id].not_in(t.project([t[:parent_id]]).where(t[:parent_id].not_eq(nil)).distinct))
69
+ end
70
+ end
71
+ end
72
+
73
+ class ActiveRecord::Base
74
+ def self.acts_as_tree(options = {})
75
+ config = {
76
+ counter_cache: options[:counter_cache] || nil,
77
+ dependent: options[:destroy] || :destroy,
78
+ touch: options[:touch] || false
79
+ }
80
+
81
+ has_many :children,
82
+ class_name: name,
83
+ foreign_key: :parent_id,
84
+ dependent: config[:dependent],
85
+ inverse_of: :parent
86
+
87
+ belongs_to :parent,
88
+ class_name: name,
89
+ counter_cache: config[:counter_cache],
90
+ touch: config[:touch],
91
+ inverse_of: :children
92
+
93
+ extend Xylem::ClassMethods
94
+ include Xylem::InstanceMethods
95
+ end
96
+ end
@@ -0,0 +1,226 @@
1
+ require 'codeclimate-test-reporter'
2
+ CodeClimate::TestReporter.start
3
+ require 'minitest/autorun'
4
+ require 'minitest/pride'
5
+
6
+ require 'xylem'
7
+
8
+ case ENV['DB']
9
+ when 'sqlite'
10
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
11
+ when 'postgres'
12
+ ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'xylem_test', username: 'postgres')
13
+ ActiveRecord::Base.connection.execute('DROP SCHEMA public CASCADE; CREATE SCHEMA public;')
14
+ else
15
+ fail 'Please provide a environment varible DB with either "postgres" or "sqlite" to define the tested database'
16
+ end
17
+
18
+ class Category < ActiveRecord::Base
19
+ acts_as_tree
20
+ connection.create_table table_name, force: true do |t|
21
+ t.integer :parent_id
22
+ end
23
+ end
24
+
25
+ class Menu < ActiveRecord::Base
26
+ acts_as_tree
27
+ default_scope { where(draft: false) }
28
+ connection.create_table table_name, force: true do |t|
29
+ t.integer :parent_id
30
+ t.boolean :draft, default: false
31
+ end
32
+ end
33
+
34
+ class PlainModel < ActiveRecord::Base
35
+ connection.create_table table_name, force: true do |t|
36
+ t.string :name
37
+ end
38
+ end
39
+
40
+ class XylemTestCase < MiniTest::Test
41
+ def setup
42
+ @product = Category.create!
43
+ @service = Category.create!
44
+ @physical = Category.create!(parent_id: @product.id)
45
+ @digital = Category.create!(parent_id: @product.id)
46
+ @training = Category.create!(parent_id: @service.id)
47
+ @consultancy = Category.create!(parent_id: @service.id)
48
+ @hosting = Category.create!(parent_id: @service.id)
49
+ @daily_training = Category.create!(parent_id: @training.id)
50
+ @weekly_training = Category.create!(parent_id: @training.id)
51
+
52
+ @gibberish = Menu.create!(draft: true)
53
+ @main = Menu.create!
54
+ @option1 = Menu.create!(parent: @main)
55
+ @option2 = Menu.create!(parent: @main, draft: true)
56
+ @option3 = Menu.create!(parent: @main)
57
+ @suboption11 = Menu.create!(parent: @option1)
58
+ @suboption12 = Menu.create!(parent: @option1)
59
+ @suboption21 = Menu.create!(parent: @option2, draft: true)
60
+ end
61
+
62
+ def teardown
63
+ ar_connection = ActiveRecord::Base.connection
64
+ ar_connection.tables.each { |t| ar_connection.execute "DELETE FROM #{t}" }
65
+ end
66
+ end
67
+
68
+ class ClassMethodsTest < XylemTestCase
69
+ def test_root
70
+ assert_equal @product, Category.root
71
+ end
72
+
73
+ def test_scoped_root
74
+ assert_equal @main, Menu.root
75
+ end
76
+
77
+ def test_roots
78
+ assert_equal [@product, @service], Category.roots
79
+ end
80
+
81
+ def test_scoped_roots
82
+ assert_equal [@main], Menu.roots
83
+ end
84
+
85
+ def test_leaves
86
+ assert_equal [@physical, @digital, @consultancy, @hosting, @daily_training, @weekly_training], Category.leaves
87
+ end
88
+
89
+ def test_scoped_leaves
90
+ assert_equal [@option3, @suboption11, @suboption12], Menu.leaves
91
+ end
92
+
93
+ def test_plain_model
94
+ refute_respond_to PlainModel, :root
95
+ refute_respond_to PlainModel, :roots
96
+ refute_respond_to PlainModel, :leaves
97
+ end
98
+ end
99
+
100
+ class InstanceMethodsTest < XylemTestCase
101
+ def test_ancestors
102
+ assert_equal [@service, @training], @weekly_training.ancestors
103
+ assert_equal [@product], @digital.ancestors
104
+ assert_empty @product.ancestors
105
+ assert_empty @service.ancestors
106
+ end
107
+
108
+ def test_scoped_ancestors
109
+ assert_equal [@main, @option1], @suboption12.ancestors
110
+ assert_empty @main.ancestors
111
+ assert_empty @gibberish.ancestors
112
+ end
113
+
114
+ def test_self_and_ancestors
115
+ assert_equal [@service, @training, @weekly_training], @weekly_training.self_and_ancestors
116
+ assert_equal [@product, @digital], @digital.self_and_ancestors
117
+ assert_equal [@product], @product.self_and_ancestors
118
+ assert_equal [@service], @service.self_and_ancestors
119
+ end
120
+
121
+ def test_scoped_self_and_ancestors
122
+ assert_equal [@main, @option1, @suboption12], @suboption12.self_and_ancestors
123
+ assert_equal [@main], @main.self_and_ancestors
124
+ assert_empty @gibberish.self_and_ancestors
125
+ end
126
+
127
+ def test_descendants
128
+ assert_equal [@physical, @digital], @product.descendants
129
+ assert_equal [@training, @consultancy, @hosting, @daily_training, @weekly_training], @service.descendants
130
+ assert_equal [@daily_training, @weekly_training], @training.descendants
131
+ assert_empty @physical.descendants
132
+ assert_empty @consultancy.descendants
133
+ end
134
+
135
+ def test_scoped_descendants
136
+ assert_equal [@option1, @option3, @suboption11, @suboption12], @main.descendants
137
+ assert_equal [@suboption11, @suboption12], @option1.descendants
138
+ assert_empty @gibberish.descendants
139
+ assert_empty @suboption11.descendants
140
+ assert_empty @suboption21.descendants
141
+ end
142
+
143
+ def test_self_and_descendants
144
+ assert_equal [@physical, @digital], @product.descendants
145
+ assert_equal [@training, @consultancy, @hosting, @daily_training, @weekly_training], @service.descendants
146
+ assert_equal [@daily_training, @weekly_training], @training.descendants
147
+ assert_empty @physical.descendants
148
+ assert_empty @consultancy.descendants
149
+ end
150
+
151
+ def test_scoped_self_and_descendants
152
+ assert_equal [@main, @option1, @option3, @suboption11, @suboption12], @main.self_and_descendants
153
+ assert_equal [@option1, @suboption11, @suboption12], @option1.self_and_descendants
154
+ assert_equal [@suboption11], @suboption11.self_and_descendants
155
+ assert_empty @gibberish.self_and_descendants
156
+ end
157
+
158
+ def test_root
159
+ assert_equal @service, @weekly_training.root
160
+ assert_equal @product, @digital.root
161
+ refute @product.root
162
+ end
163
+
164
+ def test_scoped_root
165
+ assert_equal @main, @suboption11.root
166
+ assert_equal @main, @option3.root
167
+ refute @main.root
168
+ end
169
+
170
+ def test_siblings
171
+ assert_equal [@digital], @physical.siblings
172
+ assert_equal [@training, @hosting], @consultancy.siblings
173
+ end
174
+
175
+ def test_scoped_siblings
176
+ assert_equal [@option3], @option1.siblings
177
+ assert_equal [@suboption11], @suboption12.siblings
178
+ end
179
+
180
+ def test_self_and_siblings
181
+ assert_equal [@physical, @digital], @physical.self_and_siblings
182
+ assert_equal [@training, @consultancy, @hosting], @consultancy.self_and_siblings
183
+ end
184
+
185
+ def test_scoped_self_and_siblings
186
+ assert_equal [@option1, @option3], @option1.self_and_siblings
187
+ assert_equal [@suboption11, @suboption12], @suboption12.self_and_siblings
188
+ end
189
+
190
+ def test_children
191
+ assert_equal [@physical, @digital], @product.children
192
+ assert_equal [@daily_training, @weekly_training], @training.children
193
+ end
194
+
195
+ def test_scoped_children
196
+ assert_equal [@option1, @option3], @main.children
197
+ assert_equal [@suboption11, @suboption12], @option1.children
198
+ end
199
+
200
+ def test_self_and_children
201
+ assert_equal [@product, @physical, @digital], @product.self_and_children
202
+ assert_equal [@training, @daily_training, @weekly_training], @training.self_and_children
203
+ end
204
+
205
+ def test_scoped_self_and_children
206
+ assert_equal [@product, @physical, @digital], @product.self_and_children
207
+ assert_equal [@training, @daily_training, @weekly_training], @training.self_and_children
208
+ end
209
+
210
+ def test_root?
211
+ assert @product.root?
212
+ assert @main.root?
213
+ refute @physical.root?
214
+ refute @option3.root?
215
+ end
216
+
217
+ def test_leaf?
218
+ assert @daily_training.leaf?
219
+ assert @consultancy.leaf?
220
+ assert @suboption12.leaf?
221
+ assert @option3.leaf?
222
+ refute @service.leaf?
223
+ refute @training.leaf?
224
+ refute @option1.leaf?
225
+ end
226
+ end
data/xylem.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'xylem/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'xylem'
8
+ spec.version = Xylem::VERSION
9
+ spec.authors = ['Oscar Esgalha']
10
+ spec.email = ['oscaresgalha@gmail.com']
11
+ spec.summary = %q{Xylem uses the Adjacency List approach to store and query through hierarchical data with ActiveRecord.}
12
+ spec.description = %q{Xylem provides a simple way to store and retrieve hierarchical data through ActiveRecord.}
13
+ spec.homepage = 'https://github.com/oesgalha/xylem'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'activerecord', '>= 4.0'
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.7'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'minitest', '~> 5.6'
26
+ spec.add_development_dependency 'pg', '>= 0.11'
27
+ spec.add_development_dependency 'sqlite3'
28
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xylem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Oscar Esgalha
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pg
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0.11'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0.11'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Xylem provides a simple way to store and retrieve hierarchical data through
98
+ ActiveRecord.
99
+ email:
100
+ - oscaresgalha@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".travis.yml"
107
+ - Appraisals
108
+ - Gemfile
109
+ - README.md
110
+ - Rakefile
111
+ - Vagrantfile
112
+ - bench/acts_as_tree.json
113
+ - bench/ancestry.json
114
+ - bench/awesome_nested_set.json
115
+ - bench/benchmark.rb
116
+ - bench/xylem.json
117
+ - bootstrap.sh
118
+ - gemfiles/ar_4.0.gemfile
119
+ - gemfiles/ar_4.1.gemfile
120
+ - gemfiles/ar_4.2.gemfile
121
+ - gemfiles/ar_5beta.gemfile
122
+ - lib/xylem.rb
123
+ - lib/xylem/version.rb
124
+ - test/xylem_tests.rb
125
+ - xylem.gemspec
126
+ homepage: https://github.com/oesgalha/xylem
127
+ licenses:
128
+ - MIT
129
+ metadata: {}
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubyforge_project:
146
+ rubygems_version: 2.4.5
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: Xylem uses the Adjacency List approach to store and query through hierarchical
150
+ data with ActiveRecord.
151
+ test_files:
152
+ - test/xylem_tests.rb