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
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
|