closure_tree 6.0.0.alpha → 6.0.0.gamma

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: d7fc5558a4e6b00aab12761e615cd4fe3925ce3c
4
- data.tar.gz: baf418817e89ee4dbc66717627e4871eaed2134c
3
+ metadata.gz: d32d23158e9b974aeffc5fe307d6dc59aa37907b
4
+ data.tar.gz: 190e8a6f9a7e8678657f82de4335aa14f9239a0c
5
5
  SHA512:
6
- metadata.gz: ec01be25fc3f9c11c539c6892603839d33f77f4e4429653d3e3ba68c4548a0921f26311c9fe7cade4cf0c9bbee13ab00546b8e8b677d8e1b567379c8f3fb1458
7
- data.tar.gz: 2f9021c6d64f1d3d0436bcc6439113bdb12fe7d768ead39d6523498f0e03a2a6e5abf04cbfec607af82f8f512d552559d995b50f350ccc7095cee0207a476929
6
+ metadata.gz: a7cbc97513019349f2c193dcf3976bb402406b68e3c285a19d07717a0e3ff24f5fa7860aef0589ba634a3d0f57046eb8580bcb20587d14ee2d786277ba5d1401
7
+ data.tar.gz: e9efa70b8a6c7c5e048296ceef1d285d210db2e89497fd2ea8179ebdef60b85b53b174a9d8935a7aa69530e100a274eadc635ec89e98d4e194e4cfadc5136250
data/.travis.yml CHANGED
@@ -2,7 +2,7 @@ cache: bundler
2
2
  sudo: false
3
3
  language: ruby
4
4
  rvm:
5
- - 2.2
5
+ - 2.2.3
6
6
  - jruby-head
7
7
 
8
8
  gemfile:
data/CHANGELOG.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # Changelog
2
2
 
3
3
  ### 6.0.0.alpha
4
- * Drop support for unsupported version of Rails
4
+ * Drop support for unsupported versions of Rails, 3.2 and 4.0.
5
+ * Drop support for Ruby 1.9 and JRuby 1.9
6
+ * Added support for `.hash_tree` from `.parent` and `.children`.
7
+ Addresses [PR146](https://github.com/mceachen/closure_tree/pull/146).
8
+ Thanks for reporting this and the breaking test, [Mike](https://github.com/mkralla11)!
5
9
 
6
10
  ### 5.2.0
7
11
 
data/README.md CHANGED
@@ -480,7 +480,7 @@ the spec ```tag_spec.rb```:
480
480
  Tag.rebuild! # <- required if you use fixtures
481
481
  end
482
482
  ```
483
- `
483
+
484
484
  **However, if you're just starting with Rails, may I humbly suggest you adopt a factory library**,
485
485
  rather than using fixtures? [Lots of people have written about this already](https://www.google.com/search?q=fixtures+versus+factories).
486
486
 
@@ -552,7 +552,7 @@ See the [change log](https://github.com/mceachen/closure_tree/blob/master/CHANGE
552
552
 
553
553
  ## Thanks to
554
554
 
555
- * The more than 20 engineers around the world that have contributed their time and code to this gem
555
+ * The more than 30 engineers around the world that have contributed their time and code to this gem
556
556
  (see the [changelog](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md)!)
557
557
  * https://github.com/collectiveidea/awesome_nested_set
558
558
  * https://github.com/patshaughnessy/class_factory
@@ -1,5 +1,10 @@
1
1
  module ClosureTree
2
2
  module ActiveRecordSupport
3
+
4
+ def quote(field)
5
+ connection.quote(field)
6
+ end
7
+
3
8
  def ensure_fixed_table_name(table_name)
4
9
  [
5
10
  ActiveRecord::Base.table_name_prefix,
@@ -52,11 +52,6 @@ module ClosureTree
52
52
 
53
53
  module ClassMethods
54
54
 
55
- # Fix deprecation warning:
56
- def _ct_all
57
- (ActiveRecord::VERSION::MAJOR >= 4) ? all : scoped
58
- end
59
-
60
55
  def without(instance)
61
56
  if instance.new_record?
62
57
  all
@@ -88,7 +83,7 @@ module ClosureTree
88
83
 
89
84
  def with_ancestor(*ancestors)
90
85
  ancestor_ids = ancestors.map { |ea| ea.is_a?(ActiveRecord::Base) ? ea._ct_id : ea }
91
- scope = ancestor_ids.blank? ? _ct_all : joins(:ancestor_hierarchies).
86
+ scope = ancestor_ids.blank? ? all : joins(:ancestor_hierarchies).
92
87
  where("#{_ct.hierarchy_table_name}.ancestor_id" => ancestor_ids).
93
88
  where("#{_ct.hierarchy_table_name}.generations > 0").
94
89
  readonly(false)
@@ -2,17 +2,8 @@ module ClosureTree
2
2
  module HashTree
3
3
  extend ActiveSupport::Concern
4
4
 
5
- def hash_tree_scope(limit_depth = nil)
6
- scope = self_and_descendants
7
- if limit_depth
8
- scope.where("#{_ct.quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
9
- else
10
- scope
11
- end
12
- end
13
-
14
5
  def hash_tree(options = {})
15
- self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
6
+ _ct.hash_tree(self_and_descendants, options[:limit_depth])
16
7
  end
17
8
 
18
9
  module ClassMethods
@@ -20,39 +11,7 @@ module ClosureTree
20
11
  # There is no default depth limit. This might be crazy-big, depending
21
12
  # on your tree shape. Hash huge trees at your own peril!
22
13
  def hash_tree(options = {})
23
- build_hash_tree(hash_tree_scope(options[:limit_depth]))
24
- end
25
-
26
- def hash_tree_scope(limit_depth = nil)
27
- # Deepest generation, within limit, for each descendant
28
- # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
29
- having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
30
- generation_depth = <<-SQL.strip_heredoc
31
- INNER JOIN (
32
- SELECT descendant_id, MAX(generations) as depth
33
- FROM #{_ct.quoted_hierarchy_table_name}
34
- GROUP BY descendant_id
35
- #{having_clause}
36
- ) AS generation_depth
37
- ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
38
- SQL
39
- _ct.scope_with_order(joins(generation_depth), "generation_depth.depth")
40
- end
41
-
42
- # Builds nested hash structure using the scope returned from the passed in scope
43
- def build_hash_tree(tree_scope)
44
- tree = ActiveSupport::OrderedHash.new
45
- id_to_hash = {}
46
-
47
- tree_scope.each do |ea|
48
- h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
49
- if ea.root? || tree.empty? # We're at the top of the tree.
50
- tree[ea] = h
51
- else
52
- id_to_hash[ea._ct_parent_id][ea] = h
53
- end
54
- end
55
- tree
14
+ _ct.hash_tree(nil, options[:limit_depth])
56
15
  end
57
16
  end
58
17
  end
@@ -0,0 +1,40 @@
1
+ module ClosureTree
2
+ module HashTreeSupport
3
+ def default_tree_scope(limit_depth = nil)
4
+ # Deepest generation, within limit, for each descendant
5
+ # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
6
+ having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
7
+ generation_depth = <<-SQL.strip_heredoc
8
+ INNER JOIN (
9
+ SELECT descendant_id, MAX(generations) as depth
10
+ FROM #{quoted_hierarchy_table_name}
11
+ GROUP BY descendant_id
12
+ #{having_clause}
13
+ ) AS generation_depth
14
+ ON #{quoted_table_name}.#{model_class.primary_key} = generation_depth.descendant_id
15
+ SQL
16
+ scope_with_order(model_class.joins(generation_depth), 'generation_depth.depth')
17
+ end
18
+
19
+ def hash_tree(tree_scope, limit_depth = nil)
20
+ limited_scope = if tree_scope
21
+ limit_depth ? tree_scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}") : tree_scope
22
+ else
23
+ default_tree_scope(limit_depth)
24
+ end
25
+ build_hash_tree(limited_scope)
26
+ end
27
+
28
+ # Builds nested hash structure using the scope returned from the passed in scope
29
+ def build_hash_tree(tree_scope)
30
+ tree = ActiveSupport::OrderedHash.new
31
+ id_to_hash = {}
32
+
33
+ tree_scope.each do |ea|
34
+ h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
35
+ (id_to_hash[ea._ct_parent_id] || tree)[ea] = h
36
+ end
37
+ tree
38
+ end
39
+ end
40
+ end
@@ -17,7 +17,14 @@ module ClosureTree
17
17
  class_name: _ct.model_class.to_s,
18
18
  foreign_key: _ct.parent_column_name,
19
19
  dependent: _ct.options[:dependent],
20
- inverse_of: :parent)
20
+ inverse_of: :parent) do
21
+ # We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
22
+ def hash_tree(options = {})
23
+ # we want limit_depth + 1 because we don't do self_and_descendants.
24
+ limit_depth = options[:limit_depth]
25
+ _ct.hash_tree(@association.owner.descendants, limit_depth ? limit_depth + 1 : nil)
26
+ end
27
+ end
21
28
 
22
29
  has_many :ancestor_hierarchies, *_ct.has_many_without_order_option(
23
30
  class_name: _ct.hierarchy_class_name,
@@ -27,13 +27,6 @@ module ClosureTree
27
27
 
28
28
  def self_and_descendants_preordered
29
29
  # TODO: raise NotImplementedError if sort_order is not numeric and not null?
30
- h = _ct.connection.select_one(<<-SQL)
31
- SELECT
32
- count(*) as total_descendants,
33
- max(generations) as max_depth
34
- FROM #{_ct.quoted_hierarchy_table_name}
35
- WHERE ancestor_id = #{_ct.quote(self.id)}
36
- SQL
37
30
  join_sql = <<-SQL
38
31
  JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
39
32
  ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
@@ -42,20 +35,35 @@ module ClosureTree
42
35
  JOIN #{_ct.quoted_hierarchy_table_name} depths
43
36
  ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.#{_ct.quoted_id_column_name}
44
37
  SQL
45
- node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
46
- "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.generations)"
47
- order_by = "sum(#{node_score})"
48
- self_and_descendants.joins(join_sql).group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}").reorder(order_by)
38
+
39
+ self_and_descendants
40
+ .joins(join_sql)
41
+ .group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
42
+ .reorder(self.class._ct_sum_order_by(self))
49
43
  end
50
44
 
51
45
  module ClassMethods
52
- def roots_and_descendants_preordered
53
- h = _ct.connection.select_one(<<-SQL.strip_heredoc)
46
+
47
+ # If node is nil, order the whole tree.
48
+ def _ct_sum_order_by(node = nil)
49
+ stats_sql = <<-SQL.strip_heredoc
54
50
  SELECT
55
51
  count(*) as total_descendants,
56
52
  max(generations) as max_depth
57
53
  FROM #{_ct.quoted_hierarchy_table_name}
58
54
  SQL
55
+ stats_sql += " WHERE ancestor_id = #{_ct.quote(node.id)}" if node
56
+ h = _ct.connection.select_one(stats_sql)
57
+
58
+ depth_column = node ? 'depths.generations' : 'depths.max_depth'
59
+
60
+ node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
61
+ "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
62
+
63
+ "sum(#{node_score})"
64
+ end
65
+
66
+ def roots_and_descendants_preordered
59
67
  join_sql = <<-SQL.strip_heredoc
60
68
  JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
61
69
  ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
@@ -67,10 +75,9 @@ module ClosureTree
67
75
  GROUP BY 1
68
76
  ) AS depths ON depths.descendant_id = anc.#{_ct.quoted_id_column_name}
69
77
  SQL
70
- node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
71
- "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.max_depth)"
72
- order_by = "sum(#{node_score})"
73
- joins(join_sql).group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}").reorder(order_by)
78
+ joins(join_sql)
79
+ .group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
80
+ .reorder(_ct_sum_order_by)
74
81
  end
75
82
  end
76
83
 
@@ -2,6 +2,7 @@ require 'closure_tree/support_flags'
2
2
  require 'closure_tree/support_attributes'
3
3
  require 'closure_tree/numeric_order_support'
4
4
  require 'closure_tree/active_record_support'
5
+ require 'closure_tree/hash_tree_support'
5
6
  require 'with_advisory_lock'
6
7
 
7
8
  # This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
@@ -10,6 +11,7 @@ module ClosureTree
10
11
  include ClosureTree::SupportFlags
11
12
  include ClosureTree::SupportAttributes
12
13
  include ClosureTree::ActiveRecordSupport
14
+ include ClosureTree::HashTreeSupport
13
15
 
14
16
  attr_reader :model_class
15
17
  attr_reader :options
@@ -22,7 +24,7 @@ module ClosureTree
22
24
  :name_column => 'name',
23
25
  :with_advisory_lock => true
24
26
  }.merge(options)
25
- raise IllegalArgumentException, "name_column can't be 'path'" if options[:name_column] == 'path'
27
+ raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
26
28
  if order_is_numeric?
27
29
  extend NumericOrderSupport.adapter_for_connection(connection)
28
30
  end
@@ -59,10 +61,6 @@ module ClosureTree
59
61
  ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
60
62
  end
61
63
 
62
- def quote(field)
63
- connection.quote(field)
64
- end
65
-
66
64
  def with_order_option(opts)
67
65
  if order_option?
68
66
  opts[:order] = [opts[:order], order_by].compact.join(",")
@@ -80,20 +78,12 @@ module ClosureTree
80
78
 
81
79
  # lambda-ize the order, but don't apply the default order_option
82
80
  def has_many_without_order_option(opts)
83
- if ActiveRecord::VERSION::MAJOR > 3
84
81
  [lambda { order(opts[:order]) }, opts.except(:order)]
85
- else
86
- [opts]
87
- end
88
82
  end
89
83
 
90
84
  def has_many_with_order_option(opts)
91
- if ActiveRecord::VERSION::MAJOR > 3
92
- order_options = [opts[:order], order_by].compact
93
- [lambda { order(order_options) }, opts.except(:order)]
94
- else
95
- [with_order_option(opts)]
96
- end
85
+ order_options = [opts[:order], order_by].compact
86
+ [lambda { order(order_options) }, opts.except(:order)]
97
87
  end
98
88
 
99
89
  def ids_from(scope)
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = Gem::Version.new('6.0.0.alpha')
2
+ VERSION = Gem::Version.new('6.0.0.gamma')
3
3
  end
@@ -45,7 +45,7 @@ require "#{database_folder}/models"
45
45
  def count_queries(&block)
46
46
  count = 0
47
47
  counter_fn = ->(name, started, finished, unique_id, payload) do
48
- count += 1 unless payload[:name].in? %w[CACHE SCHEMA]
48
+ count += 1 unless %w[CACHE SCHEMA].include? payload[:name]
49
49
  end
50
50
  ActiveSupport::Notifications.subscribed(counter_fn, 'sql.active_record', &block)
51
51
  count
data/spec/tag_examples.rb CHANGED
@@ -20,25 +20,25 @@ shared_examples_for Tag do
20
20
  end
21
21
 
22
22
  it 'should have a correct parent column name' do
23
- expected_parent_column_name = tag_class == UUIDTag ? "parent_uuid" : "parent_id"
23
+ expected_parent_column_name = tag_class == UUIDTag ? 'parent_uuid' : 'parent_id'
24
24
  expect(tag_class._ct.parent_column_name).to eq(expected_parent_column_name)
25
25
  end
26
26
  end
27
27
 
28
- describe "from empty db" do
28
+ describe 'from empty db' do
29
29
 
30
- context "with no tags" do
31
- it "should return no entities" do
30
+ context 'with no tags' do
31
+ it 'should return no entities' do
32
32
  expect(tag_class.roots).to be_empty
33
33
  expect(tag_class.leaves).to be_empty
34
34
  end
35
35
 
36
- it "#find_or_create_by_path with strings" do
36
+ it '#find_or_create_by_path with strings' do
37
37
  a = tag_class.create!(name: 'a')
38
38
  expect(a.find_or_create_by_path(%w{b c}).ancestry_path).to eq(%w{a b c})
39
39
  end
40
40
 
41
- it "#find_or_create_by_path with hashes" do
41
+ it '#find_or_create_by_path with hashes' do
42
42
  a = tag_class.create!(name: 'a', title: 'A')
43
43
  subject = a.find_or_create_by_path([
44
44
  {name: 'b', title: 'B'},
@@ -49,16 +49,16 @@ shared_examples_for Tag do
49
49
  end
50
50
  end
51
51
 
52
- context "with 1 tag" do
52
+ context 'with 1 tag' do
53
53
  before do
54
- @tag = tag_class.create!(name: "tag")
54
+ @tag = tag_class.create!(name: 'tag')
55
55
  end
56
56
 
57
- it "should be a leaf" do
57
+ it 'should be a leaf' do
58
58
  expect(@tag.leaf?).to be_truthy
59
59
  end
60
60
 
61
- it "should be a root" do
61
+ it 'should be a root' do
62
62
  expect(@tag.root?).to be_truthy
63
63
  end
64
64
 
@@ -66,13 +66,13 @@ shared_examples_for Tag do
66
66
  expect(@tag.parent).to be_nil
67
67
  end
68
68
 
69
- it "should return the only entity as a root and leaf" do
69
+ it 'should return the only entity as a root and leaf' do
70
70
  expect(tag_class.all).to eq([@tag])
71
71
  expect(tag_class.roots).to eq([@tag])
72
72
  expect(tag_class.leaves).to eq([@tag])
73
73
  end
74
74
 
75
- context "with child" do
75
+ context 'with child' do
76
76
  before do
77
77
  @child = tag_class.create!(name: 'tag 2')
78
78
  end
@@ -90,13 +90,13 @@ shared_examples_for Tag do
90
90
  expect(@tag.reload.children.to_a).to eq([@child])
91
91
  end
92
92
 
93
- it "adds children through add_child" do
93
+ it 'adds children through add_child' do
94
94
  @tag.add_child @child
95
95
  assert_roots_and_leaves
96
96
  assert_parent_and_children
97
97
  end
98
98
 
99
- it "adds children through collection" do
99
+ it 'adds children through collection' do
100
100
  @tag.children << @child
101
101
  assert_roots_and_leaves
102
102
  assert_parent_and_children
@@ -104,48 +104,48 @@ shared_examples_for Tag do
104
104
  end
105
105
  end
106
106
 
107
- context "with 2 tags" do
107
+ context 'with 2 tags' do
108
108
  before :each do
109
- @root = tag_class.create!(name: "root")
110
- @leaf = @root.add_child(tag_class.create!(name: "leaf"))
109
+ @root = tag_class.create!(name: 'root')
110
+ @leaf = @root.add_child(tag_class.create!(name: 'leaf'))
111
111
  end
112
- it "should return a simple root and leaf" do
112
+ it 'should return a simple root and leaf' do
113
113
  expect(tag_class.roots).to eq([@root])
114
114
  expect(tag_class.leaves).to eq([@leaf])
115
115
  end
116
- it "should return child_ids for root" do
116
+ it 'should return child_ids for root' do
117
117
  expect(@root.child_ids).to eq([@leaf.id])
118
118
  end
119
119
 
120
- it "should return an empty array for leaves" do
120
+ it 'should return an empty array for leaves' do
121
121
  expect(@leaf.child_ids).to be_empty
122
122
  end
123
123
  end
124
124
 
125
- context "3 tag collection.create db" do
125
+ context '3 tag collection.create db' do
126
126
  before :each do
127
- @root = tag_class.create! name: "root"
128
- @mid = @root.children.create! name: "mid"
129
- @leaf = @mid.children.create! name: "leaf"
127
+ @root = tag_class.create! name: 'root'
128
+ @mid = @root.children.create! name: 'mid'
129
+ @leaf = @mid.children.create! name: 'leaf'
130
130
  DestroyedTag.delete_all
131
131
  end
132
132
 
133
- it "should create all tags" do
133
+ it 'should create all tags' do
134
134
  expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf])
135
135
  end
136
136
 
137
- it "should return a root and leaf without middle tag" do
137
+ it 'should return a root and leaf without middle tag' do
138
138
  expect(tag_class.roots).to eq([@root])
139
139
  expect(tag_class.leaves).to eq([@leaf])
140
140
  end
141
141
 
142
- it "should delete leaves" do
142
+ it 'should delete leaves' do
143
143
  tag_class.leaves.destroy_all
144
144
  expect(tag_class.roots).to eq([@root]) # untouched
145
145
  expect(tag_class.leaves).to eq([@mid])
146
146
  end
147
147
 
148
- it "should delete everything if you delete the roots" do
148
+ it 'should delete everything if you delete the roots' do
149
149
  tag_class.roots.destroy_all
150
150
  expect(tag_class.all).to be_empty
151
151
  expect(tag_class.roots).to be_empty
@@ -167,7 +167,7 @@ shared_examples_for Tag do
167
167
  end
168
168
 
169
169
  it 'moves non-leaves' do
170
- new_root = tag_class.create! name: "new_root"
170
+ new_root = tag_class.create! name: 'new_root'
171
171
  new_root.children << @mid
172
172
  expect(@root.reload.descendants).to be_empty
173
173
  expect(new_root.descendants).to eq([@mid, @leaf])
@@ -175,7 +175,7 @@ shared_examples_for Tag do
175
175
  end
176
176
 
177
177
  it 'moves leaves' do
178
- new_root = tag_class.create! name: "new_root"
178
+ new_root = tag_class.create! name: 'new_root'
179
179
  new_root.children << @leaf
180
180
  expect(new_root.descendants).to eq([@leaf])
181
181
  expect(@root.reload.descendants).to eq([@mid])
@@ -183,46 +183,46 @@ shared_examples_for Tag do
183
183
  end
184
184
  end
185
185
 
186
- context "3 tag explicit_create db" do
186
+ context '3 tag explicit_create db' do
187
187
  before :each do
188
- @root = tag_class.create!(name: "root")
189
- @mid = @root.add_child(tag_class.create!(name: "mid"))
190
- @leaf = @mid.add_child(tag_class.create!(name: "leaf"))
188
+ @root = tag_class.create!(name: 'root')
189
+ @mid = @root.add_child(tag_class.create!(name: 'mid'))
190
+ @leaf = @mid.add_child(tag_class.create!(name: 'leaf'))
191
191
  end
192
192
 
193
- it "should create all tags" do
193
+ it 'should create all tags' do
194
194
  expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf])
195
195
  end
196
196
 
197
- it "should return a root and leaf without middle tag" do
197
+ it 'should return a root and leaf without middle tag' do
198
198
  expect(tag_class.roots).to eq([@root])
199
199
  expect(tag_class.leaves).to eq([@leaf])
200
200
  end
201
201
 
202
- it "should prevent parental loops from torso" do
202
+ it 'should prevent parental loops from torso' do
203
203
  @mid.children << @root
204
204
  expect(@root.valid?).to be_falsey
205
205
  expect(@mid.reload.children).to eq([@leaf])
206
206
  end
207
207
 
208
- it "should prevent parental loops from toes" do
208
+ it 'should prevent parental loops from toes' do
209
209
  @leaf.children << @root
210
210
  expect(@root.valid?).to be_falsey
211
211
  expect(@leaf.reload.children).to be_empty
212
212
  end
213
213
 
214
- it "should support re-parenting" do
214
+ it 'should support re-parenting' do
215
215
  @root.children << @leaf
216
216
  expect(tag_class.leaves).to eq([@leaf, @mid])
217
217
  end
218
218
 
219
- it "cleans up hierarchy references for leaves" do
219
+ it 'cleans up hierarchy references for leaves' do
220
220
  @leaf.destroy
221
221
  expect(tag_hierarchy_class.where(ancestor_id: @leaf.id)).to be_empty
222
222
  expect(tag_hierarchy_class.where(descendant_id: @leaf.id)).to be_empty
223
223
  end
224
224
 
225
- it "cleans up hierarchy references" do
225
+ it 'cleans up hierarchy references' do
226
226
  @mid.destroy
227
227
  expect(tag_hierarchy_class.where(ancestor_id: @mid.id)).to be_empty
228
228
  expect(tag_hierarchy_class.where(descendant_id: @mid.id)).to be_empty
@@ -233,17 +233,17 @@ shared_examples_for Tag do
233
233
  expect(tag_hierarchy_class.where(descendant_id: @root.id)).to eq(root_hiers)
234
234
  end
235
235
 
236
- it "should have different hash codes for each hierarchy model" do
236
+ it 'should have different hash codes for each hierarchy model' do
237
237
  hashes = tag_hierarchy_class.all.map(&:hash)
238
238
  expect(hashes).to match_array(hashes.uniq)
239
239
  end
240
240
 
241
- it "should return the same hash code for equal hierarchy models" do
241
+ it 'should return the same hash code for equal hierarchy models' do
242
242
  expect(tag_hierarchy_class.first.hash).to eq(tag_hierarchy_class.first.hash)
243
243
  end
244
244
  end
245
245
 
246
- it "performs as the readme says it does" do
246
+ it 'performs as the readme says it does' do
247
247
  grandparent = tag_class.create(name: 'Grandparent')
248
248
  parent = grandparent.children.create(name: 'Parent')
249
249
  child1 = tag_class.create(name: 'First Child', parent: parent)
@@ -252,13 +252,13 @@ shared_examples_for Tag do
252
252
  child3 = tag_class.new(name: 'Third Child')
253
253
  parent.add_child child3
254
254
  expect(grandparent.self_and_descendants.collect(&:name)).to eq(
255
- ["Grandparent", "Parent", "First Child", "Second Child", "Third Child"]
255
+ ['Grandparent', 'Parent', 'First Child', 'Second Child', 'Third Child']
256
256
  )
257
257
  expect(child1.ancestry_path).to eq(
258
- ["Grandparent", "Parent", "First Child"]
258
+ ['Grandparent', 'Parent', 'First Child']
259
259
  )
260
260
  expect(child3.ancestry_path).to eq(
261
- ["Grandparent", "Parent", "Third Child"]
261
+ ['Grandparent', 'Parent', 'Third Child']
262
262
  )
263
263
  d = tag_class.find_or_create_by_path %w(a b c d)
264
264
  h = tag_class.find_or_create_by_path %w(e f g h)
@@ -267,8 +267,8 @@ shared_examples_for Tag do
267
267
  expect(h.ancestry_path).to eq(%w(a b c d e f g h))
268
268
  end
269
269
 
270
- it "roots sort alphabetically" do
271
- expected = ("a".."z").to_a
270
+ it 'roots sort alphabetically' do
271
+ expected = ('a'..'z').to_a
272
272
  expected.shuffle.each { |ea| tag_class.create!(name: ea) }
273
273
  expect(tag_class.roots.collect { |ea| ea.name }).to eq(expected)
274
274
  end
@@ -403,7 +403,7 @@ shared_examples_for Tag do
403
403
  expect(@child.self_and_ancestors).to eq([@child, @parent, @grandparent])
404
404
  end
405
405
 
406
- it "should find by path" do
406
+ it 'should find by path' do
407
407
  # class method:
408
408
  expect(tag_class.find_by_path(%w{grandparent parent child})).to eq(@child)
409
409
  # instance method:
@@ -412,7 +412,7 @@ shared_examples_for Tag do
412
412
  expect(@parent.find_by_path(%w{child larvae})).to be_nil
413
413
  end
414
414
 
415
- it "should respect attribute hashes with both selection and creation" do
415
+ it 'should respect attribute hashes with both selection and creation' do
416
416
  expected_title = 'something else'
417
417
  attrs = {title: expected_title}
418
418
  existing_title = @grandparent.title
@@ -422,7 +422,7 @@ shared_examples_for Tag do
422
422
  expect(@grandparent.reload.title).to eq(existing_title)
423
423
  end
424
424
 
425
- it "should create a hierarchy with a given attribute" do
425
+ it 'should create a hierarchy with a given attribute' do
426
426
  expected_title = 'unicorn rainbows'
427
427
  attrs = {title: expected_title}
428
428
  child = tag_class.find_or_create_by_path(%w{grandparent parent child}, attrs)
@@ -433,20 +433,20 @@ shared_examples_for Tag do
433
433
  end
434
434
  end
435
435
 
436
- it "finds correctly rooted paths" do
436
+ it 'finds correctly rooted paths' do
437
437
  decoy = tag_class.find_or_create_by_path %w(a b c d)
438
438
  b_d = tag_class.find_or_create_by_path %w(b c d)
439
439
  expect(tag_class.find_by_path(%w(b c d))).to eq(b_d)
440
440
  expect(tag_class.find_by_path(%w(c d))).to be_nil
441
441
  end
442
442
 
443
- it "find_by_path for 1 node" do
443
+ it 'find_by_path for 1 node' do
444
444
  b = tag_class.find_or_create_by_path %w(a b)
445
445
  b2 = b.root.find_by_path(%w(b))
446
446
  expect(b2).to eq(b)
447
447
  end
448
448
 
449
- it "find_by_path for 2 nodes" do
449
+ it 'find_by_path for 2 nodes' do
450
450
  path = %w(a b c)
451
451
  c = tag_class.find_or_create_by_path path
452
452
  permutations = path.permutation.to_a
@@ -457,34 +457,34 @@ shared_examples_for Tag do
457
457
  end
458
458
  end
459
459
 
460
- it "find_by_path for 3 nodes" do
460
+ it 'find_by_path for 3 nodes' do
461
461
  d = tag_class.find_or_create_by_path %w(a b c d)
462
462
  expect(d.root.find_by_path(%w(b c d))).to eq(d)
463
463
  expect(tag_class.find_by_path(%w(a b c d))).to eq(d)
464
464
  expect(tag_class.find_by_path(%w(d))).to be_nil
465
465
  end
466
466
 
467
- it "should return nil for missing nodes" do
467
+ it 'should return nil for missing nodes' do
468
468
  expect(tag_class.find_by_path(%w{missing})).to be_nil
469
469
  expect(tag_class.find_by_path(%w{grandparent missing})).to be_nil
470
470
  expect(tag_class.find_by_path(%w{grandparent parent missing})).to be_nil
471
471
  expect(tag_class.find_by_path(%w{grandparent parent missing child})).to be_nil
472
472
  end
473
473
 
474
- describe ".find_or_create_by_path" do
475
- it "uses existing records" do
474
+ describe '.find_or_create_by_path' do
475
+ it 'uses existing records' do
476
476
  grandparent = tag_class.find_or_create_by_path(%w{grandparent})
477
477
  expect(grandparent).to eq(grandparent)
478
478
  child = tag_class.find_or_create_by_path(%w{grandparent parent child})
479
479
  expect(child).to eq(child)
480
480
  end
481
481
 
482
- it "creates 2-deep trees with strings" do
482
+ it 'creates 2-deep trees with strings' do
483
483
  subject = tag_class.find_or_create_by_path(%w{events anniversary})
484
484
  expect(subject.ancestry_path).to eq(%w{events anniversary})
485
485
  end
486
486
 
487
- it "creates 2-deep trees with hashes" do
487
+ it 'creates 2-deep trees with hashes' do
488
488
  subject = tag_class.find_or_create_by_path([
489
489
  {name: 'test1', title: 'TEST1'},
490
490
  {name: 'test2', title: 'TEST2'}
@@ -497,93 +497,129 @@ shared_examples_for Tag do
497
497
  end
498
498
  end
499
499
 
500
- context "hash_tree" do
501
-
500
+ context 'hash_tree' do
502
501
  before :each do
503
- @b = tag_class.find_or_create_by_path %w(a b)
502
+ @d1 = tag_class.find_or_create_by_path %w(a b c1 d1)
503
+ @c1 = @d1.parent
504
+ @b = @c1.parent
504
505
  @a = @b.parent
506
+ @a2 = tag_class.create(name: 'a2')
505
507
  @b2 = tag_class.find_or_create_by_path %w(a b2)
506
- @d1 = @b.find_or_create_by_path %w(c1 d1)
507
- @c1 = @d1.parent
508
- @d2 = @b.find_or_create_by_path %w(c2 d2)
509
- @c2 = @d2.parent
510
- @full_tree = {@a => {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}, @b2 => {}}}
508
+ @c3 = tag_class.find_or_create_by_path %w(a3 b3 c3)
509
+ @b3 = @c3.parent
510
+ @a3 = @b3.parent
511
+ @tree2 = {
512
+ @a => {@b => {}, @b2 => {}}, @a2 => {}, @a3 => {@b3 => {}}
513
+ }
514
+
515
+ @one_tree = {
516
+ @a => {},
517
+ @a2 => {},
518
+ @a3 => {}
519
+ }
520
+ @two_tree = {
521
+ @a => {
522
+ @b => {},
523
+ @b2 => {}
524
+ },
525
+ @a2 => {},
526
+ @a3 => {
527
+ @b3 => {}
528
+ }
529
+ }
530
+ @three_tree = {
531
+ @a => {
532
+ @b => {
533
+ @c1 => {},
534
+ },
535
+ @b2 => {}
536
+ },
537
+ @a2 => {},
538
+ @a3 => {
539
+ @b3 => {
540
+ @c3 => {}
541
+ }
542
+ }
543
+ }
544
+ @full_tree = {
545
+ @a => {
546
+ @b => {
547
+ @c1 => {
548
+ @d1 => {}
549
+ },
550
+ },
551
+ @b2 => {}
552
+ },
553
+ @a2 => {},
554
+ @a3 => {
555
+ @b3 => {
556
+ @c3 => {}
557
+ }
558
+ }
559
+ }
511
560
  #File.open("example.dot", "w") { |f| f.write(tag_class.root.to_dot_digraph) }
512
561
  end
513
562
 
514
- context "#hash_tree" do
515
- it "returns {} for depth 0" do
563
+ context '#hash_tree' do
564
+ it 'returns {} for depth 0' do
516
565
  expect(tag_class.hash_tree(limit_depth: 0)).to eq({})
517
566
  end
518
- it "limit_depth 1" do
519
- expect(tag_class.hash_tree(limit_depth: 1)).to eq({@a => {}})
567
+ it 'limit_depth 1' do
568
+ expect(tag_class.hash_tree(limit_depth: 1)).to eq(@one_tree)
520
569
  end
521
- it "limit_depth 2" do
522
- expect(tag_class.hash_tree(limit_depth: 2)).to eq({@a => {@b => {}, @b2 => {}}})
570
+ it 'limit_depth 2' do
571
+ expect(tag_class.hash_tree(limit_depth: 2)).to eq(@two_tree)
523
572
  end
524
- it "limit_depth 3" do
525
- expect(tag_class.hash_tree(limit_depth: 3)).to eq({@a => {@b => {@c1 => {}, @c2 => {}}, @b2 => {}}})
573
+ it 'limit_depth 3' do
574
+ expect(tag_class.hash_tree(limit_depth: 3)).to eq(@three_tree)
526
575
  end
527
- it "limit_depth 4" do
576
+ it 'limit_depth 4' do
528
577
  expect(tag_class.hash_tree(limit_depth: 4)).to eq(@full_tree)
529
578
  end
530
- it "no limit holdum" do
579
+ it 'no limit' do
531
580
  expect(tag_class.hash_tree).to eq(@full_tree)
532
581
  end
533
582
  end
534
583
 
535
- def assert_no_dupes(scope)
536
- # the named scope is complicated enough that an incorrect join could result in unnecessarily
537
- # duplicated rows:
538
- a = scope.collect { |ea| ea.id }
539
- expect(a).to eq(a.uniq)
540
- end
541
-
542
- context "#hash_tree_scope" do
543
- it "no dupes for any depth" do
544
- (0..5).each do |ea|
545
- assert_no_dupes(tag_class.hash_tree_scope(ea))
546
- end
584
+ context '.hash_tree' do
585
+ it 'returns {} for depth 0' do
586
+ expect(@b.hash_tree(limit_depth: 0)).to eq({})
547
587
  end
548
- it "no limit holdum" do
549
- assert_no_dupes(tag_class.hash_tree_scope)
588
+ it 'limit_depth 1' do
589
+ expect(@b.hash_tree(limit_depth: 1)).to eq(@two_tree[@a].slice(@b))
550
590
  end
551
- end
552
-
553
- context ".hash_tree_scope" do
554
- it "no dupes for any depth" do
555
- (0..5).each do |ea|
556
- assert_no_dupes(@a.hash_tree_scope(ea))
557
- end
591
+ it 'limit_depth 2' do
592
+ expect(@b.hash_tree(limit_depth: 2)).to eq(@three_tree[@a].slice(@b))
558
593
  end
559
- it "no limit holdum" do
560
- assert_no_dupes(@a.hash_tree_scope)
594
+ it 'limit_depth 3' do
595
+ expect(@b.hash_tree(limit_depth: 3)).to eq(@full_tree[@a].slice(@b))
561
596
  end
562
- end
563
-
564
- context ".hash_tree" do
565
- before :each do
597
+ it 'no limit from subsubroot' do
598
+ expect(@c1.hash_tree).to eq(@full_tree[@a][@b].slice(@c1))
566
599
  end
567
- it "returns {} for depth 0" do
568
- expect(@b.hash_tree(limit_depth: 0)).to eq({})
600
+ it 'no limit from subroot' do
601
+ expect(@b.hash_tree).to eq(@full_tree[@a].slice(@b))
569
602
  end
570
- it "limit_depth 1" do
571
- expect(@b.hash_tree(limit_depth: 1)).to eq({@b => {}})
603
+ it 'no limit from root' do
604
+ expect(@a.hash_tree.merge(@a2.hash_tree)).to eq(@full_tree.slice(@a, @a2))
572
605
  end
573
- it "limit_depth 2" do
574
- expect(@b.hash_tree(limit_depth: 2)).to eq({@b => {@c1 => {}, @c2 => {}}})
606
+ end
607
+
608
+ context '.hash_tree from relations' do
609
+ it 'limit_depth 2 from chained activerecord association subroots' do
610
+ expect(@a.children.hash_tree(limit_depth: 2)).to eq(@three_tree[@a])
575
611
  end
576
- it "limit_depth 3" do
577
- expect(@b.hash_tree(limit_depth: 3)).to eq({@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}})
612
+ it 'no limit from chained activerecord association subroots' do
613
+ expect(@a.children.hash_tree).to eq(@full_tree[@a])
578
614
  end
579
- it "no limit holdum from subsubroot" do
580
- expect(@c1.hash_tree).to eq({@c1 => {@d1 => {}}})
615
+ it 'limit_depth 3 from b.parent' do
616
+ expect(@b.parent.hash_tree(limit_depth: 3)).to eq(@three_tree.slice(@a))
581
617
  end
582
- it "no limit holdum from subroot" do
583
- expect(@b.hash_tree).to eq({@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}})
618
+ it 'no limit_depth from b.parent' do
619
+ expect(@b.parent.hash_tree).to eq(@full_tree.slice(@a))
584
620
  end
585
- it "no limit holdum from root" do
586
- expect(@a.hash_tree).to eq(@full_tree)
621
+ it 'no limit_depth from c.parent' do
622
+ expect(@c1.parent.hash_tree).to eq(@full_tree[@a].slice(@b))
587
623
  end
588
624
  end
589
625
  end
@@ -600,7 +636,7 @@ shared_examples_for Tag do
600
636
 
601
637
  describe 'DOT rendering' do
602
638
  it 'should render for an empty scope' do
603
- expect(tag_class.to_dot_digraph(tag_class.where("0=1"))).to eq("digraph G {\n}\n")
639
+ expect(tag_class.to_dot_digraph(tag_class.where('0=1'))).to eq("digraph G {\n}\n")
604
640
  end
605
641
  it 'should render for an empty scope' do
606
642
  tag_class.find_or_create_by_path(%w(a b1 c1))
data/tests.sh CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/bin/sh -ex
2
2
 
3
- for RMI in 2.1.5 #jruby-1.6.13 :P
3
+ for RMI in 2.2.3 #jruby-1.6.13 :P
4
4
  do
5
5
  rbenv local $RMI
6
6
  for DB in postgresql mysql sqlite
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.alpha
4
+ version: 6.0.0.gamma
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-05 00:00:00.000000000 Z
11
+ date: 2015-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -166,6 +166,7 @@ files:
166
166
  - lib/closure_tree/finders.rb
167
167
  - lib/closure_tree/has_closure_tree.rb
168
168
  - lib/closure_tree/hash_tree.rb
169
+ - lib/closure_tree/hash_tree_support.rb
169
170
  - lib/closure_tree/hierarchy_maintenance.rb
170
171
  - lib/closure_tree/model.rb
171
172
  - lib/closure_tree/numeric_deterministic_ordering.rb