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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ac352065c6d43bd84bf62fd687157226920a7dcece0c2671af82e79bed18e44
4
+ data.tar.gz: 46376fbb7414d3fc9d98ca4717f2c698cf7657da635e58d5178e00199769a215
5
+ SHA512:
6
+ metadata.gz: 3fadf7a2e1450899adee35501d6d75b51b1d4d33439d762e019d4f05a2e6e3595ed0980d80a69305d434f7ead1ed754dff2055aaa59cc24e17efe08df1506ec4
7
+ data.tar.gz: 27c70e26883dbe8efc183bf018dc533875b4a1cc48d0c7ba4a662e9d907e782b795d27e968586fa791f3505d9c7b225551dfcaa2e523fc6c976d31077c03be92
data/README.md ADDED
@@ -0,0 +1,662 @@
1
+ # Struct for Ruby
2
+
3
+ > Full-parity Ruby port of the canonical TypeScript implementation.
4
+
5
+ For motivation, language-neutral concepts, and the cross-language
6
+ parity matrix, see the [top-level README](../README.md) and
7
+ [REPORT.md](../design/REPORT.md).
8
+
9
+
10
+ ## Install
11
+
12
+ In the monorepo:
13
+
14
+ ```bash
15
+ cd ruby
16
+ bundle install
17
+ ```
18
+
19
+ The library is a single file: [`voxgig_struct.rb`](./voxgig_struct.rb).
20
+ Module: `VoxgigStruct`.
21
+
22
+ ```ruby
23
+ require_relative 'voxgig_struct'
24
+ ```
25
+
26
+
27
+ ## Quick start
28
+
29
+ ```ruby
30
+ require_relative 'voxgig_struct'
31
+
32
+ store = {
33
+ 'db' => { 'host' => 'localhost' },
34
+ 'user' => { 'first' => 'Ada', 'last' => 'Lovelace' },
35
+ 'age' => 36,
36
+ }
37
+
38
+ puts VoxgigStruct.getpath(store, 'db.host')
39
+ # localhost
40
+
41
+ puts VoxgigStruct.transform(store, {
42
+ 'name' => '`user.first`',
43
+ 'surname' => '`user.last`',
44
+ 'years' => '`age`',
45
+ }).inspect
46
+ # {"name"=>"Ada", "surname"=>"Lovelace", "years"=>36}
47
+
48
+ VoxgigStruct.validate(store, {
49
+ 'user' => {
50
+ 'first' => '`$STRING`',
51
+ 'last' => '`$STRING`',
52
+ },
53
+ 'age' => '`$INTEGER`',
54
+ })
55
+ ```
56
+
57
+
58
+ ## Function reference
59
+
60
+ Source: [`voxgig_struct.rb`](./voxgig_struct.rb). Module
61
+ `VoxgigStruct`.
62
+
63
+ ### Predicates
64
+
65
+ ```ruby
66
+ VoxgigStruct.isnode(val) # bool — map or list
67
+ VoxgigStruct.ismap(val) # bool — Hash
68
+ VoxgigStruct.islist(val) # bool — Array
69
+ VoxgigStruct.iskey(key) # bool — non-empty String or Integer
70
+ VoxgigStruct.isempty(val) # bool
71
+ VoxgigStruct.isfunc(val) # bool — Proc/lambda
72
+ ```
73
+
74
+ <!-- example: minor/isnode#map -->
75
+ ```ruby
76
+ VoxgigStruct.isnode({'a' => 1}) # true
77
+ ```
78
+ <!-- => true -->
79
+
80
+ ```ruby
81
+ VoxgigStruct.isnode([1]) # true
82
+ ```
83
+
84
+ <!-- example: minor/ismap#map -->
85
+ ```ruby
86
+ VoxgigStruct.ismap({'a' => 1}) # true
87
+ ```
88
+
89
+ <!-- => true -->
90
+
91
+ ```ruby
92
+ VoxgigStruct.ismap([]) # false
93
+ ```
94
+
95
+ <!-- example: minor/islist#list -->
96
+ ```ruby
97
+ VoxgigStruct.islist([1, 2]) # true
98
+ ```
99
+
100
+ <!-- => true -->
101
+
102
+ <!-- example: minor/iskey#str -->
103
+ ```ruby
104
+ VoxgigStruct.iskey('name') # true
105
+ ```
106
+
107
+ <!-- => true -->
108
+
109
+ <!-- example: minor/isempty#empty -->
110
+ ```ruby
111
+ VoxgigStruct.isempty([]) # true
112
+ ```
113
+
114
+ <!-- => true -->
115
+
116
+ ```ruby
117
+ VoxgigStruct.isempty(nil) # true
118
+ VoxgigStruct.isfunc(->(x) { x }) # true
119
+ ```
120
+
121
+ ### Type inspection
122
+
123
+ ```ruby
124
+ VoxgigStruct.typify(value) -> Integer # bit-field
125
+ VoxgigStruct.typename(t) -> String # human name
126
+ ```
127
+
128
+ <!-- example: minor/typify#int -->
129
+ ```ruby
130
+ VoxgigStruct.typify(1) # T_scalar | T_number | T_integer (201326720)
131
+ ```
132
+
133
+ <!-- => 201326720 -->
134
+
135
+ ```ruby
136
+ VoxgigStruct.typify(42) # T_scalar | T_number | T_integer
137
+ VoxgigStruct.typify('hi') # T_scalar | T_string
138
+ VoxgigStruct.typify(nil) # T_scalar | T_null
139
+ ```
140
+
141
+ <!-- example: minor/typename#map -->
142
+ ```ruby
143
+ VoxgigStruct.typename(8192) # 'map' (8192 == T_map)
144
+ ```
145
+
146
+ <!-- => "map" -->
147
+
148
+ ```ruby
149
+ VoxgigStruct.typename(VoxgigStruct.typify('hi')) # 'string'
150
+ ```
151
+
152
+ ### Size, slice, pad
153
+
154
+ ```ruby
155
+ VoxgigStruct.size(val) -> Integer
156
+ VoxgigStruct.slice(val, start = nil, finish = nil, mutate = false)
157
+ VoxgigStruct.pad(str, padding = nil, padchar = nil) -> String
158
+ ```
159
+
160
+ <!-- example: minor/size#three -->
161
+ ```ruby
162
+ VoxgigStruct.size([1, 2, 3]) # 3
163
+ ```
164
+ <!-- => 3 -->
165
+
166
+ `slice` keeps the first *N*; a negative `start` drops the last *|start|*
167
+ items, and `finish` is exclusive:
168
+
169
+ <!-- example: minor/slice#mid -->
170
+ ```ruby
171
+ VoxgigStruct.slice([1, 2, 3, 4, 5], 1, 4) # [2, 3, 4]
172
+ ```
173
+ <!-- => [2, 3, 4] -->
174
+
175
+ <!-- example: minor/slice#strhead -->
176
+ ```ruby
177
+ VoxgigStruct.slice('abcdef', -3) # 'abc' (drops the last 3)
178
+ ```
179
+ <!-- => "abc" -->
180
+
181
+ <!-- example: minor/pad#right -->
182
+ ```ruby
183
+ VoxgigStruct.pad('a', 3) # 'a '
184
+ ```
185
+ <!-- => "a " -->
186
+
187
+ ```ruby
188
+ VoxgigStruct.pad('hi', 5) # 'hi '
189
+ VoxgigStruct.pad('hi', -5, '*') # '***hi'
190
+ ```
191
+
192
+ ### Property access
193
+
194
+ ```ruby
195
+ VoxgigStruct.getprop(val, key, alt = UNDEF)
196
+ VoxgigStruct.setprop(parent, key, val)
197
+ VoxgigStruct.delprop(parent, key)
198
+ VoxgigStruct.getelem(val, key, alt = UNDEF)
199
+ VoxgigStruct.getdef(val, alt)
200
+ VoxgigStruct.haskey(val, key) -> bool
201
+ VoxgigStruct.keysof(val) -> Array
202
+ VoxgigStruct.items(val) -> Array
203
+ VoxgigStruct.strkey(key) -> String
204
+ ```
205
+
206
+ <!-- example: minor/getprop#hit -->
207
+ ```ruby
208
+ VoxgigStruct.getprop({'x' => 1}, 'x') # 1
209
+ ```
210
+ <!-- => 1 -->
211
+
212
+ ```ruby
213
+ VoxgigStruct.getprop({}, 'b', 'fallback') # 'fallback'
214
+ ```
215
+
216
+ <!-- example: minor/setprop#set -->
217
+ ```ruby
218
+ VoxgigStruct.setprop({'a' => 1}, 'b', 2) # {'a'=>1, 'b'=>2}
219
+ ```
220
+
221
+ <!-- => {"a": 1, "b": 2} -->
222
+
223
+ <!-- example: minor/delprop#del -->
224
+ ```ruby
225
+ VoxgigStruct.delprop({'a' => 1, 'b' => 2}, 'a') # {'b'=>2}
226
+ ```
227
+
228
+ <!-- => {"b": 2} -->
229
+
230
+ <!-- example: minor/getelem#neg -->
231
+ ```ruby
232
+ VoxgigStruct.getelem([10, 20, 30], -1) # 30
233
+ ```
234
+
235
+ <!-- => 30 -->
236
+
237
+ <!-- example: minor/haskey#hit -->
238
+ ```ruby
239
+ VoxgigStruct.haskey({'a' => 1}, 'a') # true
240
+ ```
241
+
242
+ <!-- => true -->
243
+
244
+ <!-- example: minor/items#map -->
245
+ ```ruby
246
+ VoxgigStruct.items({'a' => 1, 'b' => 2}) # [['a', 1], ['b', 2]]
247
+ ```
248
+
249
+ <!-- => [["a", 1], ["b", 2]] -->
250
+
251
+ <!-- example: minor/strkey#num -->
252
+ ```ruby
253
+ VoxgigStruct.strkey(2.2) # '2'
254
+ ```
255
+
256
+ <!-- => "2" -->
257
+
258
+ ```ruby
259
+ VoxgigStruct.strkey(1) # '1'
260
+ VoxgigStruct.strkey('foo') # 'foo'
261
+ ```
262
+
263
+ <!-- example: minor/keysof#sorted -->
264
+ ```ruby
265
+ VoxgigStruct.keysof({'b' => 4, 'a' => 5}) # ['a', 'b'] (sorted)
266
+ ```
267
+ <!-- => ["a", "b"] -->
268
+
269
+ ### Path operations
270
+
271
+ ```ruby
272
+ VoxgigStruct.getpath(store, path, injdef = nil)
273
+ VoxgigStruct.setpath(store, path, val, injdef = nil)
274
+ VoxgigStruct.pathify(val, startin = nil, endin = nil) -> String
275
+ ```
276
+
277
+ <!-- example: getpath/basic#deep -->
278
+ ```ruby
279
+ VoxgigStruct.getpath({'a' => {'b' => {'c' => 42}}}, 'a.b.c') # 42
280
+ ```
281
+ <!-- => 42 -->
282
+
283
+ ```ruby
284
+ VoxgigStruct.getpath({'a' => [10, 20]}, 'a.1') # 20
285
+
286
+ store = {}
287
+ VoxgigStruct.setpath(store, 'db.host', 'localhost')
288
+ # store == {'db' => {'host' => 'localhost'}}
289
+ ```
290
+
291
+ <!-- example: minor/setpath#nested -->
292
+ ```ruby
293
+ VoxgigStruct.setpath({'a' => 1, 'b' => 2}, 'b', 22) # {'a'=>1, 'b'=>22}
294
+ ```
295
+
296
+ <!-- => {"a": 1, "b": 22} -->
297
+
298
+ <!-- example: minor/pathify#parts -->
299
+ ```ruby
300
+ VoxgigStruct.pathify(['a', 'b', 'c']) # 'a.b.c'
301
+ ```
302
+
303
+ <!-- => "a.b.c" -->
304
+
305
+ ### Tree operations
306
+
307
+ ```ruby
308
+ VoxgigStruct.walk(val, before = nil, after = nil, maxdepth = nil)
309
+ VoxgigStruct.merge(val, maxdepth = nil)
310
+ VoxgigStruct.clone(val)
311
+ VoxgigStruct.flatten(list, depth = nil)
312
+ VoxgigStruct.filter(val, check)
313
+ ```
314
+
315
+ ```ruby
316
+ after = ->(key, val, parent, path) { val.nil? ? 'DEFAULT' : val }
317
+ VoxgigStruct.walk(tree, nil, after)
318
+ ```
319
+
320
+ Last input wins; maps deep-merge; lists merge by index:
321
+
322
+ <!-- example: merge#basic -->
323
+ ```ruby
324
+ VoxgigStruct.merge([
325
+ { 'a' => 1, 'b' => 2, 'k' => [10, 20], 'x' => { 'y' => 5, 'z' => 6 } },
326
+ { 'b' => 3, 'd' => 4, 'e' => 8, 'k' => [11], 'x' => { 'y' => 7 } },
327
+ ])
328
+ # { 'a' => 1, 'b' => 3, 'd' => 4, 'e' => 8, 'k' => [11, 20], 'x' => { 'y' => 7, 'z' => 6 } }
329
+ ```
330
+
331
+ <!-- => {"a": 1, "b": 3, "d": 4, "e": 8, "k": [11, 20], "x": {"y": 7, "z": 6}} -->
332
+
333
+ <!-- example: minor/clone#deep -->
334
+ ```ruby
335
+ VoxgigStruct.clone({ 'a' => { 'b' => [1, 2] } }) # { 'a' => { 'b' => [1, 2] } } (a deep copy)
336
+ ```
337
+
338
+ <!-- => {"a": {"b": [1, 2]}} -->
339
+
340
+ <!-- example: minor/flatten#nested -->
341
+ ```ruby
342
+ VoxgigStruct.flatten([1, [2, [3]]]) # [1, 2, [3]] (one level by default)
343
+ ```
344
+
345
+ <!-- => [1, 2, [3]] -->
346
+
347
+ ```ruby
348
+ VoxgigStruct.flatten([1, [2, [3, [4]]]]) # [1, 2, [3, [4]]]
349
+ ```
350
+
351
+ `filter` passes each `[key, value]` pair to the check and returns the
352
+ matching **values** (not the pairs):
353
+
354
+ <!-- example: minor/filter#gt3 -->
355
+ ```ruby
356
+ VoxgigStruct.filter([1, 2, 3, 4, 5], ->(kv) { kv[1] > 3 })
357
+ # [4, 5]
358
+ ```
359
+ <!-- => [4, 5] -->
360
+
361
+ ### String / URL / JSON
362
+
363
+ ```ruby
364
+ VoxgigStruct.escre(s) -> String
365
+ VoxgigStruct.escurl(s) -> String
366
+ VoxgigStruct.join(arr, sep = nil, url = nil) -> String
367
+ VoxgigStruct.joinurl(parts) -> String
368
+ VoxgigStruct.jsonify(val, flags = nil) -> String
369
+ VoxgigStruct.stringify(val, maxlen = nil, pretty = nil) -> String
370
+ VoxgigStruct.replace(s, from, to) -> String
371
+ ```
372
+
373
+ <!-- example: minor/escre#dots -->
374
+ ```ruby
375
+ VoxgigStruct.escre('a.b+c') # 'a\\.b\\+c'
376
+ ```
377
+
378
+ <!-- => "a\\.b\\+c" -->
379
+
380
+ <!-- example: minor/escurl#space -->
381
+ ```ruby
382
+ VoxgigStruct.escurl('hello world?') # 'hello%20world%3F'
383
+ ```
384
+
385
+ <!-- => "hello%20world%3F" -->
386
+
387
+ <!-- example: minor/join#sep -->
388
+ ```ruby
389
+ VoxgigStruct.join(['a', 'b', 'c'], '/') # 'a/b/c'
390
+ ```
391
+
392
+ <!-- => "a/b/c" -->
393
+
394
+ `jsonify` pretty-prints by default (indent 2); pass `{ 'indent' => 0 }` for
395
+ the compact form:
396
+
397
+ <!-- example: minor/jsonify#map -->
398
+ ```ruby
399
+ VoxgigStruct.jsonify({'a' => 1})
400
+ # {
401
+ # "a": 1
402
+ # }
403
+ ```
404
+ <!-- => "{\n \"a\": 1\n}" -->
405
+
406
+ <!-- example: minor/jsonify#compact -->
407
+ ```ruby
408
+ VoxgigStruct.jsonify({'a' => 1, 'b' => 2}, { 'indent' => 0 }) # '{"a":1,"b":2}'
409
+ ```
410
+ <!-- => "{\"a\":1,\"b\":2}" -->
411
+
412
+ `stringify` is the compact, quote-light form — keys are sorted and object
413
+ braces are kept; the second argument caps the length (the `...` counts):
414
+
415
+ <!-- example: minor/stringify#brace -->
416
+ ```ruby
417
+ VoxgigStruct.stringify({'a' => 1, 'b' => [2, 3]}) # '{a:1,b:[2,3]}'
418
+ ```
419
+ <!-- => "{a:1,b:[2,3]}" -->
420
+
421
+ <!-- example: minor/stringify#max -->
422
+ ```ruby
423
+ VoxgigStruct.stringify('verylongstring', 5) # 've...'
424
+ ```
425
+ <!-- => "ve..." -->
426
+
427
+ ### Inject / transform / validate / select
428
+
429
+ ```ruby
430
+ VoxgigStruct.inject(val, store, injdef = nil)
431
+ VoxgigStruct.transform(data, spec, injdef = nil)
432
+ VoxgigStruct.validate(data, spec, injdef = nil)
433
+ VoxgigStruct.select(children, query) -> Array
434
+ ```
435
+
436
+ <!-- example: inject#basic -->
437
+ ```ruby
438
+ # Backtick refs in strings are replaced by store values.
439
+ VoxgigStruct.inject({ 'x' => '`a`', 'y' => 2 }, { 'a' => 1 }) # { 'x' => 1, 'y' => 2 }
440
+ ```
441
+
442
+ <!-- => {"x": 1, "y": 2} -->
443
+
444
+ ```ruby
445
+ VoxgigStruct.inject(
446
+ { 'greeting' => 'hello `name`' },
447
+ { 'name' => 'Ada' }
448
+ )
449
+ # { 'greeting' => 'hello Ada' }
450
+
451
+ VoxgigStruct.transform(
452
+ { 'hold' => { 'x' => 1 }, 'top' => 99 },
453
+ { 'a' => '`hold.x`', 'b' => '`top`' }
454
+ )
455
+ # { 'a' => 1, 'b' => 99 }
456
+ ```
457
+
458
+ Transform commands drive structural ops. A command like `$EACH` appears in
459
+ **value** position — as the first element of a list
460
+ `['`$EACH`', path, subspec]` — mapping the sub-spec over every entry at
461
+ `path`:
462
+
463
+ <!-- example: transform/each#basic -->
464
+ ```ruby
465
+ VoxgigStruct.transform(
466
+ { 'v' => 1, 'a' => [{ 'q' => 13 }, { 'q' => 23 }] },
467
+ { 'x' => { 'y' => ['`$EACH`', 'a', { 'q' => '`$COPY`', 'r' => '`.q`', 'p' => '`...v`' }] } }
468
+ )
469
+ # { 'x' => { 'y' => [{ 'q' => 13, 'r' => 13, 'p' => 1 }, { 'q' => 23, 'r' => 23, 'p' => 1 }] } }
470
+ ```
471
+ <!-- => {"x": {"y": [{"q": 13, "r": 13, "p": 1}, {"q": 23, "r": 23, "p": 1}]}} -->
472
+
473
+ Putting a command in **key** position (or, for `$APPLY`, directly under a
474
+ map) is an error — commands must be list values:
475
+
476
+ <!-- example: transform/apply#badkey -->
477
+ ```ruby
478
+ VoxgigStruct.transform({}, { 'x' => '`$APPLY`' })
479
+ # raises: $APPLY: invalid placement in parent map, expected: list.
480
+ ```
481
+ <!-- throws: invalid placement in parent map -->
482
+
483
+ <!-- example: validate#shape -->
484
+ ```ruby
485
+ # Validate against a shape (raises on mismatch).
486
+ VoxgigStruct.validate(
487
+ { 'name' => 'Ada', 'age' => 36 },
488
+ { 'name' => '`$STRING`', 'age' => '`$INTEGER`' }
489
+ )
490
+ # { 'name' => 'Ada', 'age' => 36 }
491
+ ```
492
+
493
+ <!-- => {"name": "Ada", "age": 36} -->
494
+
495
+ <!-- example: select#query -->
496
+ ```ruby
497
+ # Find children matching a query.
498
+ VoxgigStruct.select(
499
+ { 'a' => { 'name' => 'Alice', 'age' => 30 }, 'b' => { 'name' => 'Bob', 'age' => 25 } },
500
+ { 'age' => 30 }
501
+ )
502
+ # [{ 'name' => 'Alice', 'age' => 30, '$KEY' => 'a' }]
503
+ ```
504
+
505
+ <!-- => [{"name": "Alice", "age": 30, "$KEY": "a"}] -->
506
+
507
+ ### Builders
508
+
509
+ ```ruby
510
+ VoxgigStruct.jm(*kv) -> Hash
511
+ VoxgigStruct.jt(*v) -> Array
512
+ ```
513
+
514
+ ```ruby
515
+ VoxgigStruct.jm('a', 1, 'b', 2) # {'a' => 1, 'b' => 2}
516
+ VoxgigStruct.jt(1, 2, 3) # [1, 2, 3]
517
+ ```
518
+
519
+ ### `Injection` class
520
+
521
+ Full implementation with `descend`, `child`, `setval` instance
522
+ methods. Used internally by `inject`/`transform`/`validate`; you
523
+ need it when writing custom injectors.
524
+
525
+ ### Injection helpers
526
+
527
+ ```ruby
528
+ VoxgigStruct.checkPlacement(modes, ijname, parentTypes, inj)
529
+ VoxgigStruct.injectorArgs(argTypes, args)
530
+ VoxgigStruct.injectChild(child, store, inj)
531
+ ```
532
+
533
+ ### Select operators
534
+
535
+ The Ruby `select` supports compound query operators:
536
+
537
+ ```
538
+ AND OR NOT CMP
539
+ ```
540
+
541
+ See [`voxgig_struct.rb`](./voxgig_struct.rb) for full operator
542
+ semantics.
543
+
544
+
545
+ ## Constants
546
+
547
+ ### Sentinels
548
+
549
+ ```ruby
550
+ VoxgigStruct::SKIP
551
+ VoxgigStruct::DELETE
552
+ VoxgigStruct::UNDEF # frozen sentinel object for "absent"
553
+ ```
554
+
555
+ ### Type bit-flags
556
+
557
+ ```ruby
558
+ VoxgigStruct::T_any VoxgigStruct::T_noval VoxgigStruct::T_boolean
559
+ VoxgigStruct::T_decimal VoxgigStruct::T_integer VoxgigStruct::T_number
560
+ VoxgigStruct::T_string VoxgigStruct::T_function VoxgigStruct::T_symbol
561
+ VoxgigStruct::T_null VoxgigStruct::T_list VoxgigStruct::T_map
562
+ VoxgigStruct::T_instance VoxgigStruct::T_scalar VoxgigStruct::T_node
563
+ ```
564
+
565
+ ### Walk / inject phase flags
566
+
567
+ ```ruby
568
+ VoxgigStruct::M_KEYPRE
569
+ VoxgigStruct::M_KEYPOST
570
+ VoxgigStruct::M_VAL
571
+ VoxgigStruct::MODENAME
572
+ ```
573
+
574
+
575
+ ## Transform commands
576
+
577
+ ```
578
+ $DELETE $COPY $KEY $META $ANNO
579
+ $MERGE $EACH $PACK $REF $FORMAT $APPLY
580
+ ```
581
+
582
+
583
+ ## Validate checkers
584
+
585
+ ```
586
+ $MAP $LIST $STRING $NUMBER $INTEGER $DECIMAL $BOOLEAN
587
+ $NULL $NIL $FUNCTION $INSTANCE $ANY $CHILD $ONE $EXACT
588
+ ```
589
+
590
+
591
+ ## Notes
592
+
593
+ ### `UNDEF`, `nil`, and JSON null
594
+
595
+ Ruby has `nil`. The port distinguishes:
596
+
597
+ - `nil` — JSON null (a defined scalar).
598
+ - `VoxgigStruct::UNDEF` — frozen sentinel for "absent".
599
+
600
+ `typify(nil)` returns `T_scalar | T_null`; `typify(UNDEF)` returns
601
+ `T_noval`.
602
+
603
+ ### Method naming
604
+
605
+ Ruby method names match canonical lowercase (`getpath`, `setpath`,
606
+ `getprop`), not Ruby's idiomatic snake_case. Parity beats style.
607
+
608
+ ### Walk-based merge
609
+
610
+ `merge` is implemented as a `walk` with `before`/`after` callbacks
611
+ and a `maxdepth` parameter, matching the canonical algorithm.
612
+
613
+ ### Test status
614
+
615
+ 81 runs, 159 assertions, 0 failures.
616
+
617
+
618
+ ## Regex
619
+
620
+ Uniform six-function regex API (see `/design/REGEX_API.md`). The Ruby port
621
+ wraps the built-in `Regexp` (Onigmo engine).
622
+
623
+ ### API
624
+
625
+ | Function | Maps to |
626
+ |---|---|
627
+ | `re_compile(pattern)` | `Regexp.new(pattern)` |
628
+ | `re_test(pattern, input)` | `input =~ re` |
629
+ | `re_find(pattern, input)` | `input.match(re)` → `[whole, group1, ...]` |
630
+ | `re_find_all(pattern, input)` | `input.scan(re)` (one row per match) |
631
+ | `re_replace(pattern, input, repl)` | `input.gsub(re, repl)` |
632
+ | `re_escape(s)` | `Regexp.escape(s)` |
633
+
634
+ ### Dialect
635
+
636
+ Patterns must stay inside the **RE2 subset** documented in `/design/REGEX.md`.
637
+ Onigmo supports backreferences and lookaround; using them will not be
638
+ portable to the Go / Rust / C / Lua / Zig ports.
639
+
640
+ ### Sharp edges
641
+
642
+ - **Catastrophic backtracking.** Onigmo has internal mitigations for
643
+ some classic ReDoS shapes — `^(a+)+$` against 22 a's plus `!` runs
644
+ in microseconds here. Larger inputs or different shapes can still
645
+ blow up; the safe rule is to stay inside the RE2 subset and avoid
646
+ nested quantifiers.
647
+ - **Zero-width `replace`.** `re_replace("a*", "abc", "X")` returns
648
+ `"XXbXcX"` — the ECMA convention shared by all PCRE/ECMA/.NET/Java/Onigmo engines plus the in-tree Thompson ports. Go (RE2) returns `"XbXcX"` instead; see `/design/REGEX_PATHOLOGICAL.md`.
649
+
650
+ See `/design/REGEX_PATHOLOGICAL.md` for the cross-port pathological-input panel.
651
+
652
+
653
+ ## Build and test
654
+
655
+ ```bash
656
+ cd ruby
657
+ bundle install
658
+ make test
659
+ ```
660
+
661
+ Tests in [`test_voxgig_struct.rb`](./test_voxgig_struct.rb) consume
662
+ fixtures from [`../build/test/`](../build/test/).