closure_tree 9.3.0 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7af281d62397e19768240a93ab284f2ceb9b261f5c4c3d2ac7d288300212b6e
4
- data.tar.gz: ef0cf220eb02fcfafb17034265ee6d3ceb514c13898bf8c71eab1915f2a156c6
3
+ metadata.gz: 8e3f99ea82d4fd195ea034a9f6fcfdd0ce1ec69e74fa2b62241b68eb74070676
4
+ data.tar.gz: b59d11e3fe4e6236c457776b5b5e7cc7133c77759789fbe816b8185db44855e8
5
5
  SHA512:
6
- metadata.gz: 0a04211a36264ac886ecb8b7260c23e767de8364725fdc39dbfbe508116a5201ec8a54f4edf1865b3b91c8ee0fca4d2152d7f1091fca056501995723e4edbce6
7
- data.tar.gz: d0ac8d6a477e357ff6582079443b6221100d868e1234d7aaaa4a815108b0db2e024e84086e2ace0c3dc3d1b85b8cd40701737f4bc292dabf479ca6263bccd994
6
+ metadata.gz: 2a1d3b69d8397b4f5d8b8ad7e81df74e9577c77dcbd10851b5fdc00a17f2b0b5caee0446fe71b3aaec28d0b79c7a4b8d97a89edcd3ac4599bbbeb4b0d6ddeb21
7
+ data.tar.gz: 88d92d332638b6c38e6575ae14ebfd8ec060c2ef0137f7335b72bda4f84537d7db43032623ac5138d31424753cbf83208e5f7f4de0ce1953fcd03a0d8314fbeb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [9.6.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.5.0...closure_tree/v9.6.0) (2026-02-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * raise error when advisory lock cannot be acquired within configured timeout ([#480](https://github.com/ClosureTree/closure_tree/issues/480)) ([c030385](https://github.com/ClosureTree/closure_tree/commit/c030385297a9d3042d43354676a794b1c5757d2a))
9
+ * sibling reordering when node changes parent or scope ([#484](https://github.com/ClosureTree/closure_tree/issues/484)) ([254ba36](https://github.com/ClosureTree/closure_tree/commit/254ba360638bf21717d214b7fd328db8ffa167e0))
10
+
11
+ ## [9.5.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree-v9.3.0...closure_tree/v9.5.0) (2026-01-21)
12
+
13
+
14
+ ### Features
15
+
16
+ * Add `dependent: :adopt` option for `has_closure_tree` ([#471](https://github.com/ClosureTree/closure_tree/issues/471)) ([d47d7c9](https://github.com/ClosureTree/closure_tree/commit/d47d7c93b59327112a68a7f8454a278877b6ba77))
17
+ * add PostgreSQL schema-qualified table name support ([#462](https://github.com/ClosureTree/closure_tree/issues/462)) ([5f9006c](https://github.com/ClosureTree/closure_tree/commit/5f9006cece95a76f665cb50c2615317e3fa48586))
18
+ * Add runtime advisory lock name customization and multi-database documentation ([#454](https://github.com/ClosureTree/closure_tree/issues/454)) ([d6ffd73](https://github.com/ClosureTree/closure_tree/commit/d6ffd7381e25a28f7a4742bfa2d9c893f0115395))
19
+ * migrate from ActiveSupport::Autoload to Zeitwerk ([#457](https://github.com/ClosureTree/closure_tree/issues/457)) ([d18e80c](https://github.com/ClosureTree/closure_tree/commit/d18e80cdbd4f3510377363bc7b5166f0cc1b0a6f))
20
+ * rewrite with clean api ([#451](https://github.com/ClosureTree/closure_tree/issues/451)) ([f56f2e1](https://github.com/ClosureTree/closure_tree/commit/f56f2e1a3490bb8a099cea8f80b676945fce1c2e))
21
+ * use with_advisory_lock that support rails 8.2 ([#479](https://github.com/ClosureTree/closure_tree/issues/479)) ([bca6231](https://github.com/ClosureTree/closure_tree/commit/bca623168d3255f57e186a3bdcd39e00d38e2a7c))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * add implicit_order_column for Rails 8.1+ compatibility ([#464](https://github.com/ClosureTree/closure_tree/issues/464)) ([f384303](https://github.com/ClosureTree/closure_tree/commit/f38430334a79ea15d236f9212118dcb5e4530746))
27
+ * configure release-please to recognize v8.0.0 release ([#455](https://github.com/ClosureTree/closure_tree/issues/455)) ([fc34f21](https://github.com/ClosureTree/closure_tree/commit/fc34f2148570afd83608b07a3f5282e5fd475783))
28
+ * eager loading of hierarchies by defining the primary key ([#465](https://github.com/ClosureTree/closure_tree/issues/465)) ([8c7e490](https://github.com/ClosureTree/closure_tree/commit/8c7e490ff89239e44aee71577288bff0e177a8d6))
29
+ * hierarchy class inheritance to avoid STI validations ([#392](https://github.com/ClosureTree/closure_tree/issues/392)) ([#472](https://github.com/ClosureTree/closure_tree/issues/472)) ([73b07a6](https://github.com/ClosureTree/closure_tree/commit/73b07a6cfeec226af16f51ce7c9c6c1d6d921b3d))
30
+ * improve api usage ([#475](https://github.com/ClosureTree/closure_tree/issues/475)) ([cde4d29](https://github.com/ClosureTree/closure_tree/commit/cde4d292236b4267ac4c0be2e2b21092a8b6298c))
31
+ * nil scope values being excluded from scope filtering ([#476](https://github.com/ClosureTree/closure_tree/issues/476)) ([f45880e](https://github.com/ClosureTree/closure_tree/commit/f45880e9765bfa3c1f171b7386bf5a5584208ad7))
32
+ * restore proper deprecation for database_less configuration ([#459](https://github.com/ClosureTree/closure_tree/issues/459)) ([de8b402](https://github.com/ClosureTree/closure_tree/commit/de8b40233d3de5243afce7ec9de9ad26c2eee181))
33
+
3
34
  ## [9.3.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.2.0...closure_tree/v9.3.0) (2025-11-19)
4
35
 
5
36
 
data/README.md CHANGED
@@ -314,6 +314,7 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
314
314
  * ```:hierarchy_table_name``` to override the hierarchy table name. This defaults to the singular name of the model + "_hierarchies", like ```tag_hierarchies```.
315
315
  * ```:dependent``` determines what happens when a node is destroyed. Defaults to ```nullify```.
316
316
  * ```:nullify``` will simply set the parent column to null. Each child node will be considered a "root" node. This is the default.
317
+ * ```:adopt``` will move children to their grandparent (parent's parent). If there is no grandparent, children become root nodes. This is useful for maintaining tree structure when removing intermediate nodes.
317
318
  * ```:delete_all``` will delete all descendant nodes (which circumvents the destroy hooks)
318
319
  * ```:destroy``` will destroy all descendant nodes (which runs the destroy hooks on each child node)
319
320
  * ```nil``` does nothing with descendant nodes
@@ -321,6 +322,7 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
321
322
  * ```:order``` used to set up [deterministic ordering](#deterministic-ordering)
322
323
  * ```:scope``` restricts root nodes and sibling ordering to specific columns. Can be a single symbol or an array of symbols. Example: ```scope: :user_id``` or ```scope: [:user_id, :group_id]```. This ensures that root nodes and siblings are scoped correctly when reordering. See [Ordering Roots](#ordering-roots) for more details.
323
324
  * ```:touch``` delegates to the `belongs_to` annotation for the parent, so `touch`ing cascades to all children (the performance of this for deep trees isn't currently optimal).
325
+ * ```:advisory_lock_timeout_seconds``` When set, the advisory lock will raise ```WithAdvisoryLock::FailedToAcquireLock``` if the lock cannot be acquired within the timeout period. This helps callers handle timeout scenarios (e.g. retry or fail fast). If the option is not specified, the lock is waited for indefinitely until it is acquired. See [Lock wait timeouts](https://github.com/ClosureTree/with_advisory_lock?tab=readme-ov-file#lock-wait-timeouts) in the with_advisory_lock gem for details.
324
326
 
325
327
  ## Accessing Data
326
328
 
data/closure_tree.gemspec CHANGED
@@ -26,11 +26,11 @@ Gem::Specification.new do |gem|
26
26
  gem.required_ruby_version = '>= 3.3.0'
27
27
 
28
28
  gem.add_dependency 'activerecord', '>= 7.2.0'
29
- gem.add_dependency 'with_advisory_lock', '>= 7.0.0'
29
+ gem.add_dependency 'with_advisory_lock', '>= 7.5.0'
30
30
  gem.add_dependency 'zeitwerk', '~> 2.7'
31
31
 
32
32
  gem.add_development_dependency 'database_cleaner'
33
- gem.add_development_dependency 'minitest'
33
+ gem.add_development_dependency 'minitest', '~> 5.0'
34
34
  gem.add_development_dependency 'minitest-reporters'
35
35
  gem.add_development_dependency 'parallel'
36
36
  gem.add_development_dependency 'simplecov'
@@ -79,5 +79,13 @@ module ClosureTree
79
79
 
80
80
  delete_manager
81
81
  end
82
+
83
+ # Convert an Arel AST to SQL using the correct connection's visitor
84
+ # This ensures proper quoting for the specific database adapter (MySQL uses backticks, PostgreSQL uses double quotes)
85
+ def to_sql_with_connection(arel_manager)
86
+ collector = Arel::Collectors::SQLString.new
87
+ visitor = connection.visitor
88
+ visitor.accept(arel_manager.ast, collector).value
89
+ end
82
90
  end
83
91
  end
@@ -20,7 +20,7 @@ module ClosureTree
20
20
 
21
21
  has_many :children, *_ct.has_many_order_with_option, class_name: _ct.model_class.to_s,
22
22
  foreign_key: _ct.parent_column_name,
23
- dependent: _ct.options[:dependent],
23
+ dependent: _ct.options[:dependent] == :adopt ? :nullify : _ct.options[:dependent],
24
24
  inverse_of: :parent do
25
25
  # We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
26
26
  def hash_tree(options = {})
@@ -47,4 +47,4 @@ module ClosureTree
47
47
  source: :descendant
48
48
  end
49
49
  end
50
- end
50
+ end
@@ -15,6 +15,7 @@ module ClosureTree
15
15
  :touch,
16
16
  :with_advisory_lock,
17
17
  :advisory_lock_name,
18
+ :advisory_lock_timeout_seconds,
18
19
  :scope
19
20
  )
20
21
 
@@ -18,7 +18,10 @@ module ClosureTree
18
18
  end
19
19
 
20
20
  def _ct_skip_sort_order_maintenance!
21
- @_ct_skip_sort_order_maintenance = true
21
+ ActiveSupport::Deprecation.new.warn(
22
+ '_ct_skip_sort_order_maintenance! is deprecated and will be removed in the next major version. ' \
23
+ 'Sort order maintenance is now handled automatically.'
24
+ )
22
25
  end
23
26
 
24
27
  def _ct_validate
@@ -38,7 +41,17 @@ module ClosureTree
38
41
  end
39
42
 
40
43
  def _ct_after_save
41
- rebuild! if saved_changes[_ct.parent_column_name] || @was_new_record
44
+ scope_changed = _ct.order_is_numeric? && _ct.scope_changed?(self)
45
+
46
+ if saved_changes[_ct.parent_column_name] || @was_new_record
47
+ rebuild!
48
+ elsif scope_changed
49
+ # Scope changed without parent change - reorder old scope's siblings
50
+ _ct_reorder_prior_siblings_if_parent_changed
51
+ _ct_reorder_siblings
52
+ elsif _ct.order_option? && saved_changes[_ct.order_column_sym]
53
+ _ct_reorder_siblings(saved_changes[_ct.order_column_sym].min)
54
+ end
42
55
  if saved_changes[_ct.parent_column_name] && !@was_new_record
43
56
  # Resetting the ancestral collections addresses
44
57
  # https://github.com/mceachen/closure_tree/issues/68
@@ -46,18 +59,31 @@ module ClosureTree
46
59
  self_and_ancestors.reload
47
60
  end
48
61
  @was_new_record = false # we aren't new anymore.
49
- @_ct_skip_sort_order_maintenance = false # only skip once.
50
62
  true # don't cancel anything.
51
63
  end
52
64
 
53
65
  def _ct_before_destroy
54
66
  _ct.with_advisory_lock do
67
+ _ct_adopt_children_to_grandparent if _ct.options[:dependent] == :adopt
55
68
  delete_hierarchy_references
56
69
  self.class.find(id).children.find_each(&:rebuild!) if _ct.options[:dependent] == :nullify
57
70
  end
58
71
  true # don't prevent destruction
59
72
  end
60
73
 
74
+ private def _ct_adopt_children_to_grandparent
75
+ grandparent_id = read_attribute(_ct.parent_column_name)
76
+ children_ids = self.class.where(_ct.parent_column_name => id).pluck(:id)
77
+
78
+ return if children_ids.empty?
79
+
80
+ # Update all children's parent_id in a single query
81
+ self.class.where(id: children_ids).update_all(_ct.parent_column_name => grandparent_id)
82
+
83
+ # Rebuild hierarchy for each child
84
+ self.class.where(id: children_ids).find_each(&:rebuild!)
85
+ end
86
+
61
87
  def rebuild!(called_by_rebuild = false)
62
88
  _ct.with_advisory_lock do
63
89
  delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
@@ -72,7 +98,7 @@ module ClosureTree
72
98
  SQL
73
99
  end
74
100
 
75
- if _ct.order_is_numeric? && !@_ct_skip_sort_order_maintenance
101
+ if _ct.order_is_numeric?
76
102
  _ct_reorder_prior_siblings_if_parent_changed
77
103
  # Prevent double-reordering of siblings:
78
104
  _ct_reorder_siblings unless called_by_rebuild
@@ -93,7 +119,7 @@ module ClosureTree
93
119
 
94
120
  hierarchy_table = hierarchy_class.arel_table
95
121
  delete_query = _ct.build_hierarchy_delete_query(hierarchy_table, id)
96
- _ct.connection.execute(delete_query.to_sql)
122
+ _ct.connection.execute(_ct.to_sql_with_connection(delete_query))
97
123
  end
98
124
  end
99
125
 
@@ -12,11 +12,16 @@ module ClosureTree
12
12
  end
13
13
 
14
14
  def _ct_reorder_prior_siblings_if_parent_changed
15
- return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
15
+ return if @was_new_record
16
+
17
+ parent_changed = saved_change_to_attribute?(_ct.parent_column_name)
18
+ scope_changed = _ct.scope_changed?(self)
19
+
20
+ return unless parent_changed || scope_changed
16
21
 
17
22
  was_parent_id = attribute_before_last_save(_ct.parent_column_name)
18
- scope_conditions = _ct.scope_values_from_instance(self)
19
- _ct.reorder_with_parent_id(was_parent_id, nil, scope_conditions)
23
+ previous_scope_conditions = _ct.previous_scope_values_from_instance(self)
24
+ _ct.reorder_with_parent_id(was_parent_id, nil, previous_scope_conditions)
20
25
  end
21
26
 
22
27
  def _ct_reorder_siblings(minimum_sort_order_value = nil)
@@ -132,9 +137,7 @@ module ClosureTree
132
137
  def prepend_child(child_node)
133
138
  child_node.order_value = -1
134
139
  child_node.parent = self
135
- child_node._ct_skip_sort_order_maintenance!
136
140
  if child_node.save
137
- _ct_reorder_children
138
141
  child_node.reload
139
142
  else
140
143
  child_node
@@ -161,19 +164,11 @@ module ClosureTree
161
164
 
162
165
  _ct.with_advisory_lock do
163
166
  prior_sibling_parent = sibling.parent
164
- reorder_from_value = if prior_sibling_parent == parent
165
- [order_value, sibling.order_value].compact.min
166
- else
167
- order_value
168
- end
169
167
 
170
168
  sibling.order_value = order_value
171
169
  sibling.parent = parent
172
- sibling._ct_skip_sort_order_maintenance!
173
170
  sibling.save # may be a no-op
174
171
 
175
- _ct_reorder_siblings(reorder_from_value)
176
-
177
172
  # The sort order should be correct now except for self and sibling, which may need to flip:
178
173
  sibling_is_after = reload.order_value < sibling.reload.order_value
179
174
  if add_after != sibling_is_after
@@ -16,9 +16,10 @@ module ClosureTree
16
16
 
17
17
  @options = {
18
18
  parent_column_name: 'parent_id',
19
- dependent: :nullify, # or :destroy or :delete_all -- see the README
19
+ dependent: :nullify, # or :destroy, :delete_all, or :adopt -- see the README
20
20
  name_column: 'name',
21
21
  with_advisory_lock: true, # This will be overridden by adapter support
22
+ advisory_lock_timeout_seconds: nil,
22
23
  numeric_order: false
23
24
  }.merge(options)
24
25
  raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
@@ -30,14 +31,34 @@ module ClosureTree
30
31
  end
31
32
  end
32
33
 
34
+ if !@options[:with_advisory_lock] && @options[:advisory_lock_timeout_seconds].present?
35
+ raise ArgumentError, "advisory_lock_timeout_seconds can't be specified when advisory_lock is disabled"
36
+ end
37
+
33
38
  return unless order_is_numeric?
34
39
 
35
40
  extend NumericOrderSupport.adapter_for_connection(connection)
36
41
  end
37
42
 
43
+ # Find the abstract base class for database connection
44
+ # This ensures hierarchy class uses the same database but doesn't inherit
45
+ # validations/callbacks from STI parent classes (issue #392)
46
+ def abstract_base_class
47
+ klass = model_class
48
+ while klass.superclass != ActiveRecord::Base
49
+ parent = klass.superclass
50
+ # Stop at abstract class (ApplicationRecord, SecondaryRecord, etc.)
51
+ return parent if parent.abstract_class?
52
+ # Stop at connection boundary (handles non-abstract parents with custom connections)
53
+ return parent if parent.connection_specification_name != parent.superclass.connection_specification_name
54
+ klass = parent
55
+ end
56
+ ActiveRecord::Base
57
+ end
58
+
38
59
  def hierarchy_class_for_model
39
60
  parent_class = model_class.module_parent
40
- hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass))
61
+ hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(abstract_base_class))
41
62
  model_class_name = model_class.to_s
42
63
  hierarchy_class.class_eval do
43
64
  # Rails 8.1+ requires an implicit_order_column for models without a primary key
@@ -137,8 +158,9 @@ module ClosureTree
137
158
  end
138
159
 
139
160
  def with_advisory_lock(&block)
140
- if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
141
- model_class.with_advisory_lock(advisory_lock_name) do
161
+ lock_method = options[:advisory_lock_timeout_seconds].present? ? :with_advisory_lock! : :with_advisory_lock
162
+ if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(lock_method)
163
+ model_class.public_send(lock_method, advisory_lock_name, advisory_lock_options) do
142
164
  transaction(&block)
143
165
  end
144
166
  else
@@ -222,12 +244,12 @@ module ClosureTree
222
244
  case scope_option
223
245
  when Symbol
224
246
  value = instance.read_attribute(scope_option)
225
- scope_hash[scope_option] = value unless value.nil?
247
+ scope_hash[scope_option] = value
226
248
  when Array
227
249
  scope_option.each do |item|
228
250
  if item.is_a?(Symbol)
229
251
  value = instance.read_attribute(item)
230
- scope_hash[item] = value unless value.nil?
252
+ scope_hash[item] = value
231
253
  end
232
254
  end
233
255
  end
@@ -235,6 +257,45 @@ module ClosureTree
235
257
  scope_hash
236
258
  end
237
259
 
260
+ def previous_scope_values_from_instance(instance)
261
+ return {} unless options[:scope] && instance
262
+
263
+ scope_option = options[:scope]
264
+ scope_hash = {}
265
+
266
+ case scope_option
267
+ when Symbol
268
+ value = instance.attribute_before_last_save(scope_option)
269
+ scope_hash[scope_option] = value
270
+ when Array
271
+ scope_option.each do |item|
272
+ if item.is_a?(Symbol)
273
+ value = instance.attribute_before_last_save(item)
274
+ scope_hash[item] = value
275
+ end
276
+ end
277
+ end
278
+
279
+ scope_hash
280
+ end
281
+
282
+ def scope_changed?(instance)
283
+ return false unless options[:scope] && instance
284
+
285
+ scope_option = options[:scope]
286
+
287
+ case scope_option
288
+ when Symbol
289
+ instance.saved_change_to_attribute?(scope_option)
290
+ when Array
291
+ scope_option.any? do |item|
292
+ item.is_a?(Symbol) && instance.saved_change_to_attribute?(item)
293
+ end
294
+ else
295
+ false
296
+ end
297
+ end
298
+
238
299
  def apply_scope_conditions(scope, instance = nil)
239
300
  return scope unless options[:scope] && instance
240
301
 
@@ -34,6 +34,10 @@ module ClosureTree
34
34
  end
35
35
  end
36
36
 
37
+ def advisory_lock_options
38
+ { timeout_seconds: options[:advisory_lock_timeout_seconds] }.compact
39
+ end
40
+
37
41
  def quoted_table_name
38
42
  connection.quote_table_name(table_name)
39
43
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClosureTree
4
- VERSION = Gem::Version.new('9.3.0')
4
+ VERSION = Gem::Version.new('9.6.0')
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.3.0
4
+ version: 9.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 7.0.0
33
+ version: 7.5.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 7.0.0
40
+ version: 7.5.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: zeitwerk
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -70,16 +70,16 @@ dependencies:
70
70
  name: minitest
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '5.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '5.0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: minitest-reporters
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -178,7 +178,7 @@ licenses:
178
178
  metadata:
179
179
  bug_tracker_uri: https://github.com/ClosureTree/closure_tree/issues
180
180
  changelog_uri: https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md
181
- documentation_uri: https://www.rubydoc.info/gems/closure_tree/9.3.0
181
+ documentation_uri: https://www.rubydoc.info/gems/closure_tree/9.6.0
182
182
  homepage_uri: https://closuretree.github.io/closure_tree/
183
183
  source_code_uri: https://github.com/ClosureTree/closure_tree
184
184
  rubygems_mfa_required: 'true'