Skip to content

Commit 68c16b0

Browse files
committed
Add optional validation toggle (#45)
Provide a checks: false option to skip validation checks for performance- critical paths, propagate the setting to children, and add dedicated tests plus documentation warnings about the risk.
1 parent e74667d commit 68c16b0

5 files changed

Lines changed: 123 additions & 4 deletions

File tree

API-CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ smooth transition to the new APIs.
3030
`to_hash`) to improve interoperability with frameworks such as Rails
3131
(see #104).
3232

33+
* Added per-tree validation toggles via `checks: false` on `Tree::TreeNode.new`
34+
to allow disabling guard checks in performance-critical code paths. This is
35+
potentially dangerous and can lead to unexpected behavior if invalid data
36+
enters the tree. Only disable checks with clear performance benchmark data
37+
supporting the risk (see #45).
38+
3339
## Release 2.2.0 Changes
3440

3541
* [Tree::TreeNode#add][add] now raises `ArgumentError` when attempting to add

History.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
* Accept hash-like inputs (`to_hash`) in hash conversion to support Rails
2323
`HashWithIndifferentAccess` data (see #104).
2424

25+
* Add a per-tree `checks: false` option to skip validation checks when
26+
performance matters. This is risky and can yield unexpected behavior if
27+
invalid data is introduced. Only disable checks with benchmark data that
28+
justifies the risk (see #45).
29+
2530
### 2.2.1pre / 2026-02-07
2631

2732
* Simplified development dependency constraints while maintaining Ruby 2.7+

lib/tree.rb

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ def validate_acyclic!
255255
# String (Integer names may cause *surprises*)
256256
#
257257
# @param [Object] content Content of the node.
258+
# @param [Hash, nil] options Optional settings such as { checks: false } to
259+
# disable validation checks for performance-critical usage. Disabling
260+
# checks is risky and can lead to unexpected behavior if invalid data is
261+
# added to the tree. Only disable checks with benchmark data that justifies
262+
# the risk.
258263
#
259264
# @raise [ArgumentError] Raised if the node name is empty.
260265
#
@@ -264,8 +269,11 @@ def validate_acyclic!
264269
# _zero-based_ indexing convention.
265270
#
266271
# @see #[]
267-
def initialize(name, content = nil)
268-
raise ArgumentError, 'Node name HAS to be provided!' if name.nil?
272+
def initialize(name, content = nil, options = nil)
273+
options = {} unless options.is_a?(Hash)
274+
@checks_enabled = options.fetch(:checks, true)
275+
276+
raise ArgumentError, 'Node name HAS to be provided!' if checks_enabled? && name.nil?
269277

270278
name = name.to_s if name.is_a?(Integer)
271279
@name = name
@@ -287,7 +295,7 @@ def detached_copy
287295
rescue TypeError
288296
@content
289297
end
290-
self.class.new(@name, cloned_content)
298+
self.class.new(@name, cloned_content, { checks: @checks_enabled })
291299
end
292300

293301
# Returns a copy of entire (sub-)tree from this node.
@@ -394,7 +402,7 @@ def to_s
394402
# @see #add
395403
# @see #initialize
396404
def [](name_or_index)
397-
raise ArgumentError, 'Name_or_index needs to be provided!' if name_or_index.nil?
405+
raise ArgumentError, 'Name_or_index needs to be provided!' if checks_enabled? && name_or_index.nil?
398406

399407
case name_or_index
400408
in Integer
@@ -447,6 +455,14 @@ def cmp(other, policy: :each)
447455
comparator.call
448456
end
449457

458+
# Returns +true+ when validation checks are enabled for this tree.
459+
def checks_enabled?
460+
@checks_enabled != false
461+
end
462+
463+
attr_writer :checks_enabled
464+
protected :checks_enabled=
465+
450466
private
451467

452468
def comparable_node?(other)

lib/tree/utils/structure_methods.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ def freeze_tree!
143143
end
144144

145145
def validate_add_child!(child)
146+
return unless checks_enabled?
147+
146148
raise ArgumentError, 'Attempting to add a nil node' unless child
147149
raise ArgumentError, 'Attempting add node to itself' if equal?(child)
148150
raise ArgumentError, 'Attempting add root as a child' if child.equal?(root)
@@ -153,12 +155,22 @@ def validate_add_child!(child)
153155
end
154156

155157
def ensure_unique_child_name!(child)
158+
return unless checks_enabled?
159+
156160
return unless @children_hash.include?(child.name)
157161

158162
raise "Child #{child.name} already added!"
159163
end
160164

161165
def insert_child_at!(child, at_index)
166+
unless checks_enabled?
167+
max = @children.size
168+
min = -(max + 1)
169+
at_index = max if at_index > max
170+
at_index = min if at_index < min
171+
return @children.insert(at_index, child)
172+
end
173+
162174
return @children.insert(at_index, child) if insertion_range.include?(at_index)
163175

164176
message = [
@@ -172,6 +184,7 @@ def insert_child_at!(child, at_index)
172184

173185
def attach_child!(child)
174186
@children_hash[child.name] = child
187+
child.send(:checks_enabled=, checks_enabled?) if child.respond_to?(:checks_enabled=, true)
175188
child.parent = self
176189
invalidate_size_cache_upwards!
177190
child

test/test_tree_checks.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# test_tree_checks.rb - This file is part of the RubyTree package.
2+
#
3+
# Copyright (c) 2026 Anupam Sengupta. All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without modification,
6+
# are permitted provided that the following conditions are met:
7+
#
8+
# - Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
#
11+
# - Redistributions in binary form must reproduce the above copyright notice, this
12+
# list of conditions and the following disclaimer in the documentation and/or
13+
# other materials provided with the distribution.
14+
#
15+
# - Neither the name of the organization nor the names of its contributors may
16+
# be used to endorse or promote products derived from this software without
17+
# specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
#
30+
# frozen_string_literal: true
31+
32+
require 'test/unit'
33+
require_relative '../lib/tree/tree_deps'
34+
35+
module TestTree
36+
class TestTreeChecks < Test::Unit::TestCase
37+
def test_checks_disabled_skips_validation
38+
root = Tree::TreeNode.new('root', nil, { checks: false })
39+
child1 = Tree::TreeNode.new('dup', nil, { checks: false })
40+
child2 = Tree::TreeNode.new('dup', nil, { checks: false })
41+
42+
root.add(child1)
43+
assert_nothing_raised { root.add(child2) }
44+
assert_equal(2, root.children.size)
45+
assert_nil(root[nil])
46+
end
47+
48+
def test_checks_enabled_validation_guards
49+
root = Tree::TreeNode.new('root')
50+
child1 = Tree::TreeNode.new('dup')
51+
child2 = Tree::TreeNode.new('dup')
52+
53+
root.add(child1)
54+
assert_raise(RuntimeError) { root.add(child2) }
55+
assert_raise(ArgumentError) { root[nil] }
56+
end
57+
58+
def test_checks_disabled_allows_out_of_range_insert
59+
root = Tree::TreeNode.new('root', nil, { checks: false })
60+
child1 = Tree::TreeNode.new('child1', nil, { checks: false })
61+
child2 = Tree::TreeNode.new('child2', nil, { checks: false })
62+
63+
root.add(child1, 999)
64+
root.add(child2, -999)
65+
assert_equal(2, root.children.size)
66+
end
67+
68+
def test_checks_setting_propagates_to_children
69+
root = Tree::TreeNode.new('root', nil, { checks: false })
70+
child = Tree::TreeNode.new('child')
71+
72+
root.add(child)
73+
assert_equal(false, child.checks_enabled?)
74+
75+
dup = Tree::TreeNode.new('child')
76+
assert_nothing_raised { root.add(dup) }
77+
end
78+
end
79+
end

0 commit comments

Comments
 (0)