voxgig_struct 0.1.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 (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +662 -0
  3. data/voxgig_struct.rb +2322 -0
  4. metadata +41 -0
data/voxgig_struct.rb ADDED
@@ -0,0 +1,2322 @@
1
+ require 'json'
2
+ require 'uri'
3
+
4
+ module VoxgigStruct
5
+ # --- Debug Logging Configuration ---
6
+ DEBUG = false
7
+
8
+ def self.log(msg)
9
+ puts "[DEBUG] #{msg}" if DEBUG
10
+ end
11
+
12
+ # --- Helper to convert internal undefined marker to Ruby nil ---
13
+ def self.conv(val)
14
+ val.equal?(UNDEF) ? nil : val
15
+ end
16
+
17
+ # --- Constants ---
18
+ S_MKEYPRE = 'key:pre'.freeze
19
+ S_MKEYPOST = 'key:post'.freeze
20
+ S_MVAL = 'val'.freeze
21
+ S_MKEY = 'key'.freeze
22
+
23
+ S_DKEY = '`$KEY`'.freeze
24
+ S_DMETA = '`$META`'.freeze
25
+ S_DTOP = '$TOP'.freeze
26
+ S_DERRS = '$ERRS'.freeze
27
+
28
+ S_any = 'any'.freeze
29
+ S_array = 'array'.freeze
30
+ S_boolean = 'boolean'.freeze
31
+ S_decimal = 'decimal'.freeze
32
+ S_function = 'function'.freeze
33
+ S_instance = 'instance'.freeze
34
+ S_integer = 'integer'.freeze
35
+ S_list = 'list'.freeze
36
+ S_map = 'map'.freeze
37
+ S_nil = 'nil'.freeze
38
+ S_node = 'node'.freeze
39
+ S_number = 'number'.freeze
40
+ S_null = 'null'.freeze
41
+ S_object = 'object'.freeze
42
+ S_scalar = 'scalar'.freeze
43
+ S_string = 'string'.freeze
44
+ S_symbol = 'symbol'.freeze
45
+ S_MT = ''.freeze # empty string constant (used as a prefix)
46
+ S_BT = '`'.freeze
47
+ S_DS = '$'.freeze
48
+ S_DT = '.'.freeze # delimiter for key paths
49
+ S_CN = ':'.freeze # colon for unknown paths
50
+ S_SP = ' '.freeze
51
+ S_VIZ = ': '.freeze
52
+ S_KEY = 'KEY'.freeze
53
+
54
+ # Types - bitfield integers matching TypeScript canonical
55
+ _t = 31
56
+ T_any = (1 << _t) - 1
57
+ _t -= 1
58
+ T_noval = 1 << _t
59
+ _t -= 1
60
+ T_boolean = 1 << _t
61
+ _t -= 1
62
+ T_decimal = 1 << _t
63
+ _t -= 1
64
+ T_integer = 1 << _t
65
+ _t -= 1
66
+ T_number = 1 << _t
67
+ _t -= 1
68
+ T_string = 1 << _t
69
+ _t -= 1
70
+ T_function = 1 << _t
71
+ _t -= 1
72
+ T_symbol = 1 << _t
73
+ _t -= 1
74
+ T_null = 1 << _t
75
+ _t -= 8
76
+ T_list = 1 << _t
77
+ _t -= 1
78
+ T_map = 1 << _t
79
+ _t -= 1
80
+ T_instance = 1 << _t
81
+ _t -= 5
82
+ T_scalar = 1 << _t
83
+ _t -= 1
84
+ T_node = 1 << _t
85
+
86
+ TYPENAME = [
87
+ S_any, S_nil, S_boolean, S_decimal, S_integer, S_number, S_string,
88
+ S_function, S_symbol, S_null,
89
+ '', '', '', '', '', '', '',
90
+ S_list, S_map, S_instance,
91
+ '', '', '', '',
92
+ S_scalar, S_node
93
+ ].freeze
94
+
95
+ SKIP = { '`$SKIP`' => true }.freeze
96
+ DELETE = { '`$DELETE`' => true }.freeze
97
+
98
+ # Unique undefined marker.
99
+ UNDEF = Object.new.freeze
100
+
101
+ # Mode constants (bitfield) matching TypeScript canonical
102
+ M_KEYPRE = 1
103
+ M_KEYPOST = 2
104
+ M_VAL = 4
105
+
106
+ MODENAME = { M_VAL => 'val', M_KEYPRE => 'key:pre', M_KEYPOST => 'key:post' }.freeze
107
+ PLACEMENT = { M_VAL => 'value', M_KEYPRE => S_MKEY, M_KEYPOST => S_MKEY }.freeze
108
+
109
+ MAXDEPTH = 32
110
+
111
+ # --- Utility functions ---
112
+
113
+ def self.sorted(val)
114
+ case val
115
+ when Hash
116
+ sorted_hash = {}
117
+ val.keys.sort.each { |k| sorted_hash[k] = sorted(val[k]) }
118
+ sorted_hash
119
+ when Array
120
+ val.map { |elem| sorted(elem) }
121
+ else
122
+ val
123
+ end
124
+ end
125
+
126
+ def self.clone(val)
127
+ return nil if val.nil? || val.equal?(UNDEF)
128
+
129
+ if isfunc(val)
130
+ val
131
+ elsif islist(val)
132
+ val.map { |v| clone(v) }
133
+ elsif ismap(val)
134
+ result = {}
135
+ val.each { |k, v| result[k] = isfunc(v) ? v : clone(v) }
136
+ result
137
+ else
138
+ val
139
+ end
140
+ end
141
+
142
+ def self.escre(s)
143
+ s = '' if s.nil?
144
+ Regexp.escape(s)
145
+ end
146
+
147
+ # ---------------------------------------------------------------------
148
+ # Regex utility — uniform re_* API (see /REGEX_API.md). Ruby's Onigmo
149
+ # engine is a strict superset of RE2.
150
+ # ---------------------------------------------------------------------
151
+
152
+ def self.re_compile(pattern)
153
+ pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern)
154
+ end
155
+
156
+ def self.re_test(pattern, input)
157
+ !!(re_compile(pattern) =~ input.to_s)
158
+ end
159
+
160
+ def self.re_find(pattern, input)
161
+ m = re_compile(pattern).match(input.to_s)
162
+ return nil if m.nil?
163
+
164
+ [m[0]] + m.captures.map { |c| c.nil? ? '' : c }
165
+ end
166
+
167
+ def self.re_find_all(pattern, input)
168
+ out = []
169
+ input.to_s.scan(re_compile(pattern)) do
170
+ m = Regexp.last_match
171
+ out << ([m[0]] + m.captures.map { |c| c.nil? ? '' : c })
172
+ end
173
+ out
174
+ end
175
+
176
+ def self.re_replace(pattern, input, replacement)
177
+ rx = re_compile(pattern)
178
+ if replacement.respond_to?(:call)
179
+ input.to_s.gsub(rx) do |_match|
180
+ m = Regexp.last_match
181
+ replacement.call([m[0]] + m.captures.map { |c| c.nil? ? '' : c })
182
+ end
183
+ else
184
+ # Translate JS-style $& / $1 to Ruby's \0 / \1
185
+ ruby_repl = replacement.gsub(/\$([&0-9])/) do |_|
186
+ ch = ::Regexp.last_match(1)
187
+ ch == '&' ? '\\0' : "\\#{ch}"
188
+ end
189
+ input.to_s.gsub(rx, ruby_repl)
190
+ end
191
+ end
192
+
193
+ def self.re_escape(s)
194
+ escre(s)
195
+ end
196
+
197
+ def self.escurl(s)
198
+ s = '' if s.nil?
199
+ URI::DEFAULT_PARSER.escape(s, /[^A-Za-z0-9\-._~]/)
200
+ end
201
+
202
+ # --- Internal getprop ---
203
+ # Returns the value if found; otherwise returns alt (default is UNDEF)
204
+ def self._getprop(val, key, alt = UNDEF)
205
+ log("(_getprop) called with val=#{val.inspect} and key=#{key.inspect}")
206
+ return alt if val.nil? || key.nil?
207
+
208
+ if islist(val)
209
+ key = key.to_i if key.to_s =~ /\A\d+\z/
210
+ unless key.is_a?(Numeric) && key >= 0 && key < val.size
211
+ log("(_getprop) index #{key.inspect} out of bounds; returning alt")
212
+ return alt
213
+ end
214
+ result = val[key]
215
+ log("(_getprop) returning #{result.inspect} from array for key #{key}")
216
+ result
217
+ elsif ismap(val)
218
+ key_str = key.to_s
219
+ if val.key?(key_str)
220
+ result = val[key_str]
221
+ log("(_getprop) found key #{key_str.inspect} in hash, returning #{result.inspect}")
222
+ result
223
+ elsif key.is_a?(String) && val.key?(key.to_sym)
224
+ result = val[key.to_sym]
225
+ log("(_getprop) found symbol key #{key.to_sym.inspect} in hash, returning #{result.inspect}")
226
+ result
227
+ else
228
+ log("(_getprop) key #{key.inspect} not found; returning alt")
229
+ alt
230
+ end
231
+ else
232
+ log('(_getprop) value is not a node; returning alt')
233
+ alt
234
+ end
235
+ end
236
+
237
+ # --- Public getprop ---
238
+ # Group A reader: a stored JSON null (nil) at the key counts as "no value",
239
+ # indistinguishable from absent, so it falls back to alt (canonical TS:
240
+ # `if (null == out) return alt`). Group B callers that must preserve a
241
+ # stored null use _getprop directly.
242
+ def self.getprop(val, key, alt = nil)
243
+ result = _getprop(val, key, UNDEF)
244
+ result.equal?(UNDEF) || result.nil? ? alt : result
245
+ end
246
+
247
+ def self.isempty(val)
248
+ return true if val.nil? || val.equal?(UNDEF) || val == ''
249
+ return true if islist(val) && val.empty?
250
+ return true if ismap(val) && val.empty?
251
+
252
+ false
253
+ end
254
+
255
+ def self.iskey(key)
256
+ (key.is_a?(String) && !key.empty?) || key.is_a?(Numeric)
257
+ end
258
+
259
+ def self.islist(val)
260
+ val.is_a?(Array)
261
+ end
262
+
263
+ def self.ismap(val)
264
+ val.is_a?(Hash)
265
+ end
266
+
267
+ def self.isnode(val)
268
+ ismap(val) || islist(val)
269
+ end
270
+
271
+ def self.items(val, apply = nil)
272
+ if ismap(val)
273
+ pairs = val.keys.sort.map { |k| [k, val[k]] }
274
+ elsif islist(val)
275
+ pairs = val.each_with_index.map { |v, i| [i.to_s, v] }
276
+ else
277
+ return []
278
+ end
279
+ apply ? pairs.map { |item| apply.call(item) } : pairs
280
+ end
281
+
282
+ def self.setprop(parent, key, val = :no_val_provided)
283
+ log(">>> setprop called with parent=#{parent.inspect}, key=#{key.inspect}, val=#{val.inspect}")
284
+ return parent unless iskey(key)
285
+
286
+ if ismap(parent)
287
+ key_str = key.to_s
288
+ if val == :no_val_provided
289
+ parent.delete(key_str)
290
+ else
291
+ parent[key_str] = val
292
+ end
293
+ elsif islist(parent)
294
+ begin
295
+ key_i = Integer(key)
296
+ rescue ArgumentError
297
+ return parent
298
+ end
299
+ if val == :no_val_provided
300
+ parent.delete_at(key_i) if key_i >= 0 && key_i < parent.length
301
+ elsif key_i >= 0
302
+ index = [key_i, parent.length].min
303
+ parent[index] = val
304
+ else
305
+ parent.unshift(val)
306
+ end
307
+ end
308
+ log("<<< setprop result: #{parent.inspect}")
309
+ parent
310
+ end
311
+
312
+ def self.stringify(val, maxlen = nil, _pretty = nil)
313
+ return '' if val.equal?(UNDEF)
314
+ return 'null' if val.nil?
315
+
316
+ if val.is_a?(String)
317
+ valstr = val
318
+ else
319
+ begin
320
+ v = val.is_a?(Hash) ? sorted(val) : val
321
+ valstr = JSON.generate(v)
322
+ valstr = valstr.gsub('"', '')
323
+ rescue StandardError
324
+ valstr = val.to_s
325
+ end
326
+ end
327
+
328
+ valstr = "#{valstr[0, maxlen - 3]}..." if !maxlen.nil? && maxlen >= 0 && (valstr.length > maxlen)
329
+
330
+ valstr
331
+ end
332
+
333
+ def self.pathify(val, startin = nil, endin = nil)
334
+ pathstr = nil
335
+
336
+ path = if islist(val)
337
+ val
338
+ elsif val.is_a?(String)
339
+ [val]
340
+ elsif val.is_a?(Numeric)
341
+ [val]
342
+ end
343
+
344
+ start = startin.nil? ? 0 : startin.negative? ? 0 : startin
345
+ end_idx = endin.nil? ? 0 : endin.negative? ? 0 : endin
346
+
347
+ if path && start >= 0
348
+ path = path[start..(-end_idx - 1)] || []
349
+ pathstr = if path.empty?
350
+ '<root>'
351
+ else
352
+ path
353
+ .select { |p| iskey(p) }
354
+ .map do |p|
355
+ if p.is_a?(Numeric)
356
+ S_MT + p.floor.to_s
357
+ else
358
+ p.gsub('.', S_MT)
359
+ end
360
+ end
361
+ .join(S_DT)
362
+ end
363
+ end
364
+
365
+ if pathstr.nil?
366
+ pathstr = "<unknown-path#{S_CN + stringify(val, 47) unless val.equal?(UNDEF)}>"
367
+ end
368
+
369
+ pathstr
370
+ end
371
+
372
+ def self.strkey(key = nil)
373
+ return '' if key.nil?
374
+ return key if key.is_a?(String)
375
+ return key.floor.to_s if key.is_a?(Numeric)
376
+
377
+ ''
378
+ end
379
+
380
+ def self.isfunc(val)
381
+ val.respond_to?(:call)
382
+ end
383
+
384
+ def self.getdef(val, alt)
385
+ val.nil? ? alt : val
386
+ end
387
+
388
+ def self.size(val)
389
+ return 0 if val.nil? || val.equal?(UNDEF)
390
+ return val.length if val.is_a?(String) || islist(val)
391
+ return val.keys.length if ismap(val)
392
+ return (val == true ? 1 : 0) if [true, false].include?(val)
393
+ return val.to_i if val.is_a?(Numeric)
394
+
395
+ 0
396
+ end
397
+
398
+ def self.slice(val, start_idx = nil, end_idx = nil, mutate = false)
399
+ return val if val.nil? || val.equal?(UNDEF)
400
+
401
+ if val.is_a?(Numeric) && !val.is_a?(TrueClass) && !val.is_a?(FalseClass)
402
+ s = start_idx.nil? ? (-Float::INFINITY) : start_idx
403
+ e = end_idx.nil? ? Float::INFINITY : (end_idx - 1)
404
+ # Not Comparable#clamp: that raises when s > e, but here we want e returned.
405
+ return [[val, s].max, e].min # rubocop:disable Style/ComparableClamp
406
+ end
407
+
408
+ vlen = size(val)
409
+
410
+ start_idx = 0 if !end_idx.nil? && start_idx.nil?
411
+
412
+ unless start_idx.nil?
413
+ s = start_idx
414
+ e = end_idx
415
+
416
+ if s.negative?
417
+ e = vlen + s
418
+ e = 0 if e.negative?
419
+ s = 0
420
+ elsif !e.nil?
421
+ if e.negative?
422
+ e = vlen + e
423
+ e = 0 if e.negative?
424
+ elsif vlen < e
425
+ e = vlen
426
+ end
427
+ else
428
+ e = vlen
429
+ end
430
+
431
+ s = vlen if vlen < s
432
+
433
+ if islist(val)
434
+ result = val[s...e] || []
435
+ if mutate
436
+ val.replace(result)
437
+ return val
438
+ end
439
+ return result
440
+ elsif val.is_a?(String)
441
+ return val[s...e] || ''
442
+ end
443
+ end
444
+
445
+ val
446
+ end
447
+
448
+ def self.pad(str, padding = nil, padchar = nil)
449
+ str = stringify(str) unless str.is_a?(String)
450
+ padding = 44 if padding.nil?
451
+ padchar = padchar.nil? ? ' ' : "#{padchar} "[0]
452
+ if padding >= 0
453
+ str.ljust(padding, padchar)
454
+ else
455
+ str.rjust(-padding, padchar)
456
+ end
457
+ end
458
+
459
+ def self.getelem(val, key, alt = UNDEF)
460
+ out = UNDEF
461
+ if islist(val) && !key.nil? && !key.equal?(UNDEF)
462
+ begin
463
+ nkey = key.to_i
464
+ if key.to_s.strip.match?(/\A-?\d+\z/)
465
+ nkey = val.length + nkey if nkey.negative?
466
+ out = nkey >= 0 && nkey < val.length ? val[nkey] : UNDEF
467
+ end
468
+ rescue StandardError
469
+ end
470
+ end
471
+ # A null (or absent) slot counts as "no value" — the same Group A rule
472
+ # getprop applies (canonical TS: `if (null == out) return alt`).
473
+ if out.equal?(UNDEF) || out.nil?
474
+ return isfunc(alt) ? alt.call : (alt.equal?(UNDEF) ? nil : alt)
475
+ end
476
+
477
+ out
478
+ end
479
+
480
+ def self.flatten(lst, depth = nil)
481
+ depth = 1 if depth.nil?
482
+ return lst unless islist(lst)
483
+
484
+ out = []
485
+ lst.each do |item|
486
+ if islist(item) && depth.positive?
487
+ out.concat(flatten(item, depth - 1))
488
+ else
489
+ out << item unless item.nil? || item.equal?(UNDEF)
490
+ end
491
+ end
492
+ out
493
+ end
494
+
495
+ def self.filter(val, check)
496
+ return [] unless isnode(val)
497
+
498
+ items(val).select { |item| check.call(item) }.map { |item| item[1] }
499
+ end
500
+
501
+ def self.delprop(parent, key)
502
+ return parent unless iskey(key)
503
+
504
+ if ismap(parent)
505
+ ks = strkey(key)
506
+ parent.delete(ks)
507
+ elsif islist(parent)
508
+ return parent unless key.to_s.match?(/\A-?\d+\z/)
509
+
510
+ begin
511
+ ki = key.to_i
512
+ parent.delete_at(ki) if ki >= 0 && ki < parent.length
513
+ rescue StandardError
514
+ end
515
+ end
516
+ parent
517
+ end
518
+
519
+ def self.join(arr, sep = nil, url = nil)
520
+ return '' unless islist(arr)
521
+
522
+ sepdef = sep.nil? ? ',' : sep.to_s
523
+ sepre = sepdef.length == 1 ? Regexp.escape(sepdef) : nil
524
+
525
+ # Filter to non-empty strings only
526
+ parts = arr.select { |n| n.is_a?(String) && n != '' }
527
+
528
+ parts = parts.map.with_index do |s, i|
529
+ if sepre
530
+ if url && i.zero?
531
+ s = s.sub(/#{sepre}+$/, '')
532
+ next s
533
+ end
534
+ s = s.sub(/^#{sepre}+/, '') if i.positive?
535
+ s = s.sub(/#{sepre}+$/, '') if i < parts.length - 1 || !url
536
+ # Collapse internal duplicate separators
537
+ s = s.gsub(/([^#{sepre}])#{sepre}+([^#{sepre}])/, "\\1#{sepdef}\\2")
538
+ end
539
+ s
540
+ end.reject(&:empty?)
541
+
542
+ parts.join(sepdef)
543
+ end
544
+
545
+ def self.joinurl(sarr)
546
+ join(sarr, '/', true)
547
+ end
548
+
549
+ def self.jsonify(val, flags = nil)
550
+ str = 'null'
551
+ unless val.nil?
552
+ begin
553
+ indent = (flags.is_a?(Hash) ? (flags['indent'] || flags[:indent]) : nil) || 2
554
+ str = _json_stringify(val, indent, 0)
555
+ str = 'null' if str.nil?
556
+ offset = (flags.is_a?(Hash) ? (flags['offset'] || flags[:offset]) : nil) || 0
557
+ if offset.positive?
558
+ lines = str.split("\n")
559
+ lines[0] || ''
560
+ rest = lines[1..] || []
561
+ rest_indented = rest.map { |l| (' ' * offset) + l }
562
+ str = "{\n#{rest_indented.join("\n")}"
563
+ end
564
+ rescue StandardError
565
+ str = '__JSONIFY_FAILED__'
566
+ end
567
+ end
568
+ str
569
+ end
570
+
571
+ # Mimic JSON.stringify(val, null, indent) from JavaScript.
572
+ # indent == 0 → compact single-line. indent > 0 → pretty printed.
573
+ # Map keys are emitted in insertion order (matches TS canonical).
574
+ def self._json_stringify(val, indent, depth)
575
+ return 'null' if val.nil?
576
+ return val.to_s if [true, false].include?(val)
577
+ return val.to_s if val.is_a?(Numeric)
578
+ return JSON.generate(val) if val.is_a?(String)
579
+
580
+ compact = indent.nil? || indent <= 0
581
+ ind = compact ? '' : ' ' * indent
582
+ current_indent = compact ? '' : ind * (depth + 1)
583
+ closing_indent = compact ? '' : ind * depth
584
+ open_nl = compact ? '' : "\n"
585
+ pair_sep = compact ? ',' : ",\n"
586
+ kv_sep = compact ? ':' : ': '
587
+
588
+ if islist(val)
589
+ return '[]' if val.empty?
590
+
591
+ items_str = val.map { |v| current_indent + _json_stringify(v, indent, depth + 1) }
592
+ "[#{open_nl}#{items_str.join(pair_sep)}#{open_nl}#{closing_indent}]"
593
+ elsif ismap(val)
594
+ return '{}' if val.empty?
595
+
596
+ pairs = val.keys.map do |k|
597
+ "#{current_indent}#{JSON.generate(k)}#{kv_sep}#{_json_stringify(val[k], indent, depth + 1)}"
598
+ end
599
+ "{#{open_nl}#{pairs.join(pair_sep)}#{open_nl}#{closing_indent}}"
600
+ elsif isfunc(val)
601
+ 'null'
602
+ else
603
+ 'null'
604
+ end
605
+ end
606
+
607
+ def self.jm(*kv)
608
+ result = {}
609
+ i = 0
610
+ while i < kv.length - 1
611
+ result[kv[i].to_s] = kv[i + 1]
612
+ i += 2
613
+ end
614
+ result
615
+ end
616
+
617
+ def self.jt(*v)
618
+ v.to_a
619
+ end
620
+
621
+ def self.replace(s, from, to)
622
+ return s.to_s unless s.is_a?(String)
623
+
624
+ if from.is_a?(Regexp)
625
+ s.gsub(from, to.to_s)
626
+ else
627
+ s.gsub(from.to_s, to.to_s)
628
+ end
629
+ end
630
+
631
+ def self.keysof(val)
632
+ return [] unless isnode(val)
633
+
634
+ if ismap(val)
635
+ val.keys.sort
636
+ elsif islist(val)
637
+ (0...val.length).map(&:to_s)
638
+ else
639
+ []
640
+ end
641
+ end
642
+
643
+ # Group A reader: a key whose stored value is JSON null (nil) counts as
644
+ # "no value", same rule as getprop (canonical TS: `null != getprop(val, key)`).
645
+ def self.haskey(val = UNDEF, key = UNDEF)
646
+ !getprop(val, key).nil?
647
+ end
648
+
649
+ # NOTE: this is a second, simpler joinurl definition that intentionally
650
+ # overrides the join-based one above; kept for cross-language source parity.
651
+ def self.joinurl(parts) # rubocop:disable Lint/DuplicateMethods
652
+ parts.compact.map.with_index do |s, i|
653
+ s = s.to_s
654
+ if i.zero?
655
+ s.sub(%r{/+$}, '')
656
+ else
657
+ s.sub(%r{([^/])/+}, '\1/').sub(%r{^/+}, '').sub(%r{/+$}, '')
658
+ end
659
+ end.reject(&:empty?).join('/')
660
+ end
661
+
662
+ # Get type name string from type bitfield value.
663
+ def self._clz32(n)
664
+ return 32 if n <= 0
665
+
666
+ 31 - (n.bit_length - 1)
667
+ end
668
+
669
+ def self.typename(t)
670
+ t = t.to_i
671
+ idx = _clz32(t)
672
+ return TYPENAME[0] if idx.negative? || idx >= TYPENAME.length
673
+
674
+ r = TYPENAME[idx]
675
+ r.nil? || r == S_MT ? TYPENAME[0] : r
676
+ end
677
+
678
+ # Determine the type of a value as a bitfield integer.
679
+ def self.typify(value = UNDEF)
680
+ return T_noval if value.equal?(UNDEF)
681
+ return T_scalar | T_null if value.nil?
682
+
683
+ return T_scalar | T_boolean if [true, false].include?(value)
684
+
685
+ return T_scalar | T_function if isfunc(value)
686
+
687
+ return T_scalar | T_number | T_integer if value.is_a?(Integer)
688
+
689
+ if value.is_a?(Float)
690
+ return value.nan? ? T_noval : (T_scalar | T_number | T_decimal)
691
+ end
692
+
693
+ return T_scalar | T_string if value.is_a?(String)
694
+
695
+ return T_scalar | T_symbol if value.is_a?(Symbol)
696
+
697
+ return T_node | T_list if islist(value)
698
+
699
+ return T_node | T_map if ismap(value)
700
+
701
+ T_any
702
+ end
703
+
704
+ # Walk a data structure depth first, applying a function to each value.
705
+ # The `path` argument passed to the before/after callbacks is a single
706
+ # mutable array per depth, shared across all callback invocations for the
707
+ # lifetime of this top-level walk call. Callbacks that need to store the
708
+ # path MUST clone it (e.g. `path.dup`); the contents will otherwise be
709
+ # overwritten by subsequent visits.
710
+ def self.walk(val, before = nil, after = nil, maxdepth = nil, key: nil, parent: nil, path: nil, pool: nil)
711
+ pool = [[]] if pool.nil?
712
+ path = pool[0] if path.nil?
713
+
714
+ depth = path.length
715
+
716
+ _before = before
717
+ _after = after
718
+
719
+ out = _before.nil? ? val : _before.call(key, val, parent, path)
720
+
721
+ md = maxdepth.is_a?(Numeric) && maxdepth >= 0 ? maxdepth : MAXDEPTH
722
+ return out if md.zero? || (md.positive? && md <= depth)
723
+
724
+ if isnode(out)
725
+ child_depth = depth + 1
726
+ child_path = pool[child_depth]
727
+ if child_path.nil?
728
+ child_path = Array.new(child_depth)
729
+ pool[child_depth] = child_path
730
+ end
731
+ # Sync prefix [0..depth-1] from the current path. Only needed once per
732
+ # parent: siblings share the same prefix and will each overwrite slot
733
+ # [depth] below.
734
+ i = 0
735
+ while i < depth
736
+ child_path[i] = path[i]
737
+ i += 1
738
+ end
739
+
740
+ items(out).each do |ckey, child|
741
+ child_path[depth] = ckey.to_s
742
+ result = walk(child, _before, _after, md, key: ckey, parent: out, path: child_path, pool: pool)
743
+ if ismap(out)
744
+ out[ckey.to_s] = result
745
+ elsif islist(out)
746
+ out[ckey.to_i] = result
747
+ end
748
+ end
749
+ end
750
+
751
+ out = _after.call(key, out, parent, path) unless _after.nil?
752
+
753
+ out
754
+ end
755
+
756
+ # --- Deep Merge Helpers for merge ---
757
+ #
758
+ # deep_merge recursively combines two nodes.
759
+ # For hashes, keys in b override those in a.
760
+ # For arrays, merge index-by-index; b's element overrides a's at that position,
761
+ # while preserving items that b does not provide.
762
+ def self.deep_merge(a, b)
763
+ if ismap(a) && ismap(b)
764
+ merged = a.dup
765
+ b.each do |k, v|
766
+ merged[k] = if merged.key?(k)
767
+ deep_merge(merged[k], v)
768
+ else
769
+ v
770
+ end
771
+ end
772
+ merged
773
+ elsif islist(a) && islist(b)
774
+ max_len = [a.size, b.size].max
775
+ merged = []
776
+ (0...max_len).each do |i|
777
+ merged[i] = if i < a.size && i < b.size
778
+ deep_merge(a[i], b[i])
779
+ elsif i < b.size
780
+ b[i]
781
+ else
782
+ a[i]
783
+ end
784
+ end
785
+ merged
786
+ else
787
+ # For non-node values, b wins.
788
+ b
789
+ end
790
+ end
791
+
792
+ # --- Merge function ---
793
+ # Merge a list of values. Later values have precedence.
794
+ # Nodes override scalars. Matching node kinds merge recursively.
795
+ def self.merge(val, maxdepth = nil)
796
+ md = maxdepth.nil? ? MAXDEPTH : [maxdepth, 0].max
797
+
798
+ return val unless islist(val)
799
+
800
+ lenlist = val.length
801
+ return nil if lenlist.zero?
802
+ return val[0] if lenlist == 1
803
+
804
+ out = getprop(val, 0, {})
805
+
806
+ (1...lenlist).each do |oI|
807
+ obj = val[oI]
808
+
809
+ if isnode(obj)
810
+ cur = [out]
811
+ dst = [out]
812
+
813
+ before_fn = lambda { |key, v, _parent, path|
814
+ pI = path.length
815
+
816
+ if md <= pI
817
+ cur << nil while cur.length <= pI
818
+ cur[pI] = v
819
+ setprop(cur[pI - 1], key, v) if pI.positive? && pI - 1 < cur.length
820
+ next nil # stop descending
821
+ elsif !isnode(v)
822
+ cur[pI] = v
823
+ else
824
+ # Extend arrays as needed
825
+ dst << nil while dst.length <= pI
826
+ cur << nil while cur.length <= pI
827
+
828
+ dst[pI] = pI.positive? ? getprop(dst[pI - 1], key) : dst[pI]
829
+ tval = dst[pI]
830
+
831
+ if tval.nil?
832
+ cur[pI] = islist(v) ? [] : {}
833
+ elsif (islist(v) && islist(tval)) || (ismap(v) && ismap(tval))
834
+ cur[pI] = tval
835
+ else
836
+ cur[pI] = v
837
+ v = nil # stop descending
838
+ end
839
+ end
840
+
841
+ v
842
+ }
843
+
844
+ after_fn = lambda { |key, _v, _parent, path|
845
+ cI = path.length
846
+ if cI < 1
847
+ next (cur.length.positive? ? cur[0] : _v)
848
+ end
849
+
850
+ target = cI - 1 < cur.length ? cur[cI - 1] : nil
851
+ value = cI < cur.length ? cur[cI] : nil
852
+
853
+ setprop(target, key, value) if target
854
+ value
855
+ }
856
+
857
+ out = walk(obj, before_fn, after_fn)
858
+ else
859
+ # Non-nodes (including nil) override directly
860
+ out = obj
861
+ end
862
+ end
863
+
864
+ if md.zero?
865
+ out = getelem(val, -1)
866
+ out = islist(out) ? [] : ismap(out) ? {} : out
867
+ end
868
+
869
+ out
870
+ end
871
+
872
+ # Get value at a key path deep inside a store.
873
+ # Matches TS canonical: getpath(store, path, injdef?)
874
+ def self.getpath(store, path, injdef = nil)
875
+ # Operate on a string array.
876
+ if islist(path)
877
+ parts = path.dup
878
+ elsif path.is_a?(String)
879
+ parts = path.split(S_DT, -1)
880
+ elsif path.is_a?(Numeric)
881
+ parts = [strkey(path)]
882
+ else
883
+ return nil
884
+ end
885
+
886
+ val = store
887
+
888
+ # Extract injdef properties (support both Hash and object with accessors)
889
+ if injdef.is_a?(Hash)
890
+ base = injdef['base'] || injdef[:base]
891
+ dparent = injdef['dparent'] || injdef[:dparent]
892
+ inj_meta = injdef['meta'] || injdef[:meta]
893
+ inj_key = injdef['key'] || injdef[:key]
894
+ dpath = injdef['dpath'] || injdef[:dpath]
895
+ handler = injdef['handler'] || injdef[:handler]
896
+ elsif injdef.respond_to?(:base)
897
+ base = injdef.base
898
+ dparent = injdef.dparent
899
+ inj_meta = injdef.meta
900
+ inj_key = injdef.key
901
+ dpath = injdef.dpath
902
+ handler = injdef.handler
903
+ else
904
+ base = nil
905
+ dparent = nil
906
+ inj_meta = nil
907
+ inj_key = nil
908
+ dpath = nil
909
+ handler = nil
910
+ end
911
+
912
+ src = base ? _getprop(store, base, store) : store
913
+ numparts = parts.length
914
+
915
+ # An empty path (incl empty string) just finds the src.
916
+ if path.nil? || store.nil? || (numparts == 1 && parts[0] == S_MT) || numparts.zero?
917
+ val = src
918
+ elsif numparts.positive?
919
+ # Check for $ACTIONs
920
+ val = _getprop(store, parts[0], UNDEF) if numparts == 1
921
+
922
+ unless isfunc(val)
923
+ val = src
924
+
925
+ # Check for meta path syntax
926
+ if parts[0].is_a?(String) && (m = parts[0].match(/^([^$]+)\$([=~])(.+)$/)) && inj_meta
927
+ val = _getprop(inj_meta, m[1], UNDEF)
928
+ parts[0] = m[3]
929
+ end
930
+
931
+ pI = 0
932
+ while !val.equal?(UNDEF) && !val.nil? && pI < numparts
933
+ part = parts[pI]
934
+
935
+ if injdef && part == '$KEY'
936
+ part = inj_key || part
937
+ elsif part.is_a?(String) && part.start_with?('$GET:')
938
+ part = stringify(getpath(src, part[5..-2]))
939
+ elsif part.is_a?(String) && part.start_with?('$REF:')
940
+ part = stringify(getpath(_getprop(store, '$SPEC', UNDEF), part[5..-2]))
941
+ elsif injdef && part.is_a?(String) && part.start_with?('$META:')
942
+ part = stringify(getpath(inj_meta, part[6..-2]))
943
+ end
944
+
945
+ # $$ escapes $
946
+ part = part.gsub('$$', '$') if part.is_a?(String)
947
+
948
+ if part == S_MT
949
+ ascends = 0
950
+ while pI + 1 < parts.length && parts[pI + 1] == S_MT
951
+ ascends += 1
952
+ pI += 1
953
+ end
954
+
955
+ if injdef && ascends.positive?
956
+ ascends -= 1 if pI == parts.length - 1
957
+ if ascends.zero?
958
+ val = dparent
959
+ else
960
+ fullpath = flatten([slice(dpath, 0 - ascends), parts[(pI + 1)..]])
961
+ val = if dpath.is_a?(Array) && ascends <= dpath.length
962
+ getpath(store, fullpath)
963
+ else
964
+ UNDEF
965
+ end
966
+ break
967
+ end
968
+ else
969
+ val = dparent || src
970
+ end
971
+ else
972
+ val = _getprop(val, part, UNDEF)
973
+ end
974
+ pI += 1
975
+ end
976
+ end
977
+ end
978
+
979
+ # Injdef may provide a custom handler to modify found value.
980
+ if handler && isfunc(handler)
981
+ ref = pathify(path)
982
+ val = handler.call(injdef, val.equal?(UNDEF) ? nil : val, ref, store)
983
+ end
984
+
985
+ val.equal?(UNDEF) ? nil : val
986
+ end
987
+
988
+ S_BKEY = '`$KEY`'.freeze
989
+ S_BANNO = '`$ANNO`'.freeze
990
+ S_BEXACT = '`$EXACT`'.freeze
991
+ S_BVAL = '`$VAL`'.freeze
992
+ S_DSPEC = '$SPEC'.freeze
993
+
994
+ R_FULL_INJECT = /\A`(\$[A-Z]+|[^`]*)[0-9]*`\z/.freeze
995
+ R_PART_INJECT = /`([^`]*)`/.freeze
996
+ R_META_PATH = /\A([^$]+)\$([=~])(.+)\z/.freeze
997
+ R_DOUBLE_DOLLAR = /\$\$/.freeze
998
+
999
+ # --- _injectstr: Resolve backtick expressions in strings ---
1000
+ def self._injectstr(val, store, inj = nil)
1001
+ return S_MT unless val.is_a?(String) && val != S_MT
1002
+
1003
+ out = val
1004
+ m = R_FULL_INJECT.match(val)
1005
+
1006
+ # Full string injection: "`path.ref`" or "`$CMD`"
1007
+ if m
1008
+ inj.full = true if inj
1009
+
1010
+ pathref = m[1]
1011
+ pathref = pathref.gsub('$BT', S_BT).gsub('$DS', S_DS) if pathref.length > 3
1012
+
1013
+ out = getpath(store, pathref, inj)
1014
+
1015
+ else
1016
+ # Partial string injection: "prefix`ref`suffix"
1017
+ out = val.gsub(R_PART_INJECT) do |_match|
1018
+ ref = ::Regexp.last_match(1)
1019
+ ref = ref.gsub('$BT', S_BT).gsub('$DS', S_DS) if ref.length > 3
1020
+
1021
+ inj.full = false if inj
1022
+
1023
+ found = getpath(store, ref, inj)
1024
+
1025
+ if found.nil?
1026
+ # Check if key exists in base data (nil = JSON null, vs not-found)
1027
+ base_data = _getprop(store, S_DTOP, store)
1028
+ ref_parts = ref.split(S_DT)
1029
+ exists = !_getprop(base_data, ref_parts[0], UNDEF).equal?(UNDEF)
1030
+ exists ? 'null' : S_MT
1031
+ elsif found.is_a?(String)
1032
+ found
1033
+ elsif isfunc(found)
1034
+ found
1035
+ else
1036
+ begin
1037
+ JSON.generate(found)
1038
+ rescue StandardError
1039
+ stringify(found)
1040
+ end
1041
+ end
1042
+ end
1043
+
1044
+ # Call the inj handler on the entire string for custom injection.
1045
+ if inj && isfunc(inj.handler)
1046
+ inj.full = true
1047
+ out = inj.handler.call(inj, out, val, store)
1048
+ end
1049
+ end
1050
+
1051
+ out
1052
+ end
1053
+
1054
+ # --- inject: Recursively inject store values into a node ---
1055
+ # Matches TS canonical: inject(val, store, injdef?)
1056
+ def self.inject(val, store, injdef = nil)
1057
+ # Reuse existing Injection state during recursion; otherwise create new one.
1058
+ if injdef.is_a?(Injection)
1059
+ inj = injdef
1060
+ else
1061
+ parent = { S_DTOP => val }
1062
+ inj = Injection.new(val, parent)
1063
+ inj.handler = method(:_injecthandler)
1064
+ inj.base = S_DTOP
1065
+ inj.modify = _injdef_prop(injdef, 'modify')
1066
+ inj.meta = _injdef_prop(injdef, 'meta') || {}
1067
+ inj.errs = getprop(store, S_DERRS, [])
1068
+ inj.dparent = store
1069
+ inj.dpath = [S_DTOP]
1070
+ inj.root = parent
1071
+
1072
+ h = _injdef_prop(injdef, 'handler')
1073
+ inj.handler = h if h
1074
+ dp = _injdef_prop(injdef, 'dparent')
1075
+ inj.dparent = dp if dp
1076
+ dpth = _injdef_prop(injdef, 'dpath')
1077
+ inj.dpath = dpth if dpth
1078
+ ex = _injdef_prop(injdef, 'extra')
1079
+ inj.extra = ex if ex
1080
+ end
1081
+
1082
+ inj.descend
1083
+
1084
+ # Descend into node.
1085
+ if isnode(val)
1086
+ if ismap(val)
1087
+ normal = val.keys.reject { |k| k.include?(S_DS) }.sort
1088
+ transforms = val.keys.select { |k| k.include?(S_DS) }.sort
1089
+ nodekeys = normal + transforms
1090
+ else
1091
+ nodekeys = (0...val.length).to_a
1092
+ end
1093
+
1094
+ nkI = 0
1095
+ while nkI < nodekeys.length
1096
+ childinj = inj.child(nkI, nodekeys)
1097
+ nodekey = childinj.key
1098
+ childinj.mode = S_MKEYPRE
1099
+
1100
+ prekey = _injectstr(nodekey, store, childinj)
1101
+
1102
+ nkI = childinj.keyI
1103
+ nodekeys = childinj.keys
1104
+
1105
+ unless prekey.nil?
1106
+ childinj.val = getprop(val, prekey)
1107
+ childinj.mode = S_MVAL
1108
+
1109
+ inject(childinj.val, store, childinj)
1110
+
1111
+ nkI = childinj.keyI
1112
+ nodekeys = childinj.keys
1113
+
1114
+ childinj.mode = S_MKEYPOST
1115
+ _injectstr(nodekey, store, childinj)
1116
+
1117
+ nkI = childinj.keyI
1118
+ nodekeys = childinj.keys
1119
+ end
1120
+
1121
+ nkI += 1
1122
+ end
1123
+
1124
+ elsif val.is_a?(String)
1125
+ inj.mode = S_MVAL
1126
+ val = _injectstr(val, store, inj)
1127
+ inj.setval(val) if val != SKIP
1128
+ end
1129
+
1130
+ # Custom modification.
1131
+ if inj.modify && val != SKIP
1132
+ mkey = inj.key
1133
+ mparent = inj.parent
1134
+ mval = getprop(mparent, mkey)
1135
+ inj.modify.call(mval, mkey, mparent, inj)
1136
+ end
1137
+
1138
+ inj.val = val
1139
+
1140
+ return getprop(inj.root, S_DTOP) if inj.prior.nil? && inj.root && haskey(inj.root, S_DTOP)
1141
+ return getprop(inj.parent, S_DTOP) if inj.key == S_DTOP && inj.parent && haskey(inj.parent, S_DTOP)
1142
+
1143
+ val
1144
+ end
1145
+
1146
+ # Helper to read a property from injdef (Hash or object)
1147
+ def self._injdef_prop(injdef, key)
1148
+ return nil if injdef.nil?
1149
+
1150
+ if injdef.is_a?(Hash)
1151
+ injdef[key] || injdef[key.to_sym]
1152
+ elsif injdef.respond_to?(key.to_sym)
1153
+ injdef.send(key.to_sym)
1154
+ end
1155
+ end
1156
+
1157
+ # Default inject handler
1158
+ def self._injecthandler(inj, val, ref, store)
1159
+ out = val
1160
+ iscmd = isfunc(val) && (ref.nil? || (ref.is_a?(String) && ref.start_with?(S_DS)))
1161
+
1162
+ if iscmd
1163
+ out = val.call(inj, val, ref, store)
1164
+ elsif inj.mode == S_MVAL && inj.full
1165
+ inj.setval(val)
1166
+ end
1167
+
1168
+ out
1169
+ end
1170
+
1171
+ # --- Transform commands ---
1172
+
1173
+ def self.transform_DELETE(inj, _val, _ref, _store)
1174
+ inj.setval(UNDEF)
1175
+ nil
1176
+ end
1177
+
1178
+ def self.transform_COPY(inj, _val, _ref, _store)
1179
+ mode = inj.mode
1180
+ key = inj.key
1181
+
1182
+ out = nil
1183
+ if mode.start_with?('key')
1184
+ out = key
1185
+ else
1186
+ out = if isnode(inj.dparent)
1187
+ getprop(inj.dparent, key)
1188
+ else
1189
+ inj.path.length == 2 ? nil : inj.dparent
1190
+ end
1191
+ inj.setval(out)
1192
+ end
1193
+ out
1194
+ end
1195
+
1196
+ def self.transform_KEY(inj, _val, _ref, _store)
1197
+ mode = inj.mode
1198
+ path = inj.path
1199
+ parent = inj.parent
1200
+
1201
+ return inj.key if mode == S_MKEYPRE
1202
+ return nil if mode != S_MVAL
1203
+
1204
+ keyspec = getprop(parent, S_BKEY)
1205
+ if keyspec
1206
+ delprop(parent, S_BKEY)
1207
+ return getprop(inj.dparent, keyspec)
1208
+ end
1209
+
1210
+ return getprop(inj.dparent, inj.key) if ismap(inj.dparent) && inj.key && haskey(inj.dparent, inj.key)
1211
+
1212
+ meta = getprop(parent, S_BANNO)
1213
+ getprop(meta, S_KEY, getprop(path, path.length - 2))
1214
+ end
1215
+
1216
+ def self.transform_ANNO(inj, _val, _ref, _store)
1217
+ delprop(inj.parent, S_BANNO)
1218
+ nil
1219
+ end
1220
+
1221
+ def self.transform_META(inj, _val, _ref, _store)
1222
+ delprop(inj.parent, S_DMETA)
1223
+ nil
1224
+ end
1225
+
1226
+ def self.transform_MERGE(inj, _val, _ref, _store)
1227
+ mode = inj.mode
1228
+ key = inj.key
1229
+ parent = inj.parent
1230
+
1231
+ if mode == S_MKEYPRE
1232
+ return key
1233
+ elsif mode == S_MKEYPOST
1234
+ args = getprop(parent, key)
1235
+ args = [args] unless islist(args)
1236
+ inj.setval(UNDEF)
1237
+ mergelist = [parent] + args + [clone(parent)]
1238
+ merge(mergelist)
1239
+ return key
1240
+ elsif mode == S_MVAL && islist(parent)
1241
+ return getprop(parent, inj.key) unless strkey(inj.key) == '0' && size(parent).positive?
1242
+
1243
+ parent.delete_at(0)
1244
+ return getprop(parent, 0)
1245
+
1246
+ end
1247
+
1248
+ nil
1249
+ end
1250
+
1251
+ def self.transform_EACH(inj, _val, _ref, store)
1252
+ mode = inj.mode
1253
+ keys_ = inj.keys
1254
+ path = inj.path
1255
+ parent = inj.parent
1256
+ nodes_ = inj.nodes
1257
+
1258
+ keys_&.replace(keys_[0, 1])
1259
+
1260
+ return nil if mode != S_MVAL || !path || !nodes_
1261
+
1262
+ srcpath = parent[1] if parent.length > 1
1263
+ child_template = clone(parent[2]) if parent.length > 2
1264
+
1265
+ srcstore = getprop(store, inj.base, store)
1266
+ src = getpath(srcstore, srcpath, inj)
1267
+
1268
+ tkey = getelem(path, -2)
1269
+ target = nodes_.length >= 2 ? nodes_[-2] : nodes_[-1]
1270
+
1271
+ rval = []
1272
+
1273
+ if isnode(src)
1274
+ if islist(src)
1275
+ tval = src.map { clone(child_template) }
1276
+ else
1277
+ tval = []
1278
+ src.each_key do |k|
1279
+ cc = clone(child_template)
1280
+ setprop(cc, S_BANNO, { S_KEY => k }) if ismap(cc)
1281
+ tval << cc
1282
+ end
1283
+ end
1284
+ tcurrent = ismap(src) ? src.values : src
1285
+
1286
+ if size(tval).positive?
1287
+ ckey = getelem(path, -2)
1288
+ tpath = path[0...-1]
1289
+
1290
+ dpath = [S_DTOP]
1291
+ if srcpath.is_a?(String) && !srcpath.empty?
1292
+ srcpath.split(S_DT).each { |p| dpath << p if p != S_MT }
1293
+ end
1294
+ dpath << "$:#{ckey}" if ckey
1295
+
1296
+ tcur = { ckey => tcurrent }
1297
+
1298
+ if size(tpath) > 1
1299
+ pkey = getelem(path, -3, S_DTOP)
1300
+ tcur = { pkey => tcur }
1301
+ dpath << "$:#{pkey}"
1302
+ end
1303
+
1304
+ tinj = inj.child(0, ckey ? [ckey] : [])
1305
+ tinj.path = tpath
1306
+ tinj.nodes = nodes_.length.positive? ? nodes_[0...-1] : []
1307
+ tinj.parent = getelem(tinj.nodes, -1)
1308
+ setprop(tinj.parent, ckey, tval) if ckey && tinj.parent
1309
+ tinj.val = tval
1310
+ tinj.dpath = dpath
1311
+ tinj.dparent = tcur
1312
+
1313
+ inject(tval, store, tinj)
1314
+ rval = tinj.val
1315
+ end
1316
+ end
1317
+
1318
+ setprop(target, tkey, rval)
1319
+ islist(rval) && size(rval).positive? ? rval[0] : nil
1320
+ end
1321
+
1322
+ def self.transform_PACK(inj, _val, _ref, store)
1323
+ mode = inj.mode
1324
+ key = inj.key
1325
+ path = inj.path
1326
+ parent = inj.parent
1327
+ nodes_ = inj.nodes
1328
+
1329
+ return nil if mode != S_MKEYPRE || !key.is_a?(String) || !path || !nodes_
1330
+
1331
+ args_val = getprop(parent, key)
1332
+ return nil if !islist(args_val) || size(args_val) < 2
1333
+
1334
+ srcpath = args_val[0]
1335
+ origchildspec = args_val[1]
1336
+
1337
+ tkey = getelem(path, -2)
1338
+ pathsize = size(path)
1339
+ target = getelem(nodes_, pathsize - 2, -> { getelem(nodes_, pathsize - 1) })
1340
+
1341
+ srcstore = getprop(store, inj.base, store)
1342
+ src = getpath(srcstore, srcpath, inj)
1343
+
1344
+ unless islist(src)
1345
+ if ismap(src)
1346
+ new_src = []
1347
+ items(src).each do |item|
1348
+ setprop(item[1], S_BANNO, { S_KEY => item[0] })
1349
+ new_src << item[1]
1350
+ end
1351
+ src = new_src
1352
+ else
1353
+ src = nil
1354
+ end
1355
+ end
1356
+
1357
+ return nil if src.nil?
1358
+
1359
+ keypath = getprop(origchildspec, S_BKEY)
1360
+ childspec = delprop(clone(origchildspec), S_BKEY)
1361
+ child = getprop(childspec, S_BVAL, childspec)
1362
+
1363
+ tval = {}
1364
+ items(src).each do |item|
1365
+ srckey = item[0]
1366
+ srcnode = item[1]
1367
+
1368
+ k = srckey
1369
+ if keypath
1370
+ k = if keypath.is_a?(String) && keypath.start_with?(S_BT)
1371
+ inject(keypath, merge([{}, store, { S_DTOP => srcnode }], 1))
1372
+ else
1373
+ getpath(srcnode, keypath, inj)
1374
+ end
1375
+ end
1376
+
1377
+ tchild = clone(child)
1378
+ setprop(tval, k, tchild)
1379
+
1380
+ anno = getprop(srcnode, S_BANNO)
1381
+ if anno.nil?
1382
+ delprop(tchild, S_BANNO)
1383
+ else
1384
+ setprop(tchild, S_BANNO, anno)
1385
+ end
1386
+ end
1387
+
1388
+ rval = {}
1389
+
1390
+ unless isempty(tval)
1391
+ tsrc = {}
1392
+ src.each_with_index do |n, i|
1393
+ kn = if keypath.nil?
1394
+ i
1395
+ elsif keypath.is_a?(String) && keypath.start_with?(S_BT)
1396
+ inject(keypath, merge([{}, store, { S_DTOP => n }], 1))
1397
+ else
1398
+ getpath(n, keypath, inj)
1399
+ end
1400
+ setprop(tsrc, kn, n)
1401
+ end
1402
+
1403
+ tpath = slice(inj.path, -1)
1404
+ ckey = getelem(inj.path, -2)
1405
+ dpath = flatten([S_DTOP, srcpath.to_s.split(S_DT), "$:#{ckey}"])
1406
+
1407
+ tcur = { ckey => tsrc }
1408
+ if size(tpath) > 1
1409
+ pkey = getelem(inj.path, -3, S_DTOP)
1410
+ tcur = { pkey => tcur }
1411
+ dpath << "$:#{pkey}"
1412
+ end
1413
+
1414
+ tinj = inj.child(0, [ckey])
1415
+ tinj.path = tpath
1416
+ tinj.nodes = slice(inj.nodes, -1)
1417
+ tinj.parent = getelem(tinj.nodes, -1)
1418
+ tinj.val = tval
1419
+ tinj.dpath = dpath
1420
+ tinj.dparent = tcur
1421
+
1422
+ inject(tval, store, tinj)
1423
+ rval = tinj.val
1424
+ end
1425
+
1426
+ setprop(target, tkey, rval)
1427
+ nil
1428
+ end
1429
+
1430
+ def self.transform_REF(inj, _val, _ref, store)
1431
+ nodes_ = inj.nodes
1432
+ return nil if inj.mode != S_MVAL
1433
+
1434
+ refpath = getprop(inj.parent, 1)
1435
+ inj.keyI = size(inj.keys)
1436
+
1437
+ specFn = getprop(store, S_DSPEC)
1438
+ spec = isfunc(specFn) ? specFn.call : nil
1439
+
1440
+ dpath = slice(inj.path, 1)
1441
+ ref = getpath(spec, refpath, {
1442
+ 'dpath' => dpath,
1443
+ 'dparent' => getpath(spec, dpath)
1444
+ })
1445
+
1446
+ tref = clone(ref)
1447
+
1448
+ cpath = slice(inj.path, -3)
1449
+ tpath = slice(inj.path, -1)
1450
+ tcur = getpath(store, cpath)
1451
+ tval = getpath(store, tpath)
1452
+ rval = nil
1453
+
1454
+ if tval || !isnode(ref)
1455
+ tinj = inj.child(0, [getelem(tpath, -1)])
1456
+ tinj.path = tpath
1457
+ tinj.nodes = slice(inj.nodes, -1)
1458
+ tinj.parent = getelem(nodes_, -2)
1459
+ tinj.val = tref
1460
+
1461
+ tinj.dpath = flatten([cpath])
1462
+ tinj.dparent = tcur
1463
+
1464
+ inject(tref, store, tinj)
1465
+ rval = tinj.val
1466
+ end
1467
+
1468
+ tkey = getelem(inj.path, -2)
1469
+ target = getelem(nodes_, -2, -> { getelem(nodes_, -1) })
1470
+ if rval.nil?
1471
+ delprop(target, tkey)
1472
+ else
1473
+ setprop(target, tkey, rval)
1474
+ end
1475
+
1476
+ inj.prior.keyI -= 1 if islist(target) && inj.prior
1477
+
1478
+ _val
1479
+ end
1480
+
1481
+ FORMATTER = {
1482
+ 'identity' => ->(_k, v, *_a) { v },
1483
+ 'upper' => ->(_k, v, *_a) { isnode(v) ? v : (v.nil? ? 'null' : v.to_s).upcase },
1484
+ 'lower' => ->(_k, v, *_a) { isnode(v) ? v : (v.nil? ? 'null' : v.to_s).downcase },
1485
+ 'string' => ->(_k, v, *_a) { isnode(v) ? v : (v.nil? ? 'null' : v.to_s) },
1486
+ 'number' => lambda { |_k, v, *_a|
1487
+ if isnode(v)
1488
+ v
1489
+ else
1490
+ n = begin
1491
+ Float(v)
1492
+ rescue StandardError
1493
+ 0
1494
+ end
1495
+ n
1496
+ end
1497
+ },
1498
+ 'integer' => lambda { |_k, v, *_a|
1499
+ if isnode(v)
1500
+ v
1501
+ else
1502
+ n = begin
1503
+ Integer(Float(v))
1504
+ rescue StandardError
1505
+ 0
1506
+ end
1507
+ n
1508
+ end
1509
+ },
1510
+ 'concat' => lambda { |k, v, *_a|
1511
+ if k.nil? && islist(v)
1512
+ items(v, ->(n) { isnode(n[1]) ? '' : (n[1].nil? ? 'null' : n[1].to_s) }).join
1513
+ else
1514
+ v
1515
+ end
1516
+ }
1517
+ }.freeze
1518
+
1519
+ def self.transform_FORMAT(inj, _val, _ref, store)
1520
+ slice(inj.keys, 0, 1, true)
1521
+ return nil if inj.mode != S_MVAL
1522
+
1523
+ name = getprop(inj.parent, 1)
1524
+ child = getprop(inj.parent, 2)
1525
+
1526
+ tkey = getelem(inj.path, -2)
1527
+ target = getelem(inj.nodes, -2, -> { getelem(inj.nodes, -1) })
1528
+
1529
+ cinj = injectChild(child, store, inj)
1530
+ resolved = cinj.val
1531
+
1532
+ formatter = T_function.anybits?(typify(name)) ? name : FORMATTER[name]
1533
+
1534
+ if formatter.nil?
1535
+ inj.errs << "$FORMAT: unknown format: #{name}."
1536
+ return nil
1537
+ end
1538
+
1539
+ out = walk(resolved, formatter)
1540
+ setprop(target, tkey, out)
1541
+ out
1542
+ end
1543
+
1544
+ def self.transform_APPLY(inj, _val, _ref, store)
1545
+ ijname = 'APPLY'
1546
+ return nil unless checkPlacement(M_VAL, ijname, T_list, inj)
1547
+
1548
+ args = slice(inj.parent, 1)
1549
+ args_list = islist(args) ? args : []
1550
+ err, apply, child = injectorArgs([T_function, T_any], args_list)
1551
+ if err
1552
+ inj.errs << "$#{ijname}: #{err}"
1553
+ return nil
1554
+ end
1555
+
1556
+ tkey = getelem(inj.path, -2)
1557
+ target = getelem(inj.nodes, -2, -> { getelem(inj.nodes, -1) })
1558
+
1559
+ cinj = injectChild(child, store, inj)
1560
+ resolved = cinj.val
1561
+
1562
+ out = apply.call(resolved, store, cinj)
1563
+ setprop(target, tkey, out)
1564
+ out
1565
+ end
1566
+
1567
+ def self.checkPlacement(modes, ijname, parentTypes, inj)
1568
+ mode_num = { S_MKEYPRE => M_KEYPRE, S_MKEYPOST => M_KEYPOST, S_MVAL => M_VAL }
1569
+ mode_int = mode_num[inj.mode] || 0
1570
+ if modes.nobits?(mode_int)
1571
+ expected = [M_KEYPRE, M_KEYPOST, M_VAL].reject { |m| modes.nobits?(m) }
1572
+ expected = expected.map { |m| PLACEMENT[m] }.join(',')
1573
+ inj.errs << "$#{ijname}: invalid placement as #{PLACEMENT[mode_int] || ''}, expected: #{expected}."
1574
+ return false
1575
+ end
1576
+ unless isempty(parentTypes)
1577
+ ptype = typify(inj.parent)
1578
+ if parentTypes.nobits?(ptype)
1579
+ inj.errs << "$#{ijname}: invalid placement in parent #{typename(ptype)}, expected: #{typename(parentTypes)}."
1580
+ return false
1581
+ end
1582
+ end
1583
+ true
1584
+ end
1585
+
1586
+ def self.injectorArgs(argTypes, args)
1587
+ numargs = size(argTypes)
1588
+ found = Array.new(1 + numargs)
1589
+ found[0] = nil
1590
+ (0...numargs).each do |argI|
1591
+ arg = args[argI]
1592
+ argType = typify(arg)
1593
+ if argTypes[argI].nobits?(argType)
1594
+ found[0] =
1595
+ "invalid argument: #{stringify(arg, 22)} (#{typename(argType)} at position #{1 + argI}) " \
1596
+ "is not of type: #{typename(argTypes[argI])}."
1597
+ break
1598
+ end
1599
+ found[1 + argI] = arg
1600
+ end
1601
+ found
1602
+ end
1603
+
1604
+ def self.injectChild(child, store, inj)
1605
+ cinj = inj
1606
+ if inj.prior
1607
+ if inj.prior.prior
1608
+ cinj = inj.prior.prior.child(inj.prior.keyI, inj.prior.keys)
1609
+ cinj.val = child
1610
+ setprop(cinj.parent, inj.prior.key, child)
1611
+ else
1612
+ cinj = inj.prior.child(inj.keyI, inj.keys)
1613
+ cinj.val = child
1614
+ setprop(cinj.parent, inj.key, child)
1615
+ end
1616
+ end
1617
+ inject(child, store, cinj)
1618
+ cinj
1619
+ end
1620
+
1621
+ # --- transform: Transform data using spec ---
1622
+ def self.transform(data, spec, injdef = nil)
1623
+ origspec = spec
1624
+ spec = clone(spec)
1625
+
1626
+ extra = _injdef_prop(injdef, 'extra')
1627
+ collect = !_injdef_prop(injdef, 'errs').nil?
1628
+ errs = collect ? _injdef_prop(injdef, 'errs') : []
1629
+
1630
+ extraTransforms = {}
1631
+ extraData = {}
1632
+
1633
+ if extra && isnode(extra)
1634
+ items(extra).each do |item|
1635
+ k, v = item
1636
+ if k.is_a?(String) && k.start_with?(S_DS)
1637
+ extraTransforms[k] = v
1638
+ else
1639
+ extraData[k] = v
1640
+ end
1641
+ end
1642
+ end
1643
+
1644
+ data_clone = merge([
1645
+ isempty(extraData) ? nil : clone(extraData),
1646
+ clone(data)
1647
+ ])
1648
+
1649
+ store = {
1650
+ S_DTOP => data_clone,
1651
+ S_DSPEC => -> { origspec },
1652
+ '$BT' => ->(*_a) { S_BT },
1653
+ '$DS' => ->(*_a) { S_DS },
1654
+ '$WHEN' => ->(*_a) { Time.now.iso8601 },
1655
+ '$DELETE' => method(:transform_DELETE),
1656
+ '$COPY' => method(:transform_COPY),
1657
+ '$KEY' => method(:transform_KEY),
1658
+ '$ANNO' => method(:transform_ANNO),
1659
+ '$META' => method(:transform_META),
1660
+ '$MERGE' => method(:transform_MERGE),
1661
+ '$EACH' => method(:transform_EACH),
1662
+ '$PACK' => method(:transform_PACK),
1663
+ '$REF' => method(:transform_REF),
1664
+ '$FORMAT' => method(:transform_FORMAT),
1665
+ '$APPLY' => method(:transform_APPLY)
1666
+ }
1667
+ extraTransforms.each { |k, v| store[k] = v }
1668
+ store[S_DERRS] = errs
1669
+
1670
+ injdef = {} if injdef.nil?
1671
+ injdef = {} unless injdef.is_a?(Hash)
1672
+ injdef = injdef.merge('errs' => errs)
1673
+
1674
+ out = inject(spec, store, injdef)
1675
+
1676
+ raise errs.join(' | ') if !errs.empty? && !collect
1677
+
1678
+ out
1679
+ end
1680
+
1681
+ # --- Validators ---
1682
+
1683
+ def self._invalidTypeMsg(path, needtype, vt, v, _whence = nil)
1684
+ vs = v.nil? || v.equal?(UNDEF) ? 'no value' : stringify(v)
1685
+ "Expected #{if size(path) > 1
1686
+ "field #{pathify(path,
1687
+ 1)} to be "
1688
+ end}#{needtype}, but found #{typename(vt) + S_VIZ unless v.nil? || v.equal?(UNDEF)}#{vs}."
1689
+ end
1690
+
1691
+ def self.validate_STRING(inj, _val = nil, _ref = nil, _store = nil)
1692
+ out = getprop(inj.dparent, inj.key)
1693
+ t = typify(out)
1694
+ if T_string.nobits?(t)
1695
+ inj.errs << _invalidTypeMsg(inj.path, S_string, t, out, 'V1010')
1696
+ return nil
1697
+ end
1698
+ if out == S_MT
1699
+ inj.errs << "Empty string at #{pathify(inj.path, 1)}"
1700
+ return nil
1701
+ end
1702
+ out
1703
+ end
1704
+
1705
+ TYPE_CHECKS = {
1706
+ S_number => ->(v) { v.is_a?(Numeric) && ![true, false].include?(v) },
1707
+ S_integer => ->(v) { v.is_a?(Integer) && ![true, false].include?(v) },
1708
+ S_decimal => ->(v) { v.is_a?(Float) },
1709
+ S_boolean => ->(v) { [true, false].include?(v) },
1710
+ S_null => lambda(&:nil?),
1711
+ S_nil => ->(v) { v.equal?(UNDEF) },
1712
+ S_map => ->(v) { v.is_a?(Hash) },
1713
+ S_list => ->(v) { v.is_a?(Array) },
1714
+ S_function => ->(v) { v.respond_to?(:call) },
1715
+ S_instance => lambda { |v|
1716
+ !v.is_a?(Hash) && !v.is_a?(Array) && !v.is_a?(String) &&
1717
+ !v.is_a?(Numeric) && ![true, false].include?(v) && !v.nil? && !v.equal?(UNDEF)
1718
+ }
1719
+ }.freeze
1720
+
1721
+ def self.validate_TYPE(inj, _val = nil, ref = nil, _store = nil)
1722
+ tname = ref.is_a?(String) && ref.length > 1 ? ref[1..].downcase : S_any
1723
+ idx = TYPENAME.index(tname)
1724
+ typev = idx ? (1 << (31 - idx)) : 0
1725
+ typev |= T_null if tname == S_nil
1726
+
1727
+ out = getprop(inj.dparent, inj.key)
1728
+ t = typify(out)
1729
+
1730
+ if t.nobits?(typev)
1731
+ inj.errs << _invalidTypeMsg(inj.path, tname, t, out, 'V1001')
1732
+ return nil
1733
+ end
1734
+ out
1735
+ end
1736
+
1737
+ def self.validate_ANY(inj, _val = nil, _ref = nil, _store = nil)
1738
+ getprop(inj.dparent, inj.key)
1739
+ end
1740
+
1741
+ def self.validate_CHILD(inj, _val = nil, _ref = nil, _store = nil)
1742
+ mode = inj.mode
1743
+ key = inj.key
1744
+ parent = inj.parent
1745
+ path = inj.path
1746
+ keys = inj.keys
1747
+
1748
+ if mode == S_MKEYPRE
1749
+ childtm = getprop(parent, key)
1750
+ pkey = getelem(path, -2)
1751
+ tval = getprop(inj.dparent, pkey)
1752
+
1753
+ if tval.nil?
1754
+ tval = {}
1755
+ elsif !ismap(tval)
1756
+ inj.errs << _invalidTypeMsg(path[0...-1], S_object, typify(tval), tval, 'V0220')
1757
+ return nil
1758
+ end
1759
+
1760
+ keysof(tval).each do |ckey|
1761
+ setprop(parent, ckey, clone(childtm))
1762
+ keys << ckey
1763
+ end
1764
+
1765
+ inj.setval(UNDEF)
1766
+ return nil
1767
+ end
1768
+
1769
+ if mode == S_MVAL
1770
+ unless islist(parent)
1771
+ inj.errs << 'Invalid $CHILD as value'
1772
+ return nil
1773
+ end
1774
+
1775
+ childtm = getprop(parent, 1)
1776
+
1777
+ if inj.dparent.nil?
1778
+ parent.clear
1779
+ return nil
1780
+ end
1781
+
1782
+ unless islist(inj.dparent)
1783
+ inj.errs << _invalidTypeMsg(path[0...-1], S_list, typify(inj.dparent), inj.dparent, 'V0230')
1784
+ inj.keyI = size(parent)
1785
+ return inj.dparent
1786
+ end
1787
+
1788
+ items(inj.dparent).each do |n|
1789
+ setprop(parent, n[0], clone(childtm))
1790
+ end
1791
+ parent.slice!(inj.dparent.length..-1) if parent.length > inj.dparent.length
1792
+ inj.keyI = 0
1793
+ return getprop(inj.dparent, 0)
1794
+ end
1795
+
1796
+ nil
1797
+ end
1798
+
1799
+ def self.validate_ONE(inj, _val = nil, _ref = nil, store = nil)
1800
+ mode = inj.mode
1801
+ parent = inj.parent
1802
+ keyI = inj.keyI
1803
+
1804
+ return unless mode == S_MVAL
1805
+
1806
+ if !islist(parent) || keyI != 0
1807
+ inj.errs << "The $ONE validator at field #{pathify(inj.path, 1, 1)} must be the first element of an array."
1808
+ return nil
1809
+ end
1810
+
1811
+ inj.keyI = size(inj.keys)
1812
+ inj.setval(inj.dparent, 2)
1813
+ inj.path = inj.path[0...-1]
1814
+ inj.key = getelem(inj.path, -1)
1815
+
1816
+ tvals = parent[1..]
1817
+ if size(tvals).zero?
1818
+ inj.errs << "The $ONE validator at field #{pathify(inj.path, 1, 1)} must have at least one argument."
1819
+ return nil
1820
+ end
1821
+
1822
+ tvals.each do |tval|
1823
+ terrs = []
1824
+ vstore = merge([{}, store], 1)
1825
+ vstore[S_DTOP] = inj.dparent
1826
+
1827
+ vcurrent = validate(inj.dparent, tval, {
1828
+ 'extra' => vstore,
1829
+ 'errs' => terrs,
1830
+ 'meta' => inj.meta
1831
+ })
1832
+
1833
+ inj.setval(vcurrent, -2)
1834
+ return nil if size(terrs).zero?
1835
+ end
1836
+
1837
+ valdesc = items(tvals).map { |n| stringify(n[1]) }.join(', ')
1838
+ valdesc = valdesc.gsub(/`\$([A-Z]+)`/) { ::Regexp.last_match(1).downcase }
1839
+
1840
+ inj.errs << _invalidTypeMsg(
1841
+ inj.path,
1842
+ (size(tvals) > 1 ? 'one of ' : '') + valdesc,
1843
+ typify(inj.dparent), inj.dparent, 'V0210'
1844
+ )
1845
+ end
1846
+
1847
+ def self.validate_EXACT(inj, _val = nil, _ref = nil, _store = nil)
1848
+ mode = inj.mode
1849
+ parent = inj.parent
1850
+ key = inj.key
1851
+ keyI = inj.keyI
1852
+
1853
+ if mode == S_MVAL
1854
+ if !islist(parent) || keyI != 0
1855
+ inj.errs << "The $EXACT validator at field #{pathify(inj.path, 1, 1)} must be the first element of an array."
1856
+ return nil
1857
+ end
1858
+
1859
+ inj.keyI = size(inj.keys)
1860
+ inj.setval(inj.dparent, 2)
1861
+ inj.path = inj.path[0...-1]
1862
+ inj.key = getelem(inj.path, -1)
1863
+
1864
+ tvals = parent[1..]
1865
+ if size(tvals).zero?
1866
+ inj.errs << "The $EXACT validator at field #{pathify(inj.path, 1, 1)} must have at least one argument."
1867
+ return nil
1868
+ end
1869
+
1870
+ currentstr = nil
1871
+ tvals.each do |tval|
1872
+ exactmatch = (tval == inj.dparent)
1873
+ if !exactmatch && isnode(tval)
1874
+ currentstr ||= stringify(inj.dparent)
1875
+ exactmatch = stringify(tval) == currentstr
1876
+ end
1877
+ return nil if exactmatch
1878
+ end
1879
+
1880
+ valdesc = items(tvals).map { |n| stringify(n[1]) }.join(', ')
1881
+ valdesc = valdesc.gsub(/`\$([A-Z]+)`/) { ::Regexp.last_match(1).downcase }
1882
+
1883
+ inj.errs << _invalidTypeMsg(
1884
+ inj.path,
1885
+ "#{'value ' unless size(inj.path) > 1}exactly equal to #{'one of ' unless size(tvals) == 1}#{valdesc}",
1886
+ typify(inj.dparent), inj.dparent, 'V0110'
1887
+ )
1888
+ else
1889
+ delprop(parent, key)
1890
+ end
1891
+ end
1892
+
1893
+ # --- _validation: Modify callback for validate ---
1894
+ def self._validation(pval, key, parent, inj)
1895
+ return if inj.nil?
1896
+ return if pval == SKIP
1897
+
1898
+ exact = getprop(inj.meta, S_BEXACT, false)
1899
+ cval = getprop(inj.dparent, key)
1900
+
1901
+ return if !exact && cval.nil?
1902
+
1903
+ ptype = typify(pval)
1904
+ return if T_string.anybits?(ptype) && pval.is_a?(String) && pval.include?(S_DS)
1905
+
1906
+ ctype = typify(cval)
1907
+
1908
+ if ptype != ctype && !pval.nil?
1909
+ inj.errs << _invalidTypeMsg(inj.path, typename(ptype), ctype, cval, 'V0010')
1910
+ return
1911
+ end
1912
+
1913
+ if ismap(cval)
1914
+ unless ismap(pval)
1915
+ inj.errs << _invalidTypeMsg(inj.path, typename(ptype), ctype, cval, 'V0020')
1916
+ return
1917
+ end
1918
+
1919
+ ckeys = keysof(cval)
1920
+ pkeys = keysof(pval)
1921
+
1922
+ if pkeys.length.positive? && getprop(pval, '`$OPEN`') != true
1923
+ badkeys = ckeys.reject { |ck| haskey(pval, ck) }
1924
+ if badkeys.length.positive?
1925
+ inj.errs << "Unexpected keys at field #{pathify(inj.path, 1)}#{S_VIZ}#{join(badkeys, ', ')}"
1926
+ end
1927
+ else
1928
+ merge([pval, cval])
1929
+ delprop(pval, '`$OPEN`') if isnode(pval)
1930
+ end
1931
+
1932
+ elsif islist(cval)
1933
+ inj.errs << _invalidTypeMsg(inj.path, typename(ptype), ctype, cval, 'V0030') unless islist(pval)
1934
+
1935
+ elsif exact
1936
+ # In exact mode, check key existence for nil values
1937
+ if cval.nil? && pval.nil?
1938
+ # Both nil: only match if key actually exists in data
1939
+ if ismap(inj.dparent) && !inj.dparent.key?(key.to_s)
1940
+ inj.errs << "Value at field #{pathify(inj.path, 1)}: key not present."
1941
+ end
1942
+ elsif cval != pval
1943
+ pathmsg = size(inj.path) > 1 ? "at field #{pathify(inj.path, 1)}: " : ''
1944
+ inj.errs << "Value #{pathmsg}#{cval} should equal #{pval}."
1945
+ end
1946
+
1947
+ else
1948
+ setprop(parent, key, cval)
1949
+ end
1950
+ end
1951
+
1952
+ def self._validatehandler(inj, val, ref, store)
1953
+ out = val
1954
+ m = ref.is_a?(String) ? R_META_PATH.match(ref) : nil
1955
+
1956
+ if m
1957
+ if m[2] == '='
1958
+ inj.setval([S_BEXACT, val])
1959
+ else
1960
+ inj.setval(val)
1961
+ end
1962
+ inj.keyI = -1
1963
+ out = SKIP
1964
+ else
1965
+ out = _injecthandler(inj, val, ref, store)
1966
+ end
1967
+
1968
+ out
1969
+ end
1970
+
1971
+ # --- validate: Validate data against shape spec ---
1972
+ def self.validate(data, spec, injdef = nil)
1973
+ extra = _injdef_prop(injdef, 'extra')
1974
+ collect = !_injdef_prop(injdef, 'errs').nil?
1975
+ errs = collect ? _injdef_prop(injdef, 'errs') : []
1976
+
1977
+ store = merge([
1978
+ {
1979
+ '$DELETE' => nil, '$COPY' => nil, '$KEY' => nil, '$META' => nil,
1980
+ '$MERGE' => nil, '$EACH' => nil, '$PACK' => nil,
1981
+
1982
+ '$STRING' => method(:validate_STRING),
1983
+ '$NUMBER' => method(:validate_TYPE),
1984
+ '$INTEGER' => method(:validate_TYPE),
1985
+ '$DECIMAL' => method(:validate_TYPE),
1986
+ '$BOOLEAN' => method(:validate_TYPE),
1987
+ '$NULL' => method(:validate_TYPE),
1988
+ '$NIL' => method(:validate_TYPE),
1989
+ '$MAP' => method(:validate_TYPE),
1990
+ '$LIST' => method(:validate_TYPE),
1991
+ '$FUNCTION' => method(:validate_TYPE),
1992
+ '$INSTANCE' => method(:validate_TYPE),
1993
+ '$ANY' => method(:validate_ANY),
1994
+ '$CHILD' => method(:validate_CHILD),
1995
+ '$ONE' => method(:validate_ONE),
1996
+ '$EXACT' => method(:validate_EXACT)
1997
+ },
1998
+ (extra.nil? ? {} : extra),
1999
+ { S_DERRS => errs }
2000
+ ], 1)
2001
+
2002
+ meta = _injdef_prop(injdef, 'meta') || {}
2003
+ setprop(meta, S_BEXACT, getprop(meta, S_BEXACT, false)) if ismap(meta)
2004
+
2005
+ out = transform(data, spec, {
2006
+ 'meta' => meta,
2007
+ 'extra' => store,
2008
+ 'modify' => method(:_validation),
2009
+ 'handler' => method(:_validatehandler),
2010
+ 'errs' => errs
2011
+ })
2012
+
2013
+ raise errs.join(' | ') if !errs.empty? && !collect
2014
+
2015
+ out
2016
+ end
2017
+
2018
+ # --- Select operators ---
2019
+
2020
+ def self.select_AND(inj, _val, _ref, store)
2021
+ if inj.mode == S_MKEYPRE
2022
+ terms = getprop(inj.parent, inj.key)
2023
+ ppath = slice(inj.path, -1)
2024
+ point = getpath(store, ppath)
2025
+
2026
+ vstore = merge([{}, store], 1)
2027
+ vstore[S_DTOP] = point
2028
+
2029
+ terms.each do |term|
2030
+ terrs = []
2031
+ validate(point, term, {
2032
+ 'extra' => vstore,
2033
+ 'errs' => terrs,
2034
+ 'meta' => inj.meta
2035
+ })
2036
+ inj.errs << "AND:#{pathify(ppath)}\u2A2F#{stringify(point)} fail:#{stringify(terms)}" unless terrs.empty?
2037
+ end
2038
+
2039
+ gkey = getelem(inj.path, -2)
2040
+ gp = getelem(inj.nodes, -2)
2041
+ setprop(gp, gkey, point)
2042
+ end
2043
+ nil
2044
+ end
2045
+
2046
+ def self.select_OR(inj, _val, _ref, store)
2047
+ if inj.mode == S_MKEYPRE
2048
+ terms = getprop(inj.parent, inj.key)
2049
+ ppath = slice(inj.path, -1)
2050
+ point = getpath(store, ppath)
2051
+
2052
+ vstore = merge([{}, store], 1)
2053
+ vstore[S_DTOP] = point
2054
+
2055
+ terms.each do |term|
2056
+ terrs = []
2057
+ validate(point, term, {
2058
+ 'extra' => vstore,
2059
+ 'errs' => terrs,
2060
+ 'meta' => inj.meta
2061
+ })
2062
+ next unless terrs.empty?
2063
+
2064
+ gkey = getelem(inj.path, -2)
2065
+ gp = getelem(inj.nodes, -2)
2066
+ setprop(gp, gkey, point)
2067
+ return nil
2068
+ end
2069
+
2070
+ inj.errs << "OR:#{pathify(ppath)}\u2A2F#{stringify(point)} fail:#{stringify(terms)}"
2071
+ end
2072
+ nil
2073
+ end
2074
+
2075
+ def self.select_NOT(inj, _val, _ref, store)
2076
+ if inj.mode == S_MKEYPRE
2077
+ term = getprop(inj.parent, inj.key)
2078
+ ppath = slice(inj.path, -1)
2079
+ point = getpath(store, ppath)
2080
+
2081
+ vstore = merge([{}, store], 1)
2082
+ vstore[S_DTOP] = point
2083
+
2084
+ terrs = []
2085
+ validate(point, term, {
2086
+ 'extra' => vstore,
2087
+ 'errs' => terrs,
2088
+ 'meta' => inj.meta
2089
+ })
2090
+
2091
+ inj.errs << "NOT:#{pathify(ppath)}\u2A2F#{stringify(point)} fail:#{stringify(term)}" if terrs.empty?
2092
+
2093
+ gkey = getelem(inj.path, -2)
2094
+ gp = getelem(inj.nodes, -2)
2095
+ setprop(gp, gkey, point)
2096
+ end
2097
+ nil
2098
+ end
2099
+
2100
+ def self.select_CMP(inj, _val, ref, store)
2101
+ if inj.mode == S_MKEYPRE
2102
+ term = getprop(inj.parent, inj.key)
2103
+ gkey = getelem(inj.path, -2)
2104
+ ppath = slice(inj.path, -1)
2105
+ point = getpath(store, ppath)
2106
+
2107
+ pass_test = false
2108
+
2109
+ begin
2110
+ if ref == '$GT' && point > term
2111
+ pass_test = true
2112
+ elsif ref == '$LT' && point < term
2113
+ pass_test = true
2114
+ elsif ref == '$GTE' && point >= term
2115
+ pass_test = true
2116
+ elsif ref == '$LTE' && point <= term
2117
+ pass_test = true
2118
+ elsif ref == '$LIKE'
2119
+ pass_test = true if stringify(point).match?(Regexp.new(term.to_s))
2120
+ end
2121
+ rescue StandardError
2122
+ end
2123
+
2124
+ if pass_test
2125
+ gp = getelem(inj.nodes, -2)
2126
+ setprop(gp, gkey, point)
2127
+ else
2128
+ inj.errs << "CMP: #{pathify(ppath)}\u2A2F#{stringify(point)} fail:#{ref} #{stringify(term)}"
2129
+ end
2130
+ end
2131
+ nil
2132
+ end
2133
+
2134
+ # --- select: Select children matching query ---
2135
+ def self.select(children, query)
2136
+ return [] unless isnode(children)
2137
+
2138
+ children = if ismap(children)
2139
+ items(children).map do |item|
2140
+ v = item[1]
2141
+ setprop(v, '$KEY', item[0]) if ismap(v)
2142
+ v
2143
+ end
2144
+ else
2145
+ children.each_with_index.map do |n, i|
2146
+ setprop(n, '$KEY', i) if ismap(n)
2147
+ n
2148
+ end
2149
+ end
2150
+
2151
+ results = []
2152
+ q = clone(query)
2153
+
2154
+ # Add $OPEN to all maps in query
2155
+ walk(q, lambda { |_k, v, _p, _t|
2156
+ setprop(v, '`$OPEN`', getprop(v, '`$OPEN`', true)) if ismap(v)
2157
+ v
2158
+ })
2159
+
2160
+ select_extra = {
2161
+ '$AND' => method(:select_AND),
2162
+ '$OR' => method(:select_OR),
2163
+ '$NOT' => method(:select_NOT),
2164
+ '$GT' => method(:select_CMP),
2165
+ '$LT' => method(:select_CMP),
2166
+ '$GTE' => method(:select_CMP),
2167
+ '$LTE' => method(:select_CMP),
2168
+ '$LIKE' => method(:select_CMP)
2169
+ }
2170
+
2171
+ children.each do |child|
2172
+ terrs = []
2173
+ validate(child, clone(q), {
2174
+ 'errs' => terrs,
2175
+ 'meta' => { S_BEXACT => true },
2176
+ 'extra' => select_extra
2177
+ })
2178
+ results << child if terrs.empty?
2179
+ end
2180
+
2181
+ results
2182
+ end
2183
+
2184
+ # --- setpath ---
2185
+ def self.setpath(store, path, val, injdef = nil)
2186
+ pt = typify(path)
2187
+ if T_list.anybits?(pt)
2188
+ parts = path
2189
+ elsif T_string.anybits?(pt)
2190
+ parts = path.split(S_DT)
2191
+ elsif T_number.anybits?(pt)
2192
+ parts = [path]
2193
+ else
2194
+ return nil
2195
+ end
2196
+
2197
+ base = _injdef_prop(injdef, 'base')
2198
+ numparts = size(parts)
2199
+ parent = base ? getprop(store, base, store) : store
2200
+
2201
+ (0...(numparts - 1)).each do |pI|
2202
+ part_key = getelem(parts, pI)
2203
+ next_parent = getprop(parent, part_key)
2204
+ unless isnode(next_parent)
2205
+ next_part = getelem(parts, pI + 1)
2206
+ next_parent = T_number.anybits?(typify(next_part)) ? [] : {}
2207
+ setprop(parent, part_key, next_parent)
2208
+ end
2209
+ parent = next_parent
2210
+ end
2211
+
2212
+ if val == DELETE
2213
+ delprop(parent, getelem(parts, -1))
2214
+ else
2215
+ setprop(parent, getelem(parts, -1), val)
2216
+ end
2217
+
2218
+ parent
2219
+ end
2220
+
2221
+ # --- Injection class ---
2222
+ class Injection
2223
+ attr_accessor :mode, :full, :keyI, :keys, :key, :val, :parent,
2224
+ :path, :nodes, :handler, :errs, :meta, :base,
2225
+ :modify, :extra, :prior, :dparent, :dpath, :root
2226
+
2227
+ def initialize(val, parent)
2228
+ @mode = VoxgigStruct::S_MVAL
2229
+ @full = false
2230
+ @keyI = 0
2231
+ @keys = [VoxgigStruct::S_DTOP]
2232
+ @key = VoxgigStruct::S_DTOP
2233
+ @val = val
2234
+ @parent = parent
2235
+ @path = [VoxgigStruct::S_DTOP]
2236
+ @nodes = [parent]
2237
+ @handler = nil
2238
+ @errs = []
2239
+ @meta = {}
2240
+ @base = nil
2241
+ @modify = nil
2242
+ @extra = nil
2243
+ @prior = nil
2244
+ @dparent = nil
2245
+ @dpath = [VoxgigStruct::S_DTOP]
2246
+ @root = nil
2247
+ end
2248
+
2249
+ def descend
2250
+ @meta['__d'] = (@meta['__d'] || 0) + 1
2251
+
2252
+ parentkey = VoxgigStruct.getelem(@path, -2)
2253
+
2254
+ if @dparent.nil?
2255
+ @dpath += [parentkey] if VoxgigStruct.size(@dpath) > 1
2256
+ elsif parentkey
2257
+ @dparent = VoxgigStruct.getprop(@dparent, parentkey)
2258
+ lastpart = VoxgigStruct.getelem(@dpath, -1)
2259
+ @dpath = if lastpart == "$:#{parentkey}"
2260
+ VoxgigStruct.slice(@dpath, -1)
2261
+ else
2262
+ @dpath + [parentkey]
2263
+ end
2264
+ end
2265
+
2266
+ @dparent
2267
+ end
2268
+
2269
+ def child(keyI, keys)
2270
+ key = VoxgigStruct.strkey(keys[keyI])
2271
+ val = @val
2272
+
2273
+ cinj = Injection.new(VoxgigStruct.getprop(val, key), val)
2274
+ cinj.mode = @mode
2275
+ cinj.full = @full
2276
+ cinj.keyI = keyI
2277
+ cinj.keys = keys
2278
+ cinj.key = key
2279
+ cinj.path = @path + [key]
2280
+ cinj.nodes = @nodes + [val]
2281
+ cinj.handler = @handler
2282
+ cinj.errs = @errs
2283
+ cinj.meta = @meta
2284
+ cinj.base = @base
2285
+ cinj.modify = @modify
2286
+ cinj.prior = self
2287
+ cinj.dpath = @dpath.dup
2288
+ cinj.dparent = @dparent
2289
+ cinj.extra = @extra
2290
+ cinj.root = @root
2291
+
2292
+ cinj
2293
+ end
2294
+
2295
+ def setval(val, ancestor = nil)
2296
+ # Mirrors the canonical TS Injection.setval: UNDEF (sentinel) and
2297
+ # nil (Ruby collapses both onto the same "no value" slot) delete
2298
+ # the slot at every ancestor level; any other value sets it. The
2299
+ # delete-on-undef shortcut is used by injectors (transform_DELETE,
2300
+ # transform_MERGE, validate_CHILD) to signal "drop this slot" via
2301
+ # their return value rather than calling delprop explicitly.
2302
+ if ancestor.nil? || (ancestor.is_a?(Numeric) && ancestor < 2)
2303
+ target = @parent
2304
+ key = @key
2305
+ else
2306
+ target = VoxgigStruct.getelem(@nodes, 0 - ancestor)
2307
+ key = VoxgigStruct.getelem(@path, 0 - ancestor)
2308
+ end
2309
+
2310
+ if val.nil? || val.equal?(VoxgigStruct::UNDEF)
2311
+ VoxgigStruct.delprop(target, key)
2312
+ else
2313
+ VoxgigStruct.setprop(target, key, val)
2314
+ end
2315
+ end
2316
+
2317
+ def to_s(prefix = nil)
2318
+ "INJ#{"/#{prefix}" if prefix}:#{VoxgigStruct.pad(VoxgigStruct.pathify(@path, 1))}" \
2319
+ "#{VoxgigStruct::MODENAME[VoxgigStruct::M_VAL] || ''}#{'/full' if @full}:key=#{@keyI}/#{@key}"
2320
+ end
2321
+ end
2322
+ end