closure_tree 7.4.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/README.md +123 -38
  4. data/bin/rails +15 -0
  5. data/bin/rake +7 -7
  6. data/closure_tree.gemspec +20 -19
  7. data/lib/closure_tree/active_record_support.rb +6 -14
  8. data/lib/closure_tree/adapter_support.rb +11 -0
  9. data/lib/closure_tree/arel_helpers.rb +83 -0
  10. data/lib/closure_tree/configuration.rb +2 -0
  11. data/lib/closure_tree/deterministic_ordering.rb +2 -0
  12. data/lib/closure_tree/digraphs.rb +7 -5
  13. data/lib/closure_tree/finders.rb +104 -55
  14. data/lib/closure_tree/has_closure_tree.rb +5 -2
  15. data/lib/closure_tree/has_closure_tree_root.rb +12 -17
  16. data/lib/closure_tree/hash_tree.rb +3 -2
  17. data/lib/closure_tree/hash_tree_support.rb +38 -13
  18. data/lib/closure_tree/hierarchy_maintenance.rb +20 -30
  19. data/lib/closure_tree/model.rb +31 -31
  20. data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -60
  21. data/lib/closure_tree/numeric_order_support.rb +20 -18
  22. data/lib/closure_tree/support.rb +31 -38
  23. data/lib/closure_tree/support_attributes.rb +31 -5
  24. data/lib/closure_tree/support_flags.rb +2 -12
  25. data/lib/closure_tree/test/matcher.rb +10 -12
  26. data/lib/closure_tree/version.rb +3 -1
  27. data/lib/closure_tree.rb +22 -2
  28. data/lib/generators/closure_tree/config_generator.rb +3 -1
  29. data/lib/generators/closure_tree/migration_generator.rb +7 -8
  30. data/lib/generators/closure_tree/templates/config.rb +2 -0
  31. metadata +25 -86
  32. data/.github/workflows/ci.yml +0 -98
  33. data/.gitignore +0 -17
  34. data/.rspec +0 -1
  35. data/.yardopts +0 -3
  36. data/Appraisals +0 -105
  37. data/Gemfile +0 -7
  38. data/Rakefile +0 -37
  39. data/_config.yml +0 -1
  40. data/bin/appraisal +0 -29
  41. data/bin/rspec +0 -29
  42. data/mktree.rb +0 -38
  43. data/tests.sh +0 -11
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/concern'
2
4
 
3
5
  # This module is only included if the order column is an integer.
@@ -10,15 +12,10 @@ module ClosureTree
10
12
  end
11
13
 
12
14
  def _ct_reorder_prior_siblings_if_parent_changed
13
- as_5_1 = ActiveSupport.version >= Gem::Version.new('5.1.0')
14
- change_method = as_5_1 ? :saved_change_to_attribute? : :attribute_changed?
15
-
16
- if public_send(change_method, _ct.parent_column_name) && !@was_new_record
17
- attribute_method = as_5_1 ? :attribute_before_last_save : :attribute_was
15
+ return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
18
16
 
19
- was_parent_id = public_send(attribute_method, _ct.parent_column_name)
20
- _ct.reorder_with_parent_id(was_parent_id)
21
- end
17
+ was_parent_id = attribute_before_last_save(_ct.parent_column_name)
18
+ _ct.reorder_with_parent_id(was_parent_id)
22
19
  end
23
20
 
24
21
  def _ct_reorder_siblings(minimum_sort_order_value = nil)
@@ -32,60 +29,94 @@ module ClosureTree
32
29
 
33
30
  def self_and_descendants_preordered
34
31
  # TODO: raise NotImplementedError if sort_order is not numeric and not null?
35
- join_sql = <<-SQL
36
- JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
37
- ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
38
- JOIN #{_ct.quoted_table_name} anc
39
- ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
40
- JOIN #{_ct.quoted_hierarchy_table_name} depths
41
- ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.#{_ct.quoted_id_column_name}
42
- SQL
32
+ hierarchy_table = self.class.hierarchy_class.arel_table
33
+ model_table = self.class.arel_table
34
+
35
+ # Create aliased tables for the joins
36
+ anc_hier = _ct.aliased_table(hierarchy_table, 'anc_hier')
37
+ anc = _ct.aliased_table(model_table, 'anc')
38
+ depths = _ct.aliased_table(hierarchy_table, 'depths')
39
+
40
+ # Build the join conditions using Arel
41
+ join_anc_hier = hierarchy_table
42
+ .join(anc_hier)
43
+ .on(anc_hier[:descendant_id].eq(hierarchy_table[:descendant_id]))
44
+
45
+ join_anc = join_anc_hier
46
+ .join(anc)
47
+ .on(anc[self.class.primary_key].eq(anc_hier[:ancestor_id]))
48
+
49
+ join_depths = join_anc
50
+ .join(depths)
51
+ .on(
52
+ depths[:ancestor_id].eq(id)
53
+ .and(depths[:descendant_id].eq(anc[self.class.primary_key]))
54
+ )
43
55
 
44
56
  self_and_descendants
45
- .joins(join_sql)
57
+ .joins(join_depths.join_sources)
46
58
  .group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
47
59
  .reorder(self.class._ct_sum_order_by(self))
48
60
  end
49
61
 
50
- module ClassMethods
51
-
62
+ class_methods do
52
63
  # If node is nil, order the whole tree.
53
64
  def _ct_sum_order_by(node = nil)
54
- stats_sql = <<-SQL.squish
55
- SELECT
56
- count(*) as total_descendants,
57
- max(generations) as max_depth
58
- FROM #{_ct.quoted_hierarchy_table_name}
59
- SQL
60
- stats_sql += " WHERE ancestor_id = #{_ct.quote(node.id)}" if node
61
- h = _ct.connection.select_one(stats_sql)
65
+ # Build the stats query using Arel
66
+ hierarchy_table = hierarchy_class.arel_table
67
+
68
+ query = hierarchy_table
69
+ .project(
70
+ Arel.star.count.as('total_descendants'),
71
+ hierarchy_table[:generations].maximum.as('max_depth')
72
+ )
73
+
74
+ query = query.where(hierarchy_table[:ancestor_id].eq(node.id)) if node
75
+
76
+ h = _ct.connection.select_one(query.to_sql)
62
77
 
63
78
  depth_column = node ? 'depths.generations' : 'depths.max_depth'
64
79
 
65
- node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
66
- "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
80
+ node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " \
81
+ "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
67
82
 
68
83
  # We want the NULLs to be first in case we are not ordering roots and they have NULL order.
69
84
  Arel.sql("SUM(#{node_score}) IS NULL DESC, SUM(#{node_score})")
70
85
  end
71
86
 
72
87
  def roots_and_descendants_preordered
73
- if _ct.dont_order_roots
74
- raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
75
- end
76
-
77
- join_sql = <<-SQL.squish
78
- JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
79
- ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
80
- JOIN #{_ct.quoted_table_name} anc
81
- ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
82
- JOIN (
83
- SELECT descendant_id, max(generations) AS max_depth
84
- FROM #{_ct.quoted_hierarchy_table_name}
85
- GROUP BY descendant_id
86
- ) #{ _ct.t_alias_keyword } depths ON depths.descendant_id = anc.#{_ct.quoted_id_column_name}
87
- SQL
88
- joins(join_sql)
88
+ raise ClosureTree::RootOrderingDisabledError, 'Root ordering is disabled on this model' if _ct.dont_order_roots
89
+
90
+ hierarchy_table = hierarchy_class.arel_table
91
+ model_table = arel_table
92
+
93
+ # Create aliased tables
94
+ anc_hier = _ct.aliased_table(hierarchy_table, 'anc_hier')
95
+ anc = _ct.aliased_table(model_table, 'anc')
96
+
97
+ # Build the subquery for depths
98
+ depths_subquery = hierarchy_table
99
+ .project(
100
+ hierarchy_table[:descendant_id],
101
+ hierarchy_table[:generations].maximum.as('max_depth')
102
+ )
103
+ .group(hierarchy_table[:descendant_id])
104
+ .as('depths')
105
+
106
+ # Build the join conditions
107
+ join_anc_hier = model_table
108
+ .join(anc_hier)
109
+ .on(anc_hier[:descendant_id].eq(model_table[primary_key]))
110
+
111
+ join_anc = join_anc_hier
112
+ .join(anc)
113
+ .on(anc[primary_key].eq(anc_hier[:ancestor_id]))
114
+
115
+ join_depths = join_anc
116
+ .join(depths_subquery)
117
+ .on(depths_subquery[:descendant_id].eq(anc[primary_key]))
118
+
119
+ joins(join_depths.join_sources)
89
120
  .group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
90
121
  .reorder(_ct_sum_order_by)
91
122
  end
@@ -116,10 +147,10 @@ module ClosureTree
116
147
  end
117
148
 
118
149
  def add_sibling(sibling, add_after = true)
119
- fail "can't add self as sibling" if self == sibling
150
+ raise "can't add self as sibling" if self == sibling
120
151
 
121
152
  if _ct.dont_order_roots && parent.nil?
122
- raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
153
+ raise ClosureTree::RootOrderingDisabledError, 'Root ordering is disabled on this model'
123
154
  end
124
155
 
125
156
  # Make sure self isn't dirty, because we're going to call reload:
@@ -127,31 +158,30 @@ module ClosureTree
127
158
 
128
159
  _ct.with_advisory_lock do
129
160
  prior_sibling_parent = sibling.parent
130
- reorder_from_value = if prior_sibling_parent == self.parent
131
- [self.order_value, sibling.order_value].compact.min
132
- else
133
- self.order_value
134
- end
135
-
136
- sibling.order_value = self.order_value
137
- sibling.parent = self.parent
161
+ reorder_from_value = if prior_sibling_parent == parent
162
+ [order_value, sibling.order_value].compact.min
163
+ else
164
+ order_value
165
+ end
166
+
167
+ sibling.order_value = order_value
168
+ sibling.parent = parent
138
169
  sibling._ct_skip_sort_order_maintenance!
139
170
  sibling.save # may be a no-op
140
171
 
141
172
  _ct_reorder_siblings(reorder_from_value)
142
173
 
143
174
  # The sort order should be correct now except for self and sibling, which may need to flip:
144
- sibling_is_after = self.reload.order_value < sibling.reload.order_value
175
+ sibling_is_after = reload.order_value < sibling.reload.order_value
145
176
  if add_after != sibling_is_after
146
177
  # We need to flip the sort orders:
147
- self_ov, sib_ov = self.order_value, sibling.order_value
178
+ self_ov = order_value
179
+ sib_ov = sibling.order_value
148
180
  update_order_value(sib_ov)
149
181
  sibling.update_order_value(self_ov)
150
182
  end
151
183
 
152
- if prior_sibling_parent != self.parent
153
- prior_sibling_parent.try(:_ct_reorder_children)
154
- end
184
+ prior_sibling_parent.try(:_ct_reorder_children) if prior_sibling_parent != parent
155
185
  sibling
156
186
  end
157
187
  end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
4
  module NumericOrderSupport
3
-
4
5
  def self.adapter_for_connection(connection)
5
- das = WithAdvisoryLock::DatabaseAdapterSupport.new(connection)
6
- if das.postgresql?
6
+ adapter_name = connection.adapter_name.downcase
7
+ if adapter_name.include?('postgresql') || adapter_name.include?('postgis')
7
8
  ::ClosureTree::NumericOrderSupport::PostgreSQLAdapter
8
- elsif das.mysql?
9
+ elsif adapter_name.include?('mysql') || adapter_name.include?('trilogy')
9
10
  ::ClosureTree::NumericOrderSupport::MysqlAdapter
10
11
  else
11
12
  ::ClosureTree::NumericOrderSupport::GenericAdapter
@@ -15,11 +16,12 @@ module ClosureTree
15
16
  module MysqlAdapter
16
17
  def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
17
18
  return if parent_id.nil? && dont_order_roots
19
+
18
20
  min_where = if minimum_sort_order_value
19
- "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
20
- else
21
- ""
22
- end
21
+ "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
22
+ else
23
+ ''
24
+ end
23
25
  connection.execute 'SET @i = 0'
24
26
  connection.execute <<-SQL.squish
25
27
  UPDATE #{quoted_table_name}
@@ -33,11 +35,12 @@ module ClosureTree
33
35
  module PostgreSQLAdapter
34
36
  def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
35
37
  return if parent_id.nil? && dont_order_roots
38
+
36
39
  min_where = if minimum_sort_order_value
37
- "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
38
- else
39
- ""
40
- end
40
+ "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
41
+ else
42
+ ''
43
+ end
41
44
  connection.execute <<-SQL.squish
42
45
  UPDATE #{quoted_table_name}
43
46
  SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
@@ -59,12 +62,11 @@ module ClosureTree
59
62
  module GenericAdapter
60
63
  def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
61
64
  return if parent_id.nil? && dont_order_roots
62
- scope = model_class.
63
- where(parent_column_sym => parent_id).
64
- order(nulls_last_order_by)
65
- if minimum_sort_order_value
66
- scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}")
67
- end
65
+
66
+ scope = model_class
67
+ .where(parent_column_sym => parent_id)
68
+ .order(nulls_last_order_by)
69
+ scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}") if minimum_sort_order_value
68
70
  scope.each_with_index do |ea, idx|
69
71
  ea.update_order_value(idx + minimum_sort_order_value.to_i)
70
72
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'closure_tree/support_flags'
2
4
  require 'closure_tree/support_attributes'
3
5
  require 'closure_tree/numeric_order_support'
4
6
  require 'closure_tree/active_record_support'
5
7
  require 'closure_tree/hash_tree_support'
6
- require 'with_advisory_lock'
8
+ require 'closure_tree/arel_helpers'
7
9
 
8
10
  # This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
9
11
  module ClosureTree
@@ -12,42 +14,40 @@ module ClosureTree
12
14
  include ClosureTree::SupportAttributes
13
15
  include ClosureTree::ActiveRecordSupport
14
16
  include ClosureTree::HashTreeSupport
17
+ include ClosureTree::ArelHelpers
15
18
 
16
- attr_reader :model_class
17
- attr_reader :options
19
+ attr_reader :model_class, :options
18
20
 
19
21
  def initialize(model_class, options)
20
22
  @model_class = model_class
23
+
21
24
  @options = {
22
- :parent_column_name => 'parent_id',
23
- :dependent => :nullify, # or :destroy or :delete_all -- see the README
24
- :name_column => 'name',
25
- :with_advisory_lock => true,
26
- :numeric_order => false
25
+ parent_column_name: 'parent_id',
26
+ dependent: :nullify, # or :destroy or :delete_all -- see the README
27
+ name_column: 'name',
28
+ with_advisory_lock: true, # This will be overridden by adapter support
29
+ numeric_order: false
27
30
  }.merge(options)
28
31
  raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
29
- if order_is_numeric?
30
- extend NumericOrderSupport.adapter_for_connection(connection)
31
- end
32
+
33
+ return unless order_is_numeric?
34
+
35
+ extend NumericOrderSupport.adapter_for_connection(connection)
32
36
  end
33
37
 
34
38
  def hierarchy_class_for_model
35
- parent_class = ActiveSupport::VERSION::MAJOR >= 6 ? model_class.module_parent : model_class.parent
39
+ parent_class = model_class.module_parent
36
40
  hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass))
37
- use_attr_accessible = use_attr_accessible?
38
- include_forbidden_attributes_protection = include_forbidden_attributes_protection?
39
41
  model_class_name = model_class.to_s
40
42
  hierarchy_class.class_eval do
41
- include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
42
43
  belongs_to :ancestor, class_name: model_class_name
43
44
  belongs_to :descendant, class_name: model_class_name
44
- attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
45
45
  def ==(other)
46
46
  self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
47
47
  end
48
48
  alias :eql? :==
49
49
  def hash
50
- ancestor_id.hash << 31 ^ descendant_id.hash
50
+ (ancestor_id.hash << 31) ^ descendant_id.hash
51
51
  end
52
52
  end
53
53
  hierarchy_class.table_name = hierarchy_table_name
@@ -59,36 +59,30 @@ module ClosureTree
59
59
  # because they may have overridden the table name, which is what we want to be consistent with
60
60
  # in order for the schema to make sense.
61
61
  tablename = options[:hierarchy_table_name] ||
62
- remove_prefix_and_suffix(table_name).singularize + "_hierarchies"
62
+ "#{remove_prefix_and_suffix(table_name, model_class).singularize}_hierarchies"
63
63
 
64
- ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
64
+ [model_class.table_name_prefix, tablename, model_class.table_name_suffix].join
65
65
  end
66
66
 
67
67
  def with_order_option(opts)
68
- if order_option?
69
- opts[:order] = [opts[:order], order_by].compact.join(",")
70
- end
68
+ opts[:order] = [opts[:order], order_by].compact.join(',') if order_option?
71
69
  opts
72
70
  end
73
71
 
74
72
  def scope_with_order(scope, additional_order_by = nil)
75
73
  if order_option?
76
- scope.order(*([additional_order_by, order_by].compact))
74
+ scope.order(*[additional_order_by, order_by].compact)
77
75
  else
78
76
  additional_order_by ? scope.order(additional_order_by) : scope
79
77
  end
80
78
  end
81
79
 
82
- def belongs_to_with_optional_option(opts)
83
- ActiveRecord::VERSION::MAJOR < 5 ? opts.except(:optional) : opts
84
- end
85
-
86
80
  # lambda-ize the order, but don't apply the default order_option
87
81
  def has_many_order_without_option(order_by_opt)
88
- [lambda { order(order_by_opt.call) }]
82
+ [-> { order(order_by_opt.call) }]
89
83
  end
90
84
 
91
- def has_many_order_with_option(order_by_opt=nil)
85
+ def has_many_order_with_option(order_by_opt = nil)
92
86
  order_options = [order_by_opt, order_by].compact
93
87
  [lambda {
94
88
  order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o }
@@ -109,9 +103,9 @@ module ClosureTree
109
103
  end
110
104
 
111
105
  def with_advisory_lock(&block)
112
- if options[:with_advisory_lock]
106
+ if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
113
107
  model_class.with_advisory_lock(advisory_lock_name) do
114
- transaction { yield }
108
+ transaction(&block)
115
109
  end
116
110
  else
117
111
  yield
@@ -123,7 +117,7 @@ module ClosureTree
123
117
  unless path.first.is_a?(Hash)
124
118
  if subclass? && has_inheritance_column?
125
119
  attributes = attributes.with_indifferent_access
126
- attributes[inheritance_column] ||= self.sti_name
120
+ attributes[inheritance_column] ||= sti_name
127
121
  end
128
122
  path = path.map { |ea| attributes.merge(name_column => ea) }
129
123
  end
@@ -131,11 +125,9 @@ module ClosureTree
131
125
  end
132
126
 
133
127
  def scoped_attributes(scope, attributes, target_table = model_class.table_name)
134
- table_prefixed_attributes = Hash[
135
- attributes.map do |column_name, column_value|
136
- ["#{target_table}.#{column_name}", column_value]
137
- end
138
- ]
128
+ table_prefixed_attributes = attributes.transform_keys do |column_name|
129
+ "#{target_table}.#{column_name}"
130
+ end
139
131
  scope.where(table_prefixed_attributes)
140
132
  end
141
133
 
@@ -150,6 +142,7 @@ module ClosureTree
150
142
  path.in_groups(max_join_tables, false).each do |subpath|
151
143
  child = model_class.find_by_path(subpath, attributes, next_parent_id)
152
144
  return nil if child.nil?
145
+
153
146
  next_parent_id = child._ct_id
154
147
  end
155
148
  child
@@ -168,7 +161,7 @@ module ClosureTree
168
161
  end
169
162
 
170
163
  def create!(model_class, attributes)
171
- create(model_class, attributes).tap { |ea| ea.save! }
164
+ create(model_class, attributes).tap(&:save!)
172
165
  end
173
166
  end
174
167
  end
@@ -1,11 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
4
+ require 'zlib'
5
+
2
6
  module ClosureTree
3
7
  module SupportAttributes
4
8
  extend Forwardable
5
9
  def_delegators :model_class, :connection, :transaction, :table_name, :base_class, :inheritance_column, :column_names
6
10
 
7
11
  def advisory_lock_name
8
- Digest::SHA1.hexdigest("ClosureTree::#{base_class.name}")[0..32]
12
+ # Allow customization via options or instance method
13
+ if options[:advisory_lock_name]
14
+ case options[:advisory_lock_name]
15
+ when Proc
16
+ # Allow dynamic generation via proc
17
+ options[:advisory_lock_name].call(base_class)
18
+ when Symbol
19
+ # Allow delegation to a model method
20
+ if model_class.respond_to?(options[:advisory_lock_name])
21
+ model_class.send(options[:advisory_lock_name])
22
+ else
23
+ raise ArgumentError, "Model #{model_class} does not respond to #{options[:advisory_lock_name]}"
24
+ end
25
+ else
26
+ # Use static string value
27
+ options[:advisory_lock_name].to_s
28
+ end
29
+ else
30
+ # Default: Use CRC32 for a shorter, consistent hash
31
+ # This gives us 8 hex characters which is plenty for uniqueness
32
+ # and leaves room for prefixes
33
+ "ct_#{Zlib.crc32(base_class.name.to_s).to_s(16)}"
34
+ end
9
35
  end
10
36
 
11
37
  def quoted_table_name
@@ -17,7 +43,7 @@ module ClosureTree
17
43
  end
18
44
 
19
45
  def hierarchy_class_name
20
- options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
46
+ options[:hierarchy_class_name] || "#{model_class}Hierarchy"
21
47
  end
22
48
 
23
49
  def primary_key_column
@@ -84,7 +110,7 @@ module ClosureTree
84
110
  end
85
111
 
86
112
  def order_by_order(reverse = false)
87
- desc = !!(order_by.to_s =~ /DESC\z/)
113
+ desc = !(order_by.to_s =~ /DESC\z/).nil?
88
114
  desc = !desc if reverse
89
115
  desc ? 'DESC' : 'ASC'
90
116
  end
@@ -111,13 +137,13 @@ module ClosureTree
111
137
 
112
138
  def quoted_order_column(include_table_name = true)
113
139
  require_order_column
114
- prefix = include_table_name ? "#{quoted_table_name}." : ""
140
+ prefix = include_table_name ? "#{quoted_table_name}." : ''
115
141
  "#{prefix}#{connection.quote_column_name(order_column)}"
116
142
  end
117
143
 
118
144
  # table_name alias keyword , like "AS". When used on table name alias, Oracle Database don't support used 'AS'
119
145
  def t_alias_keyword
120
- (ActiveRecord::Base.connection.adapter_name.to_sym == :OracleEnhanced) ? "" : "AS"
146
+ ActiveRecord::Base.connection.adapter_name.to_sym == :OracleEnhanced ? '' : 'AS'
121
147
  end
122
148
  end
123
149
  end
@@ -1,17 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
4
  module SupportFlags
3
-
4
- def use_attr_accessible?
5
- defined?(ActiveModel::MassAssignmentSecurity) &&
6
- model_class.respond_to?(:accessible_attributes) &&
7
- ! model_class.accessible_attributes.empty?
8
- end
9
-
10
- def include_forbidden_attributes_protection?
11
- defined?(ActiveModel::ForbiddenAttributesProtection) &&
12
- model_class.ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
13
- end
14
-
15
5
  def order_option?
16
6
  order_by.present?
17
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'closure_tree'
2
4
 
3
5
  module ClosureTree
@@ -19,13 +21,13 @@ module ClosureTree
19
21
  end
20
22
 
21
23
  # Checking if hierarchy table exists (common error)
22
- unless @subject.hierarchy_class.table_exists?
24
+ unless @subject.hierarchy_class.table_exists?
23
25
  @message = "expected #{@subject.name}'s hierarchy table '#{@subject.hierarchy_class.table_name}' to exist"
24
26
  return false
25
27
  end
26
28
 
27
29
  if @ordered
28
- unless @subject._ct.options.include?(:order)
30
+ unless @subject._ct.options.include?(:order)
29
31
  @message = "expected #{@subject.name} to be an ordered closure tree"
30
32
  return false
31
33
  end
@@ -36,13 +38,13 @@ module ClosureTree
36
38
  end
37
39
 
38
40
  if @with_advisory_lock && !@subject._ct.options[:with_advisory_lock]
39
- @message = "expected #{@subject.name} to have advisory lock"
40
- return false
41
+ @message = "expected #{@subject.name} to have advisory lock"
42
+ return false
41
43
  end
42
44
 
43
45
  if @without_advisory_lock && @subject._ct.options[:with_advisory_lock]
44
- @message = "expected #{@subject.name} to not have advisory lock"
45
- return false
46
+ @message = "expected #{@subject.name} to not have advisory lock"
47
+ return false
46
48
  end
47
49
 
48
50
  return true
@@ -70,13 +72,13 @@ module ClosureTree
70
72
  @message || "expected #{@subject.name} to #{description}"
71
73
  end
72
74
 
73
- alias_method :failure_message_for_should, :failure_message
75
+ alias failure_message_for_should failure_message
74
76
 
75
77
  def failure_message_when_negated
76
78
  "expected #{@subject.name} not be a closure tree, but it is."
77
79
  end
78
80
 
79
- alias_method :failure_message_for_should_not, :failure_message_when_negated
81
+ alias failure_message_for_should_not failure_message_when_negated
80
82
 
81
83
  def description
82
84
  "be a#{@ordered} closure tree#{@with_advisory_lock}"
@@ -85,7 +87,3 @@ module ClosureTree
85
87
  end
86
88
  end
87
89
  end
88
-
89
- RSpec.configure do |c|
90
- c.include ClosureTree::Test::Matcher, type: :model
91
- end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
- VERSION = Gem::Version.new('7.4.0')
4
+ VERSION = Gem::Version.new('9.0.0')
3
5
  end
data/lib/closure_tree.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
5
  module ClosureTree
@@ -14,6 +16,7 @@ module ClosureTree
14
16
  autoload :DeterministicOrdering
15
17
  autoload :NumericDeterministicOrdering
16
18
  autoload :Configuration
19
+ autoload :AdapterSupport
17
20
 
18
21
  def self.configure
19
22
  yield configuration
@@ -25,6 +28,23 @@ module ClosureTree
25
28
  end
26
29
 
27
30
  ActiveSupport.on_load :active_record do
28
- ActiveRecord::Base.send :extend, ClosureTree::HasClosureTree
29
- ActiveRecord::Base.send :extend, ClosureTree::HasClosureTreeRoot
31
+ ActiveRecord::Base.extend ClosureTree::HasClosureTree
32
+ ActiveRecord::Base.extend ClosureTree::HasClosureTreeRoot
33
+ end
34
+
35
+ # Adapter injection for different database types
36
+ ActiveSupport.on_load :active_record_postgresqladapter do
37
+ prepend ClosureTree::AdapterSupport
38
+ end
39
+
40
+ ActiveSupport.on_load :active_record_mysql2adapter do
41
+ prepend ClosureTree::AdapterSupport
42
+ end
43
+
44
+ ActiveSupport.on_load :active_record_trilogyadapter do
45
+ prepend ClosureTree::AdapterSupport
46
+ end
47
+
48
+ ActiveSupport.on_load :active_record_sqlite3adapter do
49
+ prepend ClosureTree::AdapterSupport
30
50
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
4
  module Generators # :nodoc:
3
5
  class ConfigGenerator < Rails::Generators::Base # :nodoc:
4
- source_root File.expand_path('../templates', __FILE__)
6
+ source_root File.expand_path('templates', __dir__)
5
7
  desc 'Install closure tree config.'
6
8
 
7
9
  def config