has_hierarchy 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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.