has_hierarchy 0.3.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5ff1f9affc659f62ba4a9b6eb0f25309b980939f
4
- data.tar.gz: 4de2b383019e35c329b4bb11a3e7c39f1a5b2207
3
+ metadata.gz: 296f6af57a4b4997d4f63b1ad555c9723da8701b
4
+ data.tar.gz: 83c399372635843bbeac74f6aa632d2eaeea0b05
5
5
  SHA512:
6
- metadata.gz: 8748539cfaf294108ff6d4e3bbf443dd42320171f5e89a0982b00b4aa32f4e6f255b6f3fa0a3726b970b5e7cf6b1b02a48e8bd0e203d892127547afb82c99567
7
- data.tar.gz: edc08f4f3e803bf13f4d9b8dfd84cd4b690489ddab3daf3f3a39aea833fe44d93a506b50d0319a970f9dacc91b45722b3768b321023ba7ab7dda4246dc169a87
6
+ metadata.gz: 14a6ad138900fa59c5e174e26497ede7184c8e33137f8fc724045b1fb4d2c4befc89f82ed1071f82b5507bac917f517994e592f5cefd83928579d5f6a47e4549
7
+ data.tar.gz: 3df4da8267691965a285f537ab10e994596de77e3fda143223a9f00f6bb180eba0fc73368a8ef1dcac56d45092b464892dea7b5997daedcf575a8c1aff1d30f4
data/.travis.yml CHANGED
@@ -5,6 +5,16 @@ rvm:
5
5
  - 2.0.0
6
6
  - 2.1.0
7
7
 
8
+ env:
9
+ - HAS_HIERARCHY_ORM=mongoid
10
+ - HAS_HIERARCHY_ORM=active_record
11
+
12
+ matrix:
13
+ fast_finish: true
14
+
15
+ services:
16
+ - mongodb
17
+
8
18
  addons:
9
19
  code_climate:
10
20
  repo_token: cf2619e78bacdbeab2c8c1e2e1f37ff6c47b06374ee717f38cbbcdafaceeeb59
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  # has_hierarchy
7
7
 
8
- Provides tree behavior to active_record models.
8
+ Tree behavior for ActiveRecord models and Mongoid documents.
9
9
 
10
10
  ## Installation
11
11
 
@@ -19,15 +19,15 @@ And then execute:
19
19
 
20
20
  ## Usage
21
21
 
22
- Example tree:
22
+ Example model:
23
23
  ```sh
24
24
  $ rails g model Item \
25
- name:string \
26
- path:string \
27
- depth:integer \
28
- position:integer \
29
- parent:belongs_to \
30
- children_count:integer
25
+ name:string \
26
+ path:string \
27
+ depth:integer \
28
+ position:integer \
29
+ parent:belongs_to \
30
+ children_count:integer
31
31
  ```
32
32
  ```ruby
33
33
  class Item < ActiveRecord::Base
@@ -36,12 +36,24 @@ class Item < ActiveRecord::Base
36
36
  counter_cache: true,
37
37
  dependent: :destroy
38
38
  end
39
+ ```
39
40
 
40
- foo = Item.create!(name: 'foo')
41
- bar = Item.create!(name: 'bar')
42
- qux = bar.children.create!(name: 'qux')
43
- baz = bar.children.create!(name: 'baz')
44
- quux = qux.children.create!(name: 'quux')
41
+ or Mongoid document:
42
+ ```ruby
43
+ class Item
44
+ include Mongoid::Document
45
+ include Mongoid::HasHierarchy
46
+
47
+ has_hierarchy path_part: :name,
48
+ depth_cache: true,
49
+ counter_cache: true,
50
+ dependent: :destroy
51
+
52
+ field :name, type: String
53
+ field :path, type: String
54
+ field :depth, type: Fixnum, default: 0
55
+ field :children_count, type: Fixnum, default: 0
56
+ end
45
57
  ```
46
58
 
47
59
  Options:
@@ -58,6 +70,12 @@ dependent - optional, :dependent option for children association.
58
70
 
59
71
  Operations on the tree:
60
72
  ```ruby
73
+ foo = Item.create!(name: 'foo')
74
+ bar = Item.create!(name: 'bar')
75
+ qux = bar.children.create!(name: 'qux')
76
+ baz = bar.children.create!(name: 'baz')
77
+ quux = qux.children.create!(name: 'quux')
78
+
61
79
  Item.roots
62
80
  # => [ foo, bar ]
63
81
 
@@ -88,7 +106,7 @@ bar.root? # => true
88
106
  qux.leaf? # => false
89
107
  ```
90
108
 
91
- Path cache is required for following methods:
109
+ Ancestors/descendants (requires path_cache):
92
110
  ```ruby
93
111
  bar.root_of?(quux) # => true
94
112
  bar.ancestor_of?(quux) # => true
@@ -100,6 +118,9 @@ bar.descendants # => [ qux, quux, baz ]
100
118
 
101
119
  Ordering (see [has_order](https://github.com/kolesnikovde/has_order)):
102
120
  ```ruby
121
+ bar.previous_siblings # => [ foo, quux ]
122
+ foo.next_siblings # => [ quux, bar ]
123
+
103
124
  foo.move_after(quux)
104
125
  Item.ordered.tree
105
126
  # => {
@@ -21,10 +21,13 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency 'bundler', '~> 1'
22
22
  spec.add_development_dependency 'rake', '~> 10'
23
23
  spec.add_development_dependency 'rspec', '~> 3'
24
- spec.add_development_dependency 'sqlite3', '~> 1'
24
+
25
+ spec.add_development_dependency 'sqlite3', '~> 1'
26
+ spec.add_development_dependency 'activerecord', '~> 4'
27
+ spec.add_development_dependency 'mongoid', '~> 4'
28
+
25
29
  spec.add_development_dependency 'codeclimate-test-reporter'
26
30
 
27
- spec.add_runtime_dependency 'activerecord', '~> 4'
28
31
  spec.add_runtime_dependency 'activesupport', '~> 4'
29
- spec.add_runtime_dependency 'has_order', '~> 0.1'
32
+ spec.add_runtime_dependency 'has_order', '~> 0.2'
30
33
  end
@@ -0,0 +1,26 @@
1
+ module HasHierarchy
2
+ module CounterCache
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_save :update_children_counter, if: :parent_id_changed?
7
+ after_destroy :decrement_children_counter, if: :parent_id?
8
+ end
9
+
10
+ protected
11
+
12
+ def update_children_counter
13
+ if parent_id
14
+ self.class.increment_counter(children_count_column, parent_id)
15
+ end
16
+
17
+ if parent_id_was
18
+ self.class.decrement_counter(children_count_column, parent_id_was)
19
+ end
20
+ end
21
+
22
+ def decrement_children_counter
23
+ self.class.decrement_counter(children_count_column, parent_id)
24
+ end
25
+ end
26
+ end
@@ -5,11 +5,16 @@ module HasHierarchy
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
+ include Mongoid::HasOrder if defined?(Mongoid)
9
+
8
10
  options = has_hierarchy_options
9
11
 
10
12
  has_order scope: Array(options[:scope]).concat([ :parent_id ]),
11
13
  position_column: options[:order]
12
14
 
15
+ alias_method :previous_siblings, :lower
16
+ alias_method :next_siblings, :higher
17
+
13
18
  include HasOrderOverrides
14
19
  end
15
20
 
@@ -0,0 +1,34 @@
1
+ module HasHierarchy
2
+ module OrmAdapter
3
+ module ActiveRecord
4
+ def ancestors
5
+ tree_scope.where(path_part_column => path_parts)
6
+ end
7
+
8
+ def siblings
9
+ t = self.class.arel_table
10
+
11
+ tree_scope.where(t[:parent_id].eq(parent_id).and(t[:id].not_eq(id)))
12
+ end
13
+
14
+ def subtree
15
+ t = self.class.arel_table
16
+
17
+ tree_scope.where(t[:id].eq(id).or(descendants_conditions))
18
+ end
19
+
20
+ def descendants
21
+ tree_scope.where(descendants_conditions)
22
+ end
23
+
24
+ protected
25
+
26
+ def descendants_conditions
27
+ t = self.class.arel_table
28
+
29
+ t[path_column].matches("#{path_for_children}%")
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,27 @@
1
+ module HasHierarchy
2
+ module OrmAdapter
3
+ module Mongoid
4
+ def ancestors
5
+ tree_scope.where(path_part_column.in => path_parts)
6
+ end
7
+
8
+ def siblings
9
+ tree_scope.where(:parent_id => parent_id, :id.ne => id)
10
+ end
11
+
12
+ def subtree
13
+ tree_scope.or({ id: id }, descendants_conditions)
14
+ end
15
+
16
+ def descendants
17
+ tree_scope.where(descendants_conditions)
18
+ end
19
+
20
+ protected
21
+
22
+ def descendants_conditions
23
+ { path_column => /^#{path_for_children}/ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ module HasHierarchy
2
+ # :nocov:
3
+ module OrmAdapter
4
+ if defined?(::ActiveRecord)
5
+ ::ActiveRecord::Base.extend(HasHierarchy)
6
+ end
7
+
8
+ if defined?(::Mongoid)
9
+ module ::Mongoid::HasHierarchy
10
+ def self.included(base)
11
+ base.extend(::HasHierarchy)
12
+ end
13
+ end
14
+ end
15
+
16
+ def self.included(base)
17
+ base.class_eval do
18
+ if defined?(::ActiveRecord) and self < ::ActiveRecord::Base
19
+ require 'has_hierarchy/orm_adapter/active_record'
20
+ include ActiveRecord
21
+ elsif defined?(::Mongoid) and self < ::Mongoid::Document
22
+ require 'has_hierarchy/orm_adapter/mongoid'
23
+ include Mongoid
24
+ end
25
+ end
26
+ end
27
+ end
28
+ # :nocov:
29
+ end
@@ -23,33 +23,23 @@ module HasHierarchy
23
23
  end
24
24
 
25
25
  def root
26
- self.class.find_by(path_part_column => path_parts.first)
26
+ if root_part = path_parts.first
27
+ self.class.find_by(path_part_column => root_part)
28
+ end
27
29
  end
28
30
 
29
31
  def root_of?(node)
30
32
  node.path_parts.first == path_part if path_part.present?
31
33
  end
32
34
 
33
- def ancestors
34
- tree_scope.where(ancestors_conditions)
35
- end
36
-
37
35
  def ancestor_of?(node)
38
36
  node.path_parts.include?(path_part)
39
37
  end
40
38
 
41
- def descendants
42
- tree_scope.where(descendants_conditions)
43
- end
44
-
45
39
  def descendant_of?(node)
46
40
  path_parts.include?(node.path_part)
47
41
  end
48
42
 
49
- def subtree
50
- tree_scope.where(subtree_conditions)
51
- end
52
-
53
43
  def depth
54
44
  path_parts.size
55
45
  end
@@ -80,35 +70,21 @@ module HasHierarchy
80
70
  [ path, path_part, path_separator ].join
81
71
  end
82
72
 
83
- def populate_path
84
- self.path = root? ? '' : parent.path_for_children
85
- end
86
-
87
- def ancestors_conditions
88
- { path_part_column => path_parts }
89
- end
90
-
91
- def descendants_conditions
92
- arel_path = self.class.arel_table[path_column]
93
- arel_path.matches("#{path_for_children}%")
94
- end
95
-
96
- def subtree_conditions
97
- arel_path_part = self.class.arel_table[path_part_column]
98
- arel_path_part.eq(path_part).or(descendants_conditions)
73
+ def populate_path(path = nil)
74
+ self.path = root? ? '' : (path || parent.path_for_children)
99
75
  end
100
76
 
101
- def rebuild_subtree
102
- populate_path
77
+ def rebuild_subtree(path = nil)
78
+ populate_path(path)
103
79
 
104
80
  children.each do |child|
105
- child.rebuild_subtree
81
+ child.rebuild_subtree(path_for_children)
106
82
  child.save!
107
83
  end
108
84
  end
109
85
 
110
86
  def need_to_rebuild_subtree?
111
- parent_id_changed? or changed_attributes.include?(path_part_column)
87
+ parent_id_changed? or changed_attributes.include?(path_part_column.to_s)
112
88
  end
113
89
  end
114
90
  end
@@ -1,3 +1,3 @@
1
1
  module HasHierarchy
2
- VERSION = '0.3.1'
2
+ VERSION = '0.4.0'
3
3
  end
data/lib/has_hierarchy.rb CHANGED
@@ -3,6 +3,8 @@ require 'has_hierarchy/version'
3
3
  require 'has_hierarchy/order'
4
4
  require 'has_hierarchy/path'
5
5
  require 'has_hierarchy/depth_cache'
6
+ require 'has_hierarchy/counter_cache'
7
+ require 'has_hierarchy/orm_adapter'
6
8
 
7
9
  module HasHierarchy
8
10
  DEFAULT_OPTIONS = {
@@ -22,13 +24,13 @@ module HasHierarchy
22
24
 
23
25
  setup_has_hierarchy_options(options)
24
26
 
25
- include Order if options[:order]
26
- include Path if options[:path_cache]
27
- include DepthCache if options[:depth_cache]
27
+ include Order if options[:order]
28
+ include Path if options[:path_cache]
29
+ include DepthCache if options[:depth_cache]
30
+ include CounterCache if options[:counter_cache]
28
31
 
29
32
  belongs_to :parent, class_name: self.name,
30
- inverse_of: :children,
31
- counter_cache: options[:counter_cache]
33
+ inverse_of: :children
32
34
 
33
35
  has_many :children, class_name: self.name,
34
36
  foreign_key: :parent_id,
@@ -36,6 +38,8 @@ module HasHierarchy
36
38
  dependent: options[:dependent]
37
39
 
38
40
  define_tree_scope(options[:scope])
41
+
42
+ include HasHierarchy::OrmAdapter
39
43
  end
40
44
 
41
45
  module ClassMethods
@@ -72,6 +76,7 @@ module HasHierarchy
72
76
  cattr_accessor(:path_part_column) { options[:path_part] }
73
77
  cattr_accessor(:path_separator) { options[:path_separator] }
74
78
  cattr_accessor(:depth_column) { options[:depth_cache] }
79
+ cattr_accessor(:children_count_column) { options[:counter_cache] }
75
80
  cattr_accessor(:has_hierarchy_options) { options }
76
81
  end
77
82
 
@@ -112,10 +117,6 @@ module HasHierarchy
112
117
  parent_id == node.parent_id and id != node.id
113
118
  end
114
119
 
115
- def siblings
116
- tree_scope.where(siblings_conditions)
117
- end
118
-
119
120
  def move_children_to_parent
120
121
  children.each do |c|
121
122
  c.parent = self.parent
@@ -128,13 +129,5 @@ module HasHierarchy
128
129
  def tree_scope
129
130
  self.class.tree_scope(self)
130
131
  end
131
-
132
- def siblings_conditions
133
- t = self.class.arel_table
134
-
135
- t[:parent_id].eq(parent_id).and(t[:id].not_eq(id))
136
- end
137
132
  end
138
133
  end
139
-
140
- ActiveRecord::Base.extend(HasHierarchy)
@@ -1,310 +1,5 @@
1
1
  require 'spec_helper'
2
-
3
- shared_context 'example tree' do
4
- let!(:foo) { described_class.create!(name: 'foo') }
5
- let!(:bar) { described_class.create!(name: 'bar') }
6
- let!(:qux) { bar.children.create!(name: 'qux') }
7
- let!(:baz) { bar.children.create!(name: 'baz') }
8
- let!(:quux) { qux.children.create!(name: 'quux') }
9
-
10
- def reload_items
11
- [ foo, bar, baz, qux, quux ].each(&:reload)
12
- end
13
-
14
- before do
15
- reload_items
16
- end
17
- end
18
-
19
- shared_examples 'adjacency list' do
20
- include_context 'example tree'
21
-
22
- describe '.tree' do
23
- it 'arranges tree' do
24
- expect(described_class.tree).to be_arranged_like({
25
- foo => {},
26
- bar => {
27
- qux => {
28
- quux => {}
29
- },
30
- baz => {}
31
- }
32
- })
33
- end
34
-
35
- it 'allows custom order' do
36
- expect(described_class.alphabetic.tree).to be_arranged_like({
37
- bar => {
38
- baz => {},
39
- qux => {
40
- quux => {}
41
- }
42
- },
43
- foo => {}
44
- })
45
- end
46
- end
47
-
48
- describe '.roots' do
49
- it 'returns roots' do
50
- expect(described_class.roots).to match_array([ foo, bar ])
51
- end
52
- end
53
-
54
- describe '#move_children_to_parent' do
55
- it 'changes children parent' do
56
- bar.move_children_to_parent
57
-
58
- expect(described_class.tree).to be_arranged_like({
59
- foo => {},
60
- bar => {},
61
- qux => {
62
- quux => {}
63
- },
64
- baz => {}
65
- })
66
- end
67
- end
68
-
69
- describe '#root?' do
70
- it 'returns true if node has parent' do
71
- expect(bar).to be_root
72
- expect(baz).not_to be_root
73
- end
74
- end
75
-
76
- describe '#leaf?' do
77
- it 'returns true if node does not have children' do
78
- expect(quux).to be_leaf
79
- expect(qux).not_to be_leaf
80
- end
81
- end
82
-
83
- describe '#parent_of?' do
84
- it 'returns true if node is a parent of given node' do
85
- expect(bar).to be_parent_of(qux)
86
- expect(bar).not_to be_parent_of(quux)
87
- end
88
- end
89
-
90
- describe '#child_of?' do
91
- it 'returns true if node is a child of given node' do
92
- expect(qux).to be_child_of(bar)
93
- expect(qux).not_to be_child_of(quux)
94
- end
95
- end
96
-
97
- describe '#sibling_of?' do
98
- it 'returns true if both nodes have same parent' do
99
- expect(foo).to be_sibling_of(bar)
100
- expect(baz).to be_sibling_of(qux)
101
- expect(foo).not_to be_sibling_of(qux)
102
- end
103
- end
104
- end
105
-
106
- shared_examples 'materialized path' do
107
- include_context 'example tree'
108
- it_behaves_like 'adjacency list'
109
-
110
- describe '.find_by_path' do
111
- it 'returns node' do
112
- expect(described_class.find_by_path('bar')).to eq(bar)
113
- expect(described_class.find_by_path('bar/qux/')).to eq(qux)
114
- expect(described_class.find_by_path('bar/qux/quux')).to eq(quux)
115
- end
116
- end
117
-
118
- describe '.find_by_path!' do
119
- it 'returns node or raises RecordNotFound' do
120
- expect(described_class.find_by_path!('bar/qux/')).to eq(qux)
121
- expect{ described_class.find_by_path!('wrong') }.to raise_error(ActiveRecord::RecordNotFound)
122
- end
123
- end
124
-
125
- describe '#full_path' do
126
- it 'returns full node path' do
127
- expect(bar.full_path).to eq('bar')
128
- expect(qux.full_path).to eq('bar/qux')
129
- expect(quux.full_path).to eq('bar/qux/quux')
130
- end
131
- end
132
-
133
- describe '#root' do
134
- it 'returns first node ancestor' do
135
- expect(baz.root).to eq(bar)
136
- end
137
-
138
- it 'returns nil if node is a root' do
139
- expect(bar.root).to be nil
140
- end
141
- end
142
-
143
- describe '#ancestors' do
144
- it 'returns node ancestors' do
145
- expect(quux.ancestors).to match_array([ qux, bar ])
146
- expect(qux.ancestors).to match_array([ bar ])
147
- expect(bar.ancestors).to be_empty
148
- end
149
- end
150
-
151
- describe '#descendants' do
152
- it 'returns node descendants' do
153
- expect(bar.descendants).to match_array([ qux, quux, baz ])
154
- expect(qux.descendants).to match_array([ quux ])
155
- expect(quux.descendants).to be_empty
156
- end
157
- end
158
-
159
- describe '#subtree' do
160
- it 'returns node with descendants' do
161
- expect(bar.subtree.tree).to be_arranged_like({
162
- bar => {
163
- qux => {
164
- quux => {}
165
- },
166
- baz => {}
167
- }
168
- })
169
- end
170
-
171
- it 'returns node if node is a leaf' do
172
- expect(baz.subtree).to eq([ baz ])
173
- end
174
- end
175
-
176
- describe '#root_of?' do
177
- it 'returns true of node is a root of given node' do
178
- expect(bar).to be_root_of(qux)
179
- expect(bar).to be_root_of(quux)
180
- expect(bar).not_to be_root_of(bar)
181
- expect(bar).not_to be_root_of(foo)
182
- end
183
- end
184
-
185
- describe '#ancestor_of?' do
186
- it 'returns true if node is an ancestors of given node' do
187
- expect(bar).to be_ancestor_of(qux)
188
- expect(bar).to be_ancestor_of(quux)
189
- expect(bar).not_to be_ancestor_of(bar)
190
- expect(bar).not_to be_ancestor_of(foo)
191
- end
192
- end
193
-
194
- describe '#descendant_of?' do
195
- it 'returns true if node is a descendant of given node' do
196
- expect(quux).to be_descendant_of(qux)
197
- expect(quux).to be_descendant_of(bar)
198
- expect(quux).not_to be_descendant_of(quux)
199
- expect(quux).not_to be_descendant_of(foo)
200
- end
201
- end
202
-
203
- describe '#depth' do
204
- it 'returns ancestors count' do
205
- expect(bar.depth).to eq(0)
206
- expect(qux.depth).to eq(1)
207
- expect(quux.depth).to eq(2)
208
- end
209
- end
210
-
211
- describe 'node id column change' do
212
- before do
213
- bar.name = 'bor'
214
- bar.save!
215
- end
216
-
217
- it 'updates children pathes' do
218
- expect(described_class.find_by_path('bor')).to eq(bar)
219
- expect(described_class.find_by_path('bor/qux')).to eq(qux)
220
- expect(described_class.find_by_path('bor/qux/quux')).to eq(quux)
221
- end
222
- end
223
-
224
- describe 'parent change' do
225
- let(:prev_parent) { baz.parent }
226
- let(:new_parent) { foo }
227
- let(:new_ancestors) { [ foo ] }
228
-
229
- before do
230
- baz.parent = new_parent
231
- baz.save!
232
- reload_items
233
- end
234
-
235
- it 'updates counter_cache' do
236
- expect(prev_parent.children_count).to eq(prev_parent.children.count)
237
- expect(new_parent.children_count).to eq(new_parent.children.count)
238
- end
239
-
240
- it 'changes ancestors' do
241
- expect(baz.ancestors).to eq(new_ancestors)
242
- end
243
-
244
- it 'applies to all descendants' do
245
- baz.children.each do |child|
246
- expect(child).to be_descendant_of(new_parent)
247
-
248
- child.children.each do |subchild|
249
- expect(subchild).to be_descendant_of(new_parent)
250
- end
251
- end
252
- end
253
- end
254
- end
255
-
256
- shared_examples 'tree with cached depth' do
257
- include_context 'example tree'
258
- it_behaves_like 'adjacency list'
259
-
260
- it 'stores node level' do
261
- expect(described_class.where(depth: 0)).to match_array([ foo, bar ])
262
- expect(described_class.where(depth: 1)).to match_array([ qux, baz ])
263
- expect(described_class.where(depth: 2)).to match_array([ quux ])
264
- end
265
- end
266
-
267
- shared_examples 'scoped tree' do
268
- let!(:foo) { described_class.create!(name: 'foo', category: 'foo') }
269
- let!(:bar) { described_class.create!(name: 'bar', category: 'bar') }
270
-
271
- it 'restricts scope' do
272
- expect(bar.siblings).to be_empty
273
- end
274
- end
275
-
276
- shared_examples 'ordered tree' do
277
- include_context 'example tree'
278
-
279
- it '#move_after' do
280
- quux.move_after(foo)
281
- reload_items
282
-
283
- expect(described_class.ordered.tree).to be_arranged_like({
284
- foo => {},
285
- quux => {},
286
- bar => {
287
- qux => {},
288
- baz => {}
289
- }
290
- })
291
- end
292
-
293
- it '#move_before' do
294
- baz.move_before(quux)
295
- reload_items
296
-
297
- expect(described_class.ordered.tree).to be_arranged_like({
298
- foo => {},
299
- bar => {
300
- qux => {
301
- baz => {},
302
- quux => {},
303
- }
304
- }
305
- })
306
- end
307
- end
2
+ require 'tree'
308
3
 
309
4
  describe AdjacencyListTreeItem do
310
5
  it_behaves_like 'adjacency list'
data/spec/spec_helper.rb CHANGED
@@ -1,22 +1,12 @@
1
1
  require 'codeclimate-test-reporter'
2
2
  CodeClimate::TestReporter.start
3
3
 
4
- require 'sqlite3'
5
- require 'active_record'
4
+ orm_adapter = ENV['HAS_HIERARCHY_ORM']
6
5
 
7
- ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
8
- ActiveRecord::Schema.verbose = false
9
-
10
- require File.expand_path('../db/schema.rb', __FILE__)
11
- require File.expand_path('../support/models.rb', __FILE__)
12
- require File.expand_path('../support/matchers.rb', __FILE__)
13
-
14
- RSpec.configure do |config|
15
- config.around :each do |example|
16
- ActiveRecord::Base.transaction do
17
- example.run
18
-
19
- raise ActiveRecord::Rollback
20
- end
21
- end
6
+ unless %w[active_record mongoid].include?(orm_adapter)
7
+ raise 'Unknown ORM.'
22
8
  end
9
+
10
+ require "support/orm/#{orm_adapter}/setup"
11
+ require 'support/models'
12
+ require 'support/matchers'
@@ -1,17 +1,11 @@
1
- require 'has_hierarchy'
2
-
3
- class Item < ActiveRecord::Base
4
- scope :alphabetic, ->{ order('name asc') }
5
- end
6
-
7
1
  class AdjacencyListTreeItem < Item
8
- has_hierarchy counter_cache: :children_count,
2
+ has_hierarchy counter_cache: true,
9
3
  path_cache: false,
10
4
  order: true
11
5
  end
12
6
 
13
7
  class MaterializedPathTreeItem < Item
14
- has_hierarchy counter_cache: :children_count,
8
+ has_hierarchy counter_cache: true,
15
9
  path_part: :name
16
10
  end
17
11
 
@@ -10,3 +10,7 @@ ActiveRecord::Schema.define(version: 0) do
10
10
  t.belongs_to :parent
11
11
  end
12
12
  end
13
+
14
+ class Item < ActiveRecord::Base
15
+ scope :alphabetic, ->{ order('name asc') }
16
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_record'
2
+ require 'sqlite3'
3
+ require 'has_hierarchy'
4
+
5
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
6
+ ActiveRecord::Schema.verbose = false
7
+
8
+ require_relative 'item_model'
9
+
10
+ RSpec.configure do |config|
11
+ config.around :each do |example|
12
+ ActiveRecord::Base.transaction do
13
+ example.run
14
+
15
+ raise ActiveRecord::Rollback
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ class Item
2
+ include Mongoid::Document
3
+ include Mongoid::HasHierarchy
4
+
5
+ field :name, type: String
6
+ field :path, type: String
7
+ field :depth, type: Fixnum, default: 0
8
+ field :category, type: String
9
+ field :children_count, type: Fixnum, default: 0
10
+
11
+ scope :alphabetic, ->{ asc(:name) }
12
+ end
@@ -0,0 +1,12 @@
1
+ require 'mongoid'
2
+ require 'has_hierarchy'
3
+
4
+ Mongoid.configure do |config|
5
+ config.connect_to('mongoid_has_hierarchy_test')
6
+ end
7
+
8
+ require_relative 'item_model'
9
+
10
+ RSpec.configure do |config|
11
+ config.after(:each) { Mongoid.purge! }
12
+ end
data/spec/tree.rb ADDED
@@ -0,0 +1,323 @@
1
+ shared_context 'example tree' do
2
+ let!(:foo) { described_class.create!(name: 'foo') }
3
+ let!(:bar) { described_class.create!(name: 'bar') }
4
+ let!(:qux) { bar.children.create!(name: 'qux') }
5
+ let!(:baz) { bar.children.create!(name: 'baz') }
6
+ let!(:quux) { qux.children.create!(name: 'quux') }
7
+
8
+ def reload_items
9
+ [ foo, bar, baz, qux, quux ].each(&:reload)
10
+ end
11
+
12
+ before do
13
+ reload_items
14
+ end
15
+ end
16
+
17
+ shared_examples 'adjacency list' do
18
+ include_context 'example tree'
19
+
20
+ describe '.tree' do
21
+ it 'arranges tree' do
22
+ expect(described_class.tree).to be_arranged_like({
23
+ foo => {},
24
+ bar => {
25
+ qux => {
26
+ quux => {}
27
+ },
28
+ baz => {}
29
+ }
30
+ })
31
+ end
32
+
33
+ it 'allows custom order' do
34
+ expect(described_class.alphabetic.tree).to be_arranged_like({
35
+ bar => {
36
+ baz => {},
37
+ qux => {
38
+ quux => {}
39
+ }
40
+ },
41
+ foo => {}
42
+ })
43
+ end
44
+ end
45
+
46
+ describe '.roots' do
47
+ it 'returns roots' do
48
+ expect(described_class.roots).to match_array([ foo, bar ])
49
+ end
50
+ end
51
+
52
+ describe '#move_children_to_parent' do
53
+ it 'changes children parent' do
54
+ bar.move_children_to_parent
55
+
56
+ expect(described_class.tree).to be_arranged_like({
57
+ foo => {},
58
+ bar => {},
59
+ qux => {
60
+ quux => {}
61
+ },
62
+ baz => {}
63
+ })
64
+ end
65
+ end
66
+
67
+ describe '#root?' do
68
+ it 'returns true if node has parent' do
69
+ expect(bar).to be_root
70
+ expect(baz).not_to be_root
71
+ end
72
+ end
73
+
74
+ describe '#leaf?' do
75
+ it 'returns true if node does not have children' do
76
+ expect(quux).to be_leaf
77
+ expect(qux).not_to be_leaf
78
+ end
79
+ end
80
+
81
+ describe '#parent_of?' do
82
+ it 'returns true if node is a parent of given node' do
83
+ expect(bar).to be_parent_of(qux)
84
+ expect(bar).not_to be_parent_of(quux)
85
+ end
86
+ end
87
+
88
+ describe '#child_of?' do
89
+ it 'returns true if node is a child of given node' do
90
+ expect(qux).to be_child_of(bar)
91
+ expect(qux).not_to be_child_of(quux)
92
+ end
93
+ end
94
+
95
+ describe '#sibling_of?' do
96
+ it 'returns true if both nodes have same parent' do
97
+ expect(foo).to be_sibling_of(bar)
98
+ expect(baz).to be_sibling_of(qux)
99
+ expect(foo).not_to be_sibling_of(qux)
100
+ end
101
+ end
102
+ end
103
+
104
+ shared_examples 'materialized path' do
105
+ include_context 'example tree'
106
+ it_behaves_like 'adjacency list'
107
+
108
+ describe '.find_by_path' do
109
+ it 'returns node' do
110
+ expect(described_class.find_by_path('bar')).to eq(bar)
111
+ expect(described_class.find_by_path('bar/qux/')).to eq(qux)
112
+ expect(described_class.find_by_path('bar/qux/quux')).to eq(quux)
113
+ end
114
+ end
115
+
116
+ describe '.find_by_path!' do
117
+ it 'returns node or raises RecordNotFound' do
118
+ expect(described_class.find_by_path!('bar/qux/')).to eq(qux)
119
+ expect{ described_class.find_by_path!('wrong') }.to raise_error(ActiveRecord::RecordNotFound)
120
+ end
121
+ end
122
+
123
+ describe '#full_path' do
124
+ it 'returns full node path' do
125
+ expect(bar.full_path).to eq('bar')
126
+ expect(qux.full_path).to eq('bar/qux')
127
+ expect(quux.full_path).to eq('bar/qux/quux')
128
+ end
129
+ end
130
+
131
+ describe '#root' do
132
+ it 'returns first node ancestor' do
133
+ expect(baz.root).to eq(bar)
134
+ end
135
+
136
+ it 'returns nil if node is a root' do
137
+ expect(bar.root).to be nil
138
+ end
139
+ end
140
+
141
+ describe '#ancestors' do
142
+ it 'returns node ancestors' do
143
+ expect(quux.ancestors).to match_array([ qux, bar ])
144
+ expect(qux.ancestors).to match_array([ bar ])
145
+ expect(bar.ancestors).to be_empty
146
+ end
147
+ end
148
+
149
+ describe '#descendants' do
150
+ it 'returns node descendants' do
151
+ expect(bar.descendants).to match_array([ qux, quux, baz ])
152
+ expect(qux.descendants).to match_array([ quux ])
153
+ expect(quux.descendants).to be_empty
154
+ end
155
+ end
156
+
157
+ describe '#subtree' do
158
+ it 'returns node with descendants' do
159
+ expect(bar.subtree.tree).to be_arranged_like({
160
+ bar => {
161
+ qux => {
162
+ quux => {}
163
+ },
164
+ baz => {}
165
+ }
166
+ })
167
+ end
168
+
169
+ it 'returns node if node is a leaf' do
170
+ expect(baz.subtree).to eq([ baz ])
171
+ end
172
+ end
173
+
174
+ describe '#root_of?' do
175
+ it 'returns true of node is a root of given node' do
176
+ expect(bar).to be_root_of(qux)
177
+ expect(bar).to be_root_of(quux)
178
+ expect(bar).not_to be_root_of(bar)
179
+ expect(bar).not_to be_root_of(foo)
180
+ end
181
+ end
182
+
183
+ describe '#ancestor_of?' do
184
+ it 'returns true if node is an ancestors of given node' do
185
+ expect(bar).to be_ancestor_of(qux)
186
+ expect(bar).to be_ancestor_of(quux)
187
+ expect(bar).not_to be_ancestor_of(bar)
188
+ expect(bar).not_to be_ancestor_of(foo)
189
+ end
190
+ end
191
+
192
+ describe '#descendant_of?' do
193
+ it 'returns true if node is a descendant of given node' do
194
+ expect(quux).to be_descendant_of(qux)
195
+ expect(quux).to be_descendant_of(bar)
196
+ expect(quux).not_to be_descendant_of(quux)
197
+ expect(quux).not_to be_descendant_of(foo)
198
+ end
199
+ end
200
+
201
+ describe '#depth' do
202
+ it 'returns ancestors count' do
203
+ expect(bar.depth).to eq(0)
204
+ expect(qux.depth).to eq(1)
205
+ expect(quux.depth).to eq(2)
206
+ end
207
+ end
208
+
209
+ describe 'node id column change' do
210
+ before do
211
+ bar.name = 'bor'
212
+ bar.save!
213
+ end
214
+
215
+ it 'updates children pathes' do
216
+ expect(described_class.find_by_path('bor')).to eq(bar)
217
+ expect(described_class.find_by_path('bor/qux')).to eq(qux)
218
+ expect(described_class.find_by_path('bor/qux/quux')).to eq(quux)
219
+ end
220
+ end
221
+
222
+ describe 'parent change' do
223
+ let(:prev_parent) { bar }
224
+ let(:new_parent) { foo }
225
+ let(:new_ancestors) { [ foo ] }
226
+
227
+ before do
228
+ baz.parent = new_parent
229
+ baz.save!
230
+ reload_items
231
+ end
232
+
233
+ it 'updates counter cache' do
234
+ expect(prev_parent.children_count).to eq(prev_parent.children.count)
235
+ expect(new_parent.children_count).to eq(new_parent.children.count)
236
+
237
+ new_parent.descendants.destroy_all
238
+ new_parent.reload
239
+
240
+ expect(new_parent.children_count).to eq(0)
241
+ end
242
+
243
+ it 'changes ancestors' do
244
+ expect(baz.ancestors).to match_array(new_ancestors)
245
+ end
246
+
247
+ it 'applies to all descendants' do
248
+ baz.children.each do |child|
249
+ expect(child).to be_descendant_of(new_parent)
250
+
251
+ child.children.each do |subchild|
252
+ expect(subchild).to be_descendant_of(new_parent)
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ shared_examples 'tree with cached depth' do
260
+ include_context 'example tree'
261
+ it_behaves_like 'adjacency list'
262
+
263
+ it 'stores node level' do
264
+ expect(described_class.where(depth: 0)).to match_array([ foo, bar ])
265
+ expect(described_class.where(depth: 1)).to match_array([ qux, baz ])
266
+ expect(described_class.where(depth: 2)).to match_array([ quux ])
267
+ end
268
+ end
269
+
270
+ shared_examples 'scoped tree' do
271
+ let!(:foo) { described_class.create!(name: 'foo', category: 'foo') }
272
+ let!(:bar) { described_class.create!(name: 'bar', category: 'bar') }
273
+
274
+ it 'restricts scope' do
275
+ expect(bar.siblings).to be_empty
276
+ end
277
+ end
278
+
279
+ shared_examples 'ordered tree' do
280
+ include_context 'example tree'
281
+
282
+ before(:each) do
283
+ quux.move_after(foo)
284
+ reload_items
285
+ end
286
+
287
+ describe '#previous_siblings' do
288
+ it 'returns siblings with smaller position' do
289
+ expect(bar.previous_siblings).to match_array([ foo, quux ])
290
+ end
291
+ end
292
+
293
+ describe '#next_siblings' do
294
+ it 'returns siblings with greater position' do
295
+ expect(foo.next_siblings).to match_array([ quux, bar ])
296
+ end
297
+ end
298
+
299
+ it '#move_after' do
300
+ expect(described_class.ordered.tree).to be_arranged_like({
301
+ foo => {},
302
+ quux => {},
303
+ bar => {
304
+ qux => {},
305
+ baz => {}
306
+ }
307
+ })
308
+ end
309
+
310
+ it '#move_before' do
311
+ baz.move_before(quux)
312
+ reload_items
313
+
314
+ expect(described_class.ordered.tree).to be_arranged_like({
315
+ foo => {},
316
+ baz => {},
317
+ quux => {},
318
+ bar => {
319
+ qux => {}
320
+ }
321
+ })
322
+ end
323
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_hierarchy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kolesnikov Danil
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-22 00:00:00.000000000 Z
11
+ date: 2014-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -67,33 +67,47 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1'
69
69
  - !ruby/object:Gem::Dependency
70
- name: codeclimate-test-reporter
70
+ name: activerecord
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '4'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '4'
83
83
  - !ruby/object:Gem::Dependency
84
- name: activerecord
84
+ name: mongoid
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
89
  version: '4'
90
- type: :runtime
90
+ type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: codeclimate-test-reporter
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: activesupport
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -114,14 +128,14 @@ dependencies:
114
128
  requirements:
115
129
  - - "~>"
116
130
  - !ruby/object:Gem::Version
117
- version: '0.1'
131
+ version: '0.2'
118
132
  type: :runtime
119
133
  prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
- version: '0.1'
138
+ version: '0.2'
125
139
  description: Provides tree behavior to active_record models.
126
140
  email:
127
141
  - kolesnikovde@gmail.com
@@ -133,21 +147,28 @@ files:
133
147
  - ".gitignore"
134
148
  - ".rspec"
135
149
  - ".travis.yml"
136
- - CHANGELOG.md
137
150
  - Gemfile
138
151
  - README.md
139
152
  - Rakefile
140
153
  - has_hierarchy.gemspec
141
154
  - lib/has_hierarchy.rb
155
+ - lib/has_hierarchy/counter_cache.rb
142
156
  - lib/has_hierarchy/depth_cache.rb
143
157
  - lib/has_hierarchy/order.rb
158
+ - lib/has_hierarchy/orm_adapter.rb
159
+ - lib/has_hierarchy/orm_adapter/active_record.rb
160
+ - lib/has_hierarchy/orm_adapter/mongoid.rb
144
161
  - lib/has_hierarchy/path.rb
145
162
  - lib/has_hierarchy/version.rb
146
- - spec/db/schema.rb
147
163
  - spec/has_hierarchy_spec.rb
148
164
  - spec/spec_helper.rb
149
165
  - spec/support/matchers.rb
150
166
  - spec/support/models.rb
167
+ - spec/support/orm/active_record/item_model.rb
168
+ - spec/support/orm/active_record/setup.rb
169
+ - spec/support/orm/mongoid/item_model.rb
170
+ - spec/support/orm/mongoid/setup.rb
171
+ - spec/tree.rb
151
172
  homepage: https://github.com/kolesnikovde/has_hierarchy
152
173
  licenses:
153
174
  - MIT
@@ -173,8 +194,12 @@ signing_key:
173
194
  specification_version: 4
174
195
  summary: Provides tree behavior to active_record models.
175
196
  test_files:
176
- - spec/db/schema.rb
177
197
  - spec/has_hierarchy_spec.rb
178
198
  - spec/spec_helper.rb
179
199
  - spec/support/matchers.rb
180
200
  - spec/support/models.rb
201
+ - spec/support/orm/active_record/item_model.rb
202
+ - spec/support/orm/active_record/setup.rb
203
+ - spec/support/orm/mongoid/item_model.rb
204
+ - spec/support/orm/mongoid/setup.rb
205
+ - spec/tree.rb
data/CHANGELOG.md DELETED
@@ -1,59 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.3.1
4
-
5
- - Added path normalization to `.find_by_path`
6
- - Added `.find_by_path!`
7
- - Added `#full_path`
8
- - Fixed boolean options.
9
- - Added options validation.
10
-
11
- ## 0.3.0
12
-
13
- - Added "path_separator" option.
14
- - Added ordering support.
15
- - Cleaned up options.
16
- - Renamed to has_hierarchy.
17
-
18
- ## 0.2.1
19
-
20
- - Fixed `.find_by_node_path`.
21
- - Added tree rebuilding on node_id change.
22
- - Added depth caching.
23
-
24
- ## 0.2.0
25
-
26
- - Added custom node path values.
27
- - Added `.find_by_node_path`.
28
- - Added `#child_of?`.
29
- - Updated "node_path_column" option (renamed to "node_path_cache").
30
- - Rewrited specs.
31
-
32
- ## 0.1.3
33
-
34
- - Added README.md.
35
- - Added `#leaf?`.
36
- - Added `has_children_options` accessor.
37
- - Added counter cache and root scope specs.
38
- - Updated "orphan_strategy" option (renamed to "dependent").
39
- - Updated rake tasks.
40
- - Updated .gitignore.
41
- - Updated codestyle.
42
- - Fixed rspec deprication warnings.
43
-
44
- ## 0.1.2
45
-
46
- - Added root association.
47
- - Added `#root_id`.
48
- - Added `#root_of?`.
49
- - Added `#parent_of?`.
50
-
51
- ## 0.1.1
52
-
53
- - Fixed scopes (always using lambdas).
54
-
55
- ## 0.1.0
56
-
57
- - Added `#move_children_to_parent`.
58
- - `#ancestor_tokens` renamed to `#ancestor_ids`.
59
- - Added lambda scopes support.