closure_tree 6.0.0.alpha → 6.0.0.gamma

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: 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