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.
- checksums.yaml +7 -0
- data/README.md +662 -0
- data/voxgig_struct.rb +2322 -0
- 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/).
|