Module: Alap::DeepClone
- Defined in:
- lib/alap/deep_clone.rb
Overview
Deep-clone for plain config data — Ruby port of src/core/deepCloneData.ts.
Detaches a config from the caller’s input by recursively rebuilding it, rejecting anything that is not plain data. Two reasons:
-
Detachment. Frameworks (Active Record instances, Hash subclasses, custom wrappers) carry behaviour that would otherwise leak into downstream immutability / serialization steps.
-
Trust boundary. Config is data. Handlers are registered separately via the runtime registry. A callable in config is a shape error; rejecting it here surfaces the error before any downstream step has to cope with it.
Allowed: Hash (string-keyed), Array, String, Integer, Float, Symbol, true, false, nil. Rejected: callables (Proc, Method, UnboundMethod), Hash/Array subclasses, class instances, cycles, non-String Hash keys, and structures that exceed the resource bounds.
Resource bounds, matching src/core/deepCloneData.ts:
- MAX_CLONE_DEPTH = 64 — rejects pathologically nested structures
- MAX_CLONE_NODES = 10_000 — rejects node-count DoS bombs
__proto__, constructor, prototype keys (plus the Python-port dunders retained for cross-port parity) are silently skipped during clone.
Defined Under Namespace
Classes: Error
Constant Summary collapse
- MAX_CLONE_DEPTH =
64- MAX_CLONE_NODES =
10_000- BLOCKED_KEYS =
Set.new(%w[ __proto__ constructor prototype __class__ __bases__ __mro__ __subclasses__ ]).freeze
Class Method Summary collapse
-
.call(value) ⇒ Object
Deep-clone
valuewith exotic types rejected.
Class Method Details
.call(value) ⇒ Object
Deep-clone value with exotic types rejected. Raises Alap::DeepClone::Error on callables, Hash/Array subclasses, class instances, cycles, non-String keys, depth over MAX_CLONE_DEPTH, or node count over MAX_CLONE_NODES.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/alap/deep_clone.rb', line 51 def self.call(value) seen = Set.new node_count = 0 path_or_root = ->(path) { path.empty? ? "<root>" : path } clone_value = nil clone_value = lambda do |v, depth, path| # primitives — no clone, no count case v when nil, true, false then return v when Integer, Float, String, Symbol then return v end if v.is_a?(Proc) || v.is_a?(Method) || v.is_a?(UnboundMethod) raise Error, "deep_clone: callables are not permitted in config " \ "(got #{v.class} at #{path_or_root.call(path)}). " \ "Handlers must be registered separately via the runtime registry." end if depth > MAX_CLONE_DEPTH raise Error, "deep_clone: depth exceeds #{MAX_CLONE_DEPTH} " \ "(at #{path_or_root.call(path)})" end node_count += 1 if node_count > MAX_CLONE_NODES raise Error, "deep_clone: node count exceeds #{MAX_CLONE_NODES}" end vid = v.object_id if seen.include?(vid) raise Error, "deep_clone: cycle detected (at #{path_or_root.call(path)})" end seen.add(vid) begin # Strict type check (instance_of? rather than is_a?) so Hash / # Array subclasses are rejected — matches TS behaviour that # checks getPrototypeOf === Object.prototype. if v.instance_of?(Hash) out = {} v.each do |k, val| unless k.is_a?(String) raise Error, "deep_clone: Hash keys must be Strings " \ "(got #{k.class} at #{path_or_root.call(path)})" end next if BLOCKED_KEYS.include?(k) sub_path = path.empty? ? k : "#{path}.#{k}" out[k] = clone_value.call(val, depth + 1, sub_path) end out elsif v.instance_of?(Array) v.each_with_index.map { |item, i| clone_value.call(item, depth + 1, "#{path}[#{i}]") } else raise Error, "deep_clone: unsupported type in config: " \ "#{v.class} at #{path_or_root.call(path)}. " \ "Config must be plain data (Hash / Array / String / " \ "Integer / Float / Symbol / true / false / nil)." end ensure seen.delete(vid) end end clone_value.call(value, 0, "") end |