@voxgig/sdkgen 0.16.0 → 0.18.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.
- package/dist/action/feature.js +3 -2
- package/dist/action/feature.js.map +1 -1
- package/dist/action/target.js +11 -6
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/Entity.js +2 -1
- package/dist/cmp/Entity.js.map +1 -1
- package/dist/cmp/Main.js +23 -9
- package/dist/cmp/Main.js.map +1 -1
- package/dist/sdkgen.d.ts +7 -1
- package/dist/sdkgen.js +5 -2
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.js +1 -2
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.jsonic +8 -5
- package/package.json +7 -5
- package/project/.sdk/model/feature/limit.jsonic +12 -0
- package/project/.sdk/model/feature/log.jsonic +32 -0
- package/project/.sdk/model/feature/page.jsonic +9 -0
- package/project/.sdk/model/feature/telemetry.jsonic +10 -0
- package/project/.sdk/model/target/go.jsonic +7 -0
- package/project/.sdk/model/target/js.jsonic +23 -0
- package/project/.sdk/model/target/ts.jsonic +25 -0
- package/project/.sdk/src/cmp/js/Config_js.ts +49 -0
- package/project/.sdk/src/cmp/js/Entity_js.ts +92 -0
- package/project/.sdk/src/cmp/js/MainEntity_js.ts +22 -0
- package/project/.sdk/src/cmp/js/Main_js.ts +88 -0
- package/project/.sdk/src/cmp/js/Package_js.ts +58 -0
- package/project/.sdk/src/cmp/js/Quick_js.ts +82 -0
- package/project/.sdk/src/cmp/js/ReadmeInstall_js.ts +19 -0
- package/project/.sdk/src/cmp/js/ReadmeQuick_js.ts +25 -0
- package/project/.sdk/src/cmp/js/TestAcceptEntity_js.ts +13 -0
- package/project/.sdk/src/cmp/js/TestAccept_js.ts +18 -0
- package/project/.sdk/src/cmp/js/TestEntity_js.ts +13 -0
- package/project/.sdk/src/cmp/js/TestMain_js.ts +19 -0
- package/project/.sdk/src/cmp/js/Test_js.ts +24 -0
- package/project/.sdk/src/cmp/js/fragment/Entity.fragment.js +79 -0
- package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +57 -0
- package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/js/fragment/Main.fragment.js +67 -0
- package/project/.sdk/src/cmp/ts/Config_ts.ts +36 -0
- package/project/.sdk/src/cmp/ts/Entity_ts.ts +93 -0
- package/project/.sdk/src/cmp/ts/MainEntity_ts.ts.off +22 -0
- package/project/.sdk/src/cmp/ts/Main_ts.ts +88 -0
- package/project/.sdk/src/cmp/ts/Main_ts.ts~ +88 -0
- package/project/.sdk/src/cmp/ts/Package_ts.ts +63 -0
- package/project/.sdk/src/cmp/ts/Package_ts.ts~ +58 -0
- package/project/.sdk/src/cmp/ts/Quick_ts.ts.off +82 -0
- package/project/.sdk/src/cmp/ts/ReadmeInstall_ts.ts.off +19 -0
- package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts.off +25 -0
- package/project/.sdk/src/cmp/ts/TestAcceptEntity_ts.ts.off +13 -0
- package/project/.sdk/src/cmp/ts/TestAccept_ts.ts.off +18 -0
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts.off +13 -0
- package/project/.sdk/src/cmp/ts/TestMain_ts.ts.off +19 -0
- package/project/.sdk/src/cmp/ts/Test_ts.ts.off +24 -0
- package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +15 -0
- package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.js +79 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.js +57 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.js +61 -0
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.js +67 -0
- package/project/.sdk/tm/go/LICENSE +22 -0
- package/project/.sdk/tm/js/LICENSE +22 -0
- package/project/.sdk/tm/js/src/feature/log/LogFeature.js +108 -0
- package/project/.sdk/tm/js/src/utility/AuthUtility.js +21 -0
- package/project/.sdk/tm/js/src/utility/BodyUtility.js +29 -0
- package/project/.sdk/tm/js/src/utility/DoneUtility.js +15 -0
- package/project/.sdk/tm/js/src/utility/ErrorUtility.js +33 -0
- package/project/.sdk/tm/js/src/utility/FindparamUtility.js +31 -0
- package/project/.sdk/tm/js/src/utility/FullurlUtility.js +39 -0
- package/project/.sdk/tm/js/src/utility/HeadersUtility.js +13 -0
- package/project/.sdk/tm/js/src/utility/JoinurlUtility.js +14 -0
- package/project/.sdk/tm/js/src/utility/MethodUtility.js +22 -0
- package/project/.sdk/tm/js/src/utility/OperatorUtility.js +44 -0
- package/project/.sdk/tm/js/src/utility/OptionsUtility.js +54 -0
- package/project/.sdk/tm/js/src/utility/ParamsUtility.js +21 -0
- package/project/.sdk/tm/js/src/utility/QueryUtility.js +21 -0
- package/project/.sdk/tm/js/src/utility/ReqformUtility.js +32 -0
- package/project/.sdk/tm/js/src/utility/RequestUtility.js +48 -0
- package/project/.sdk/tm/js/src/utility/ResbasicUtility.js +27 -0
- package/project/.sdk/tm/js/src/utility/ResbodyUtility.js +15 -0
- package/project/.sdk/tm/js/src/utility/ResformUtility.js +34 -0
- package/project/.sdk/tm/js/src/utility/ResheadersUtility.js +19 -0
- package/project/.sdk/tm/js/src/utility/ResponseUtility.js +37 -0
- package/project/.sdk/tm/js/src/utility/ResultUtility.js +28 -0
- package/project/.sdk/tm/js/src/utility/SpecUtility.js +35 -0
- package/project/.sdk/tm/js/src/utility/StructUtility.js +1203 -0
- package/project/.sdk/tm/js/src/utility/Utility.js +74 -0
- package/project/.sdk/tm/js/test/runner.js +171 -0
- package/project/.sdk/tm/js/test/utility/Custom.test.js +85 -0
- package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +187 -0
- package/project/.sdk/tm/js/test/utility/StructUtility.test.js +367 -0
- package/project/.sdk/tm/py/LICENSE +22 -0
- package/project/.sdk/tm/ts/LICENSE +22 -0
- package/project/.sdk/tm/ts/src/README.md +1 -0
- package/project/.sdk/tm/ts/src/feature/README.md +1 -0
- package/project/.sdk/tm/ts/src/tsconfig.json +15 -0
- package/project/.sdk/tm/ts/src/utility/README.md +3 -0
- package/project/.sdk/tm/ts/test/README.md +2 -0
- package/project/.sdk/tm/ts/test/README.md~ +2 -0
- package/project/.sdk/tm/ts/test/tsconfig.json +13 -0
- package/src/action/feature.ts +4 -3
- package/src/action/target.ts +12 -6
- package/src/cmp/Entity.ts +3 -1
- package/src/cmp/Main.ts +30 -10
- package/src/sdkgen.ts +15 -5
- package/src/utility.ts +1 -2
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
|
|
2
|
+
/* Copyright (c) 2025 Voxgig Ltd. MIT LICENSE. */
|
|
3
|
+
|
|
4
|
+
/* Voxgig Struct
|
|
5
|
+
* =============
|
|
6
|
+
*
|
|
7
|
+
* Utility functions to manipulate in-memory JSON-like data
|
|
8
|
+
* structures. The general design principle is
|
|
9
|
+
* "by-example". Transform specifications mirror the desired output.
|
|
10
|
+
* This implementation is desgined for porting to multiple language.
|
|
11
|
+
*
|
|
12
|
+
* - isnode, islist, islist, iskey: identify value kinds
|
|
13
|
+
* - clone: create a copy of a JSON-like data structure
|
|
14
|
+
* - items: list entries of a map or list as [key, value] pairs
|
|
15
|
+
* - getprop: safely get a property value by key
|
|
16
|
+
* - setprop: safely set a property value by key
|
|
17
|
+
* - getpath: get the value at a key path deep inside an object
|
|
18
|
+
* - merge: merge multiple nodes, overriding values in earlier nodes.
|
|
19
|
+
* - walk: walk a node tree, applying a function at each node and leaf.
|
|
20
|
+
* - inject: inject values from a data store into a new data structure.
|
|
21
|
+
* - transform: transform a data structure to an example structure.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// String constants.
|
|
26
|
+
const S = {
|
|
27
|
+
MKEYPRE: 'key:pre',
|
|
28
|
+
MKEYPOST: 'key:post',
|
|
29
|
+
MVAL: 'val',
|
|
30
|
+
MKEY: 'key',
|
|
31
|
+
|
|
32
|
+
TKEY: '`$KEY`',
|
|
33
|
+
TMETA: '`$META`',
|
|
34
|
+
|
|
35
|
+
KEY: 'KEY',
|
|
36
|
+
|
|
37
|
+
DTOP: '$TOP',
|
|
38
|
+
|
|
39
|
+
object: 'object',
|
|
40
|
+
array: 'array',
|
|
41
|
+
number: 'number',
|
|
42
|
+
boolean: 'boolean',
|
|
43
|
+
string: 'string',
|
|
44
|
+
function: 'function',
|
|
45
|
+
empty: '',
|
|
46
|
+
base: 'base',
|
|
47
|
+
|
|
48
|
+
BT: '`',
|
|
49
|
+
DS: '$',
|
|
50
|
+
DT: '.',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const UNDEF = undefined
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// Value is a node - defined, and a map (hash) or list (array).
|
|
57
|
+
function isnode(val) {
|
|
58
|
+
return null != val && S.object == typeof val
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
// Value is a defined map (hash) with string keys.
|
|
63
|
+
function ismap(val) {
|
|
64
|
+
return null != val && S.object == typeof val && !Array.isArray(val)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Value is a defined list (array) with integer keys (indexes).
|
|
69
|
+
function islist(val) {
|
|
70
|
+
return Array.isArray(val)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// Value is a defined string (non-empty) or integer key.
|
|
75
|
+
function iskey(key) {
|
|
76
|
+
const keytype = typeof key
|
|
77
|
+
return (S.string === keytype && S.empty !== key) || S.number === keytype
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
// Check for an "empty" value - undefined, null, empty string, array, object.
|
|
82
|
+
function isempty(val) {
|
|
83
|
+
return null == val || S.empty === val ||
|
|
84
|
+
(Array.isArray(val) && 0 === val.length) ||
|
|
85
|
+
(S.object === typeof val && 0 === Object.keys(val).length)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// TOOD: TEST
|
|
90
|
+
function isfunc(val) {
|
|
91
|
+
return S.function === typeof val
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
// Escape regular expression.
|
|
96
|
+
function escre(s) {
|
|
97
|
+
s = null == s ? S.empty : s
|
|
98
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
// Escape URL.
|
|
103
|
+
function escurl(s) {
|
|
104
|
+
s = null == s ? S.empty : s
|
|
105
|
+
return encodeURIComponent(s)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
function joinurl(sarr) {
|
|
110
|
+
return sarr
|
|
111
|
+
.filter(s=>null!=s&&''!==s)
|
|
112
|
+
.map((s,i)=> 0===i ? s.replace(/([^\/])\/+/,'$1/').replace(/\/+$/,'') :
|
|
113
|
+
s.replace(/([^\/])\/+/,'$1/').replace(/^\/+/,'').replace(/\/+$/,''))
|
|
114
|
+
.filter(s=>''!==s)
|
|
115
|
+
.join('/')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
// List the keys of a map or list as an array of tuples of the form [key, value].
|
|
120
|
+
function items(val) {
|
|
121
|
+
return ismap(val) ? Object.entries(val) :
|
|
122
|
+
islist(val) ? val.map((n, i) => [i, n]) :
|
|
123
|
+
[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
// Sorted keys of a map, or indexes of an array.
|
|
128
|
+
function keysof(val) {
|
|
129
|
+
return !isnode(val) ? [] : ismap(val) ? Object.keys(val).sort() : val.map((n,i)=>i)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
function haskey(val, key) {
|
|
134
|
+
return UNDEF !== getprop(val, key)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// Safely stringify a value for printing (NOT JSON!).
|
|
139
|
+
function stringify(val, maxlen) {
|
|
140
|
+
let json = S.empty
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
json = JSON.stringify(val)
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
json = S.empty + val
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
json = S.string !== typeof json ? S.empty + json : json
|
|
150
|
+
json = json.replace(/"/g, '')
|
|
151
|
+
|
|
152
|
+
if (null != maxlen) {
|
|
153
|
+
let js = json.substring(0, maxlen)
|
|
154
|
+
json = maxlen < json.length ? (js.substring(0, maxlen - 3) + '...') : json
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return json
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
// Clone a JSON-like data structure.
|
|
162
|
+
// NOTE: function values are *not* cloned.
|
|
163
|
+
function clone(val) {
|
|
164
|
+
const refs = []
|
|
165
|
+
const replacer = (k,v)=>S.function === typeof v ?
|
|
166
|
+
(refs.push(v),'`$FUNCTION:'+(refs.length-1)+'`') : v
|
|
167
|
+
const reviver = (k,v,m)=>S.string === typeof v ?
|
|
168
|
+
(m=v.match(/^`\$FUNCTION:([0-9]+)`$/),m?refs[m[1]]:v) : v
|
|
169
|
+
return UNDEF === val ? UNDEF : JSON.parse(JSON.stringify(val,replacer),reviver)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// Safely get a property of a node. UNDEF arguments return UNDEF.
|
|
174
|
+
// If the key is not found, return the alternative value.
|
|
175
|
+
function getprop(val, key, alt) {
|
|
176
|
+
let out = UNDEF === val ? alt : UNDEF === key ? alt : val[key]
|
|
177
|
+
out = UNDEF === out ? alt : out
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
// Safely set a property. UNDEF arguments and invalid keys are ignored.
|
|
183
|
+
// Returns the (possible modified) parent.
|
|
184
|
+
// If the value is UNDEF it the key will be deleted from the parent.
|
|
185
|
+
// If the parent is a list, and the key is negative, prepend the value.
|
|
186
|
+
// If the key is above the list size, append the value.
|
|
187
|
+
// If the value is UNDEF, remove the list element at index key, and shift the
|
|
188
|
+
// remaining elements down. These rules avoids "holes" in the list.
|
|
189
|
+
function setprop(parent, key, val) {
|
|
190
|
+
if (!iskey(key)) {
|
|
191
|
+
return parent
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (ismap(parent)) {
|
|
195
|
+
key = S.empty + key
|
|
196
|
+
if (UNDEF === val) {
|
|
197
|
+
delete parent[key]
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
parent[key] = val
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (islist(parent)) {
|
|
204
|
+
// Ensure key is an integer.
|
|
205
|
+
let keyI = +key
|
|
206
|
+
|
|
207
|
+
if (isNaN(keyI)) {
|
|
208
|
+
return parent
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
keyI = Math.floor(keyI)
|
|
212
|
+
|
|
213
|
+
// Delete list element at position keyI, shifting later elements down.
|
|
214
|
+
if (UNDEF === val) {
|
|
215
|
+
if (0 <= keyI && keyI < parent.length) {
|
|
216
|
+
for (let pI = keyI; pI < parent.length - 1; pI++) {
|
|
217
|
+
parent[pI] = parent[pI + 1]
|
|
218
|
+
}
|
|
219
|
+
parent.length = parent.length - 1
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Set or append value at position keyI, or append if keyI out of bounds.
|
|
224
|
+
else if (0 <= keyI) {
|
|
225
|
+
parent[parent.length < keyI ? parent.length : keyI] = val
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Prepend value if keyI is negative
|
|
229
|
+
else {
|
|
230
|
+
parent.unshift(val)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return parent
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// Walk a data structure depth first.
|
|
239
|
+
function walk(
|
|
240
|
+
// These arguments are the public interface.
|
|
241
|
+
val,
|
|
242
|
+
apply,
|
|
243
|
+
|
|
244
|
+
// These areguments are used for recursive state.
|
|
245
|
+
key,
|
|
246
|
+
parent,
|
|
247
|
+
path
|
|
248
|
+
) {
|
|
249
|
+
if (isnode(val)) {
|
|
250
|
+
for (let [ckey, child] of items(val)) {
|
|
251
|
+
setprop(val, ckey, walk(child, apply, ckey, val, [...(path || []), S.empty + ckey]))
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Nodes are applied *after* their children.
|
|
256
|
+
// For the root node, key and parent will be UNDEF.
|
|
257
|
+
return apply(key, val, parent, path || [])
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
// Merge a list of values into each other. Later values have precedence.
|
|
262
|
+
// Nodes override scalars. Node kinds (list or map) override each other.
|
|
263
|
+
// The first element is modified.
|
|
264
|
+
function merge(objs) {
|
|
265
|
+
let out = UNDEF
|
|
266
|
+
|
|
267
|
+
if (!islist(objs)) {
|
|
268
|
+
out = objs
|
|
269
|
+
}
|
|
270
|
+
else if (0 === objs.length) {
|
|
271
|
+
out = UNDEF
|
|
272
|
+
}
|
|
273
|
+
else if (1 === objs.length) {
|
|
274
|
+
out = objs[0]
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
|
|
278
|
+
out = getprop(objs, 0, {})
|
|
279
|
+
|
|
280
|
+
// Merge remaining down onto first.
|
|
281
|
+
for (let oI = 1; oI < objs.length; oI++) {
|
|
282
|
+
let obj = objs[oI]
|
|
283
|
+
|
|
284
|
+
if (!isnode(obj)) {
|
|
285
|
+
// Nodes win.
|
|
286
|
+
out = obj
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// Nodes win, also over nodes of a different kind.
|
|
290
|
+
if (!isnode(out) || (ismap(obj) && islist(out)) || (islist(obj) && ismap(out))) {
|
|
291
|
+
out = obj
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
let cur = [out] // Node stack
|
|
295
|
+
let cI = 0
|
|
296
|
+
|
|
297
|
+
// Walk overriding node, creating paths in output as needed.
|
|
298
|
+
walk(obj, (key, val, parent, path) => {
|
|
299
|
+
if (null == key) {
|
|
300
|
+
return val
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let lenpath = path.length
|
|
304
|
+
|
|
305
|
+
cI = lenpath - 1
|
|
306
|
+
if (UNDEF === cur[cI]) {
|
|
307
|
+
cur[cI] = getpath(path.slice(0, lenpath - 1), out)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Create node if needed.
|
|
311
|
+
if (!isnode(cur[cI])) {
|
|
312
|
+
cur[cI] = islist(parent) ? [] : {}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Node child is just ahead of us on the stack.
|
|
316
|
+
if (isnode(val) && !isempty(val)) {
|
|
317
|
+
setprop(cur[cI], key, cur[cI + 1])
|
|
318
|
+
cur[cI + 1] = UNDEF
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Scalar child or empty node.
|
|
322
|
+
else {
|
|
323
|
+
setprop(cur[cI], key, val)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return val
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
// Get a value deep inside a node using a key path.
|
|
338
|
+
// For example the path `a.b` gets the value 1 from {a:{b:1}}.
|
|
339
|
+
// The path can specified as a dotted string, or a string array.
|
|
340
|
+
// If the path starts with a dot (or the first element is ''), the path is considered local,
|
|
341
|
+
// and resolved against the `current` argument, if defined.
|
|
342
|
+
// Integer path parts are used as array indexes.
|
|
343
|
+
// The state argument allows for custom handling when called from `inject` or `transform`.
|
|
344
|
+
function getpath(path, store, current, state) {
|
|
345
|
+
|
|
346
|
+
const parts = islist(path) ? path : S.string === typeof path ? path.split(S.DT) : UNDEF
|
|
347
|
+
|
|
348
|
+
if (UNDEF === parts) {
|
|
349
|
+
return UNDEF
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let root = store
|
|
353
|
+
let val = store
|
|
354
|
+
|
|
355
|
+
// An empty path (incl empty string) just finds the store.
|
|
356
|
+
if (null == path || null == store || (1 === parts.length && S.empty === parts[0])) {
|
|
357
|
+
// The actual store data may be in a store sub property, defined by state.base.
|
|
358
|
+
val = getprop(store, getprop(state, S.base), store)
|
|
359
|
+
}
|
|
360
|
+
else if (0 < parts.length) {
|
|
361
|
+
let pI = 0
|
|
362
|
+
|
|
363
|
+
// Relative path uses `current` argument.
|
|
364
|
+
if (S.empty === parts[0]) {
|
|
365
|
+
pI = 1
|
|
366
|
+
root = current
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let part = pI < parts.length ? parts[pI] : UNDEF
|
|
370
|
+
let first = getprop(root, part)
|
|
371
|
+
|
|
372
|
+
// At top level, check state.base, if provided
|
|
373
|
+
val = (UNDEF === first && 0 === pI) ?
|
|
374
|
+
getprop(getprop(root, getprop(state, S.base)), part) :
|
|
375
|
+
first
|
|
376
|
+
|
|
377
|
+
// Move along the path, trying to descend into the store.
|
|
378
|
+
for (pI++; UNDEF !== val && pI < parts.length; pI++) {
|
|
379
|
+
val = getprop(val, parts[pI])
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// State may provide a custom handler to modify found value.
|
|
385
|
+
if (null != state && S.function === typeof state.handler) {
|
|
386
|
+
val = state.handler(state, val, current, path, store)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return val
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
// Inject store values into a string. Not a public utility - used by `inject`.
|
|
394
|
+
// Inject are marked with `path` where path is resolved with getpath against the
|
|
395
|
+
// store or current (if defined) arguments. See `getpath`.
|
|
396
|
+
// Custom injection handling can be provided by state.handler (this is used for
|
|
397
|
+
// transform functions).
|
|
398
|
+
// The path can also have the special syntax $NAME999 where NAME is upper case letters only,
|
|
399
|
+
// and 999 is any digits, which are discarded. This syntax specifies the name of a transform,
|
|
400
|
+
// and optionally allows transforms to be ordered by alphanumeric sorting.
|
|
401
|
+
function injectstr(val, store, current, state) {
|
|
402
|
+
if (S.string !== typeof val) {
|
|
403
|
+
return S.empty
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let out = val
|
|
407
|
+
const m = val.match(/^`(\$[A-Z]+|[^`]+)[0-9]*`$/)
|
|
408
|
+
|
|
409
|
+
// Full string is an injection.
|
|
410
|
+
if (m) {
|
|
411
|
+
if (state) {
|
|
412
|
+
state.full = true
|
|
413
|
+
}
|
|
414
|
+
let ref = m[1]
|
|
415
|
+
|
|
416
|
+
// Special escapes inside injection.
|
|
417
|
+
ref = 3 < ref.length ? ref.replace(/\$BT/g, S.BT).replace(/\$DS/g, S.DS) : ref
|
|
418
|
+
|
|
419
|
+
out = getpath(ref, store, current, state)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check for injections within the string.
|
|
423
|
+
else {
|
|
424
|
+
out = val.replace(/`([^`]+)`/g,
|
|
425
|
+
(_m, ref) => {
|
|
426
|
+
ref = 3 < ref.length ? ref.replace(/\$BT/g, S.BT).replace(/\$DS/g, S.DS) : ref
|
|
427
|
+
if (state) {
|
|
428
|
+
state.full = false
|
|
429
|
+
}
|
|
430
|
+
const found = getpath(ref, store, current, state)
|
|
431
|
+
|
|
432
|
+
return UNDEF === found ? S.empty :
|
|
433
|
+
S.object === typeof found ? JSON.stringify(found) :
|
|
434
|
+
found
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// Also call the handler on the entire string.
|
|
438
|
+
if (state.handler) {
|
|
439
|
+
state.full = true
|
|
440
|
+
out = state.handler(state, out, current, val, store)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return out
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
// Inject values from a data store into a node recursively, resolving paths against the store,
|
|
449
|
+
// or current if they are local. THe modify argument allows custom modification of the result.
|
|
450
|
+
// The state (InjectState) argument is used to maintain recursive state.
|
|
451
|
+
function inject(
|
|
452
|
+
val,
|
|
453
|
+
store,
|
|
454
|
+
modify,
|
|
455
|
+
current,
|
|
456
|
+
state,
|
|
457
|
+
) {
|
|
458
|
+
const valtype = typeof val
|
|
459
|
+
|
|
460
|
+
// Create state if at root of injection.
|
|
461
|
+
// The input value is placed inside a virtual parent holder
|
|
462
|
+
// to simplify edge cases.
|
|
463
|
+
if (UNDEF === state) {
|
|
464
|
+
const parent = { [S.DTOP]: val }
|
|
465
|
+
state = {
|
|
466
|
+
mode: S.MVAL,
|
|
467
|
+
full: false,
|
|
468
|
+
keyI: 0,
|
|
469
|
+
keys: [S.DTOP],
|
|
470
|
+
key: S.DTOP,
|
|
471
|
+
val,
|
|
472
|
+
parent,
|
|
473
|
+
path: [S.DTOP],
|
|
474
|
+
nodes: [parent],
|
|
475
|
+
handler: injecthandler,
|
|
476
|
+
base: S.DTOP,
|
|
477
|
+
modify,
|
|
478
|
+
errs: store.$ERRS || [],
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Resolve current node in store for local paths.
|
|
483
|
+
if (UNDEF === current) {
|
|
484
|
+
current = { $TOP: store }
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const parentkey = state.path[state.path.length - 2]
|
|
488
|
+
current = null == parentkey ? current : getprop(current, parentkey)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Desend into node.
|
|
492
|
+
if (isnode(val)) {
|
|
493
|
+
|
|
494
|
+
// Keys are sorted alphanumerically to ensure determinism.
|
|
495
|
+
// Injection transforms ($FOO) are processed *after* other keys.
|
|
496
|
+
// NOTE: the optional digits suffix of the transform can thsu be used to
|
|
497
|
+
// order the transforms.
|
|
498
|
+
const origkeys = ismap(val) ? [
|
|
499
|
+
...Object.keys(val).filter(k => !k.includes(S.DS)),
|
|
500
|
+
...Object.keys(val).filter(k => k.includes(S.DS)).sort(),
|
|
501
|
+
] : val.map((_n, i) => i)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
// Each child key-value pair is processed in three injection phases:
|
|
505
|
+
// 1. state.mode='key:pre' - Key string is injected, returning a possibly altered key.
|
|
506
|
+
// 2. state.mode='val' - The child value is injected.
|
|
507
|
+
// 3. state.mode='key:post' - Key string is injected again, allowing child mutation.
|
|
508
|
+
for (let okI = 0; okI < origkeys.length; okI++) {
|
|
509
|
+
const origkey = S.empty + origkeys[okI]
|
|
510
|
+
|
|
511
|
+
let childpath = [...(state.path || []), origkey]
|
|
512
|
+
let childnodes = [...(state.nodes || []), val]
|
|
513
|
+
|
|
514
|
+
const childstate = {
|
|
515
|
+
mode: S.MKEYPRE,
|
|
516
|
+
full: false,
|
|
517
|
+
keyI: okI,
|
|
518
|
+
keys: origkeys,
|
|
519
|
+
key: origkey,
|
|
520
|
+
val,
|
|
521
|
+
parent: val,
|
|
522
|
+
path: childpath,
|
|
523
|
+
nodes: childnodes,
|
|
524
|
+
handler: injecthandler,
|
|
525
|
+
base: state.base,
|
|
526
|
+
errs: store.$ERRS || [],
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const prekey = injectstr(origkey, store, current, childstate)
|
|
530
|
+
okI = childstate.keyI
|
|
531
|
+
|
|
532
|
+
// Prevent further processing by returning an UNDEF prekey
|
|
533
|
+
if (null != prekey) {
|
|
534
|
+
let child = val[prekey]
|
|
535
|
+
childstate.mode = S.MVAL
|
|
536
|
+
inject(
|
|
537
|
+
child,
|
|
538
|
+
store,
|
|
539
|
+
modify,
|
|
540
|
+
current,
|
|
541
|
+
childstate,
|
|
542
|
+
)
|
|
543
|
+
okI = childstate.keyI
|
|
544
|
+
|
|
545
|
+
childstate.mode = S.MKEYPOST
|
|
546
|
+
injectstr(origkey, store, current, childstate)
|
|
547
|
+
okI = childstate.keyI
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Inject paths into string scalars.
|
|
553
|
+
else if (S.string === valtype) {
|
|
554
|
+
state.mode = S.MVAL
|
|
555
|
+
const newval = injectstr(val, store, current, state)
|
|
556
|
+
val = newval
|
|
557
|
+
|
|
558
|
+
setprop(state.parent, state.key, newval)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Other scalars are left in place unchanged.
|
|
562
|
+
|
|
563
|
+
// Custom modification.
|
|
564
|
+
if (modify) {
|
|
565
|
+
modify(
|
|
566
|
+
state.key,
|
|
567
|
+
val,
|
|
568
|
+
state.parent,
|
|
569
|
+
state,
|
|
570
|
+
current,
|
|
571
|
+
store
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Original val reference may no longer be correct.
|
|
576
|
+
const out = getprop(state.parent, S.DTOP)
|
|
577
|
+
|
|
578
|
+
// Output is only needed at the top level as the final result
|
|
579
|
+
return out
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
// Default inject handler for transforms. If the path resolves to a function,
|
|
584
|
+
// call the function passing the injection state. This is how transforms operate.
|
|
585
|
+
const injecthandler = (state, val, current, ref, store) => {
|
|
586
|
+
let out = val
|
|
587
|
+
|
|
588
|
+
if (S.function === typeof val && ref.startsWith('$')) {
|
|
589
|
+
out = val(state, val, current, store)
|
|
590
|
+
}
|
|
591
|
+
else if (S.MVAL === state.mode && state.full) {
|
|
592
|
+
setprop(state.parent, state.key, val)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return out
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
// The transform_* functions are define inject handlers (see InjectHandler).
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
// Delete a key from a map or list.
|
|
603
|
+
const transform_DELETE = (state) => {
|
|
604
|
+
const { key, parent } = state
|
|
605
|
+
setprop(parent, key, UNDEF)
|
|
606
|
+
return UNDEF
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
// Copy value from source data.
|
|
611
|
+
const transform_COPY = (state, _val, current) => {
|
|
612
|
+
const { mode, key, parent } = state
|
|
613
|
+
|
|
614
|
+
let out
|
|
615
|
+
if (mode.startsWith(S.MKEY)) {
|
|
616
|
+
out = key
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
out = getprop(current, key)
|
|
620
|
+
setprop(parent, key, out)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return out
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
// As a value, inject the key of the parent node.
|
|
628
|
+
// As a key, defined the name of the key property in the source object.
|
|
629
|
+
const transform_KEY = (state, _val, current) => {
|
|
630
|
+
const { mode, path, parent } = state
|
|
631
|
+
|
|
632
|
+
if (S.MVAL !== mode) {
|
|
633
|
+
return UNDEF
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const keyspec = getprop(parent, S.TKEY)
|
|
637
|
+
if (UNDEF !== keyspec) {
|
|
638
|
+
setprop(parent, S.TKEY, UNDEF)
|
|
639
|
+
return getprop(current, keyspec)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return getprop(getprop(parent, S.TMETA), S.KEY, getprop(path, path.length - 2))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
// Store meta data about a node.
|
|
647
|
+
const transform_META = (state) => {
|
|
648
|
+
const { parent } = state
|
|
649
|
+
setprop(parent, S.TMETA, UNDEF)
|
|
650
|
+
return UNDEF
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
// Merge a list of objects into the current object.
|
|
655
|
+
// Must be a key in an object. The value is merged over the current object.
|
|
656
|
+
// If the value is an array, the elements are first merged using `merge`.
|
|
657
|
+
// If the value is the empty string, merge the top level store.
|
|
658
|
+
// Format: { '`$MERGE`': '`source-path`' | ['`source-paths`', ...] }
|
|
659
|
+
const transform_MERGE = (
|
|
660
|
+
state, _val, store
|
|
661
|
+
) => {
|
|
662
|
+
const { mode, key, parent } = state
|
|
663
|
+
|
|
664
|
+
if (S.MKEYPRE === mode) { return key }
|
|
665
|
+
|
|
666
|
+
// Operate after child values have been transformed.
|
|
667
|
+
if (S.MKEYPOST === mode) {
|
|
668
|
+
|
|
669
|
+
let args = getprop(parent, key)
|
|
670
|
+
args = S.empty === args ? [store.$TOP] : Array.isArray(args) ? args : [args]
|
|
671
|
+
|
|
672
|
+
setprop(parent, key, UNDEF)
|
|
673
|
+
|
|
674
|
+
// Literals in the parent have precedence.
|
|
675
|
+
const mergelist = [parent, ...args, clone(parent)]
|
|
676
|
+
|
|
677
|
+
merge(mergelist)
|
|
678
|
+
|
|
679
|
+
return key
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return UNDEF
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
// Convert a node to a list.
|
|
687
|
+
// Format: ['`$EACH`', '`source-path-of-node`', child-template]
|
|
688
|
+
const transform_EACH = (
|
|
689
|
+
state,
|
|
690
|
+
_val,
|
|
691
|
+
current,
|
|
692
|
+
store
|
|
693
|
+
) => {
|
|
694
|
+
const { mode, keys, path, parent, nodes } = state
|
|
695
|
+
|
|
696
|
+
// Remove arguments to avoid spurious processing.
|
|
697
|
+
if (keys) {
|
|
698
|
+
keys.length = 1
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Defensive context checks.
|
|
702
|
+
if (S.MVAL !== mode || null == path || null == nodes) {
|
|
703
|
+
return UNDEF
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Get arguments.
|
|
707
|
+
const srcpath = parent[1] // Path to source data.
|
|
708
|
+
const child = clone(parent[2]) // Child template.
|
|
709
|
+
|
|
710
|
+
// Source data
|
|
711
|
+
const src = getpath(srcpath, store, current, state)
|
|
712
|
+
|
|
713
|
+
// Create parallel data structures:
|
|
714
|
+
// source entries :: child templates
|
|
715
|
+
let tcurrent = []
|
|
716
|
+
let tval = []
|
|
717
|
+
|
|
718
|
+
const tkey = path[path.length - 2]
|
|
719
|
+
const target = nodes[path.length - 2] || nodes[path.length - 1]
|
|
720
|
+
|
|
721
|
+
if (isnode(src)) {
|
|
722
|
+
if (islist(src)) {
|
|
723
|
+
tval = src.map(() => clone(child))
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
tval = Object.entries(src).map(n => ({
|
|
727
|
+
...clone(child),
|
|
728
|
+
|
|
729
|
+
// Make a note of the key for $KEY transforms
|
|
730
|
+
[S.TMETA]: { KEY: n[0] }
|
|
731
|
+
}))
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
tcurrent = Object.values(src)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Parent structure.
|
|
738
|
+
tcurrent = { $TOP: tcurrent }
|
|
739
|
+
|
|
740
|
+
// Build the substructure.
|
|
741
|
+
tval = inject(
|
|
742
|
+
tval,
|
|
743
|
+
store,
|
|
744
|
+
state.modify,
|
|
745
|
+
tcurrent,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
setprop(target, tkey, tval)
|
|
749
|
+
|
|
750
|
+
// Prevent callee from damaging first list entry (since we are in `val` mode).
|
|
751
|
+
return tval[0]
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
// Convert a node to a map.
|
|
757
|
+
// Format: { '`$PACK`':['`source-path`', child-template]}
|
|
758
|
+
const transform_PACK = (
|
|
759
|
+
state,
|
|
760
|
+
_val,
|
|
761
|
+
current,
|
|
762
|
+
store
|
|
763
|
+
) => {
|
|
764
|
+
const { mode, key, path, parent, nodes } = state
|
|
765
|
+
|
|
766
|
+
// Defensive context checks.
|
|
767
|
+
if (S.MKEYPRE !== mode || S.string !== typeof key || null == path || null == nodes) {
|
|
768
|
+
return UNDEF
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Get arguments.
|
|
772
|
+
const args = parent[key]
|
|
773
|
+
const srcpath = args[0] // Path to source data.
|
|
774
|
+
const child = clone(args[1]) // Child template.
|
|
775
|
+
|
|
776
|
+
// Find key and target node.
|
|
777
|
+
const keyprop = child[S.TKEY]
|
|
778
|
+
const tkey = path[path.length - 2]
|
|
779
|
+
const target = nodes[path.length - 2] || nodes[path.length - 1]
|
|
780
|
+
|
|
781
|
+
// Source data
|
|
782
|
+
let src = getpath(srcpath, store, current, state)
|
|
783
|
+
|
|
784
|
+
// Prepare source as a list.
|
|
785
|
+
src = islist(src) ? src :
|
|
786
|
+
ismap(src) ? Object.entries(src)
|
|
787
|
+
.reduce((a, n) =>
|
|
788
|
+
(n[1][S.TMETA] = { KEY: n[0] }, a.push(n[1]), a), []) :
|
|
789
|
+
UNDEF
|
|
790
|
+
|
|
791
|
+
if (null == src) {
|
|
792
|
+
return UNDEF
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Get key if specified.
|
|
796
|
+
let childkey = getprop(child, S.TKEY)
|
|
797
|
+
let keyname = UNDEF === childkey ? keyprop : childkey
|
|
798
|
+
setprop(child, S.TKEY, UNDEF)
|
|
799
|
+
|
|
800
|
+
// Build parallel target object.
|
|
801
|
+
let tval = {}
|
|
802
|
+
tval = src.reduce((a, n) => {
|
|
803
|
+
let kn = getprop(n, keyname)
|
|
804
|
+
setprop(a, kn, clone(child))
|
|
805
|
+
const nchild = getprop(a, kn)
|
|
806
|
+
setprop(nchild, S.TMETA, getprop(n, S.TMETA))
|
|
807
|
+
return a
|
|
808
|
+
}, tval)
|
|
809
|
+
|
|
810
|
+
// Build parallel source object.
|
|
811
|
+
let tcurrent = {}
|
|
812
|
+
src.reduce((a, n) => {
|
|
813
|
+
let kn = getprop(n, keyname)
|
|
814
|
+
setprop(a, kn, n)
|
|
815
|
+
return a
|
|
816
|
+
}, tcurrent)
|
|
817
|
+
|
|
818
|
+
tcurrent = { $TOP: tcurrent }
|
|
819
|
+
|
|
820
|
+
// Build substructure.
|
|
821
|
+
tval = inject(
|
|
822
|
+
tval,
|
|
823
|
+
store,
|
|
824
|
+
state.modify,
|
|
825
|
+
tcurrent,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
setprop(target, tkey, tval)
|
|
829
|
+
|
|
830
|
+
// Drop transform key.
|
|
831
|
+
return UNDEF
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
// Transform data using spec.
|
|
836
|
+
// Only operates on static JSON-like data (values can be functions however).
|
|
837
|
+
// Arrays are treated as if they are objects with indices as keys.
|
|
838
|
+
function transform(
|
|
839
|
+
data, // Source data to transform into new data (original not mutated)
|
|
840
|
+
spec, // Transform specification; output follows this shape
|
|
841
|
+
extra, // Additional store of data and transforms.
|
|
842
|
+
modify // Optionally modify individual values.
|
|
843
|
+
) {
|
|
844
|
+
// Clone the spec so that the clone can be modified in place as the transform result.
|
|
845
|
+
spec = clone(spec)
|
|
846
|
+
|
|
847
|
+
const extraTransforms = {}
|
|
848
|
+
const extraData = null == extra ? {} : items(extra)
|
|
849
|
+
.reduce((a, n) =>
|
|
850
|
+
(n[0].startsWith(S.DS) ? extraTransforms[n[0]] = n[1] : (a[n[0]] = n[1]), a), {})
|
|
851
|
+
|
|
852
|
+
const dataClone = merge([
|
|
853
|
+
clone(UNDEF === extraData ? {} : extraData),
|
|
854
|
+
clone(UNDEF === data ? {} : data),
|
|
855
|
+
])
|
|
856
|
+
|
|
857
|
+
// Define a top level store that provides transform operations.
|
|
858
|
+
const store = {
|
|
859
|
+
|
|
860
|
+
// The inject function recognises this special location for the root of the source data.
|
|
861
|
+
// NOTE: to escape data that contains "`$FOO`" keys at the top level,
|
|
862
|
+
// place that data inside a holding map: { myholder: mydata }.
|
|
863
|
+
$TOP: dataClone,
|
|
864
|
+
|
|
865
|
+
// Escape backtick (this also works inside backticks).
|
|
866
|
+
$BT: () => S.BT,
|
|
867
|
+
|
|
868
|
+
// Escape dollar sign (this also works inside backticks).
|
|
869
|
+
$DS: () => S.DS,
|
|
870
|
+
|
|
871
|
+
// Insert current date and time as an ISO string.
|
|
872
|
+
$WHEN: () => new Date().toISOString(),
|
|
873
|
+
|
|
874
|
+
$DELETE: transform_DELETE,
|
|
875
|
+
$COPY: transform_COPY,
|
|
876
|
+
$KEY: transform_KEY,
|
|
877
|
+
$META: transform_META,
|
|
878
|
+
$MERGE: transform_MERGE,
|
|
879
|
+
$EACH: transform_EACH,
|
|
880
|
+
$PACK: transform_PACK,
|
|
881
|
+
|
|
882
|
+
// Custom extra transforms, if any.
|
|
883
|
+
...extraTransforms,
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const out = inject(spec, store, modify, store)
|
|
887
|
+
|
|
888
|
+
return out
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
function validate(
|
|
894
|
+
data, // Source data to transform into new data (original not mutated)
|
|
895
|
+
spec, // Transform specification; output follows this shape
|
|
896
|
+
|
|
897
|
+
// TODO
|
|
898
|
+
extra, // Additional custom checks
|
|
899
|
+
|
|
900
|
+
// modify // Optionally modify individual values.
|
|
901
|
+
collecterrs,
|
|
902
|
+
) {
|
|
903
|
+
const errs = collecterrs || []
|
|
904
|
+
const out = transform(
|
|
905
|
+
data,
|
|
906
|
+
spec,
|
|
907
|
+
{
|
|
908
|
+
$ERRS: errs,
|
|
909
|
+
|
|
910
|
+
$DELETE: null,
|
|
911
|
+
$COPY: null,
|
|
912
|
+
$KEY: null,
|
|
913
|
+
$META: null,
|
|
914
|
+
$MERGE: null,
|
|
915
|
+
$EACH: null,
|
|
916
|
+
$PACK: null,
|
|
917
|
+
|
|
918
|
+
$STRING: (state, val, current)=>{
|
|
919
|
+
const { mode, key, parent } = state
|
|
920
|
+
let out = getprop(current, key)
|
|
921
|
+
|
|
922
|
+
let t = typeof out
|
|
923
|
+
if(S.string === t) {
|
|
924
|
+
if(S.empty === out) {
|
|
925
|
+
state.errs.push('Empty string at '+pathify(state.path))
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
return out
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
state.errs.push(invalidTypeMsg(state.path,S.string,t,out))
|
|
933
|
+
return
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
$NUMBER: (state, val, current)=>{
|
|
938
|
+
const { mode, key, parent } = state
|
|
939
|
+
let out = getprop(current, key)
|
|
940
|
+
|
|
941
|
+
let t = typeof out
|
|
942
|
+
if(S.number !== t) {
|
|
943
|
+
state.errs.push(invalidTypeMsg(state.path,S.number,t,out))
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return out
|
|
948
|
+
},
|
|
949
|
+
|
|
950
|
+
$BOOLEAN: (state, val, current)=>{
|
|
951
|
+
const { mode, key, parent } = state
|
|
952
|
+
let out = getprop(current, key)
|
|
953
|
+
|
|
954
|
+
let t = typeof out
|
|
955
|
+
if(S.boolean !== t) {
|
|
956
|
+
state.errs.push(invalidTypeMsg(state.path,S.boolean,t,out))
|
|
957
|
+
return
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return out
|
|
961
|
+
},
|
|
962
|
+
|
|
963
|
+
$OBJECT: (state, val, current)=>{
|
|
964
|
+
const { mode, key, parent } = state
|
|
965
|
+
let out = getprop(current, key)
|
|
966
|
+
|
|
967
|
+
let t = typeof out
|
|
968
|
+
|
|
969
|
+
if(null == out || S.object !== t) {
|
|
970
|
+
state.errs.push(invalidTypeMsg(state.path,S.object,t,out))
|
|
971
|
+
return
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return out
|
|
975
|
+
},
|
|
976
|
+
|
|
977
|
+
$ARRAY: (state, val, current)=>{
|
|
978
|
+
const { mode, key, parent } = state
|
|
979
|
+
let out = getprop(current, key)
|
|
980
|
+
|
|
981
|
+
let t = typeof out
|
|
982
|
+
if(!Array.isArray(out)) {
|
|
983
|
+
state.errs.push(invalidTypeMsg(state.path,S.array,t,out))
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return out
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
$FUNCTION: (state, val, current)=>{
|
|
991
|
+
const { mode, key, parent } = state
|
|
992
|
+
let out = getprop(current, key)
|
|
993
|
+
|
|
994
|
+
let t = typeof out
|
|
995
|
+
if(S.function !== t) {
|
|
996
|
+
state.errs.push(invalidTypeMsg(state.path,S.function,t,out))
|
|
997
|
+
return
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return out
|
|
1001
|
+
},
|
|
1002
|
+
|
|
1003
|
+
$ANY: (state, val, current)=>{
|
|
1004
|
+
const { mode, key, parent } = state
|
|
1005
|
+
let out = getprop(current, key)
|
|
1006
|
+
return out
|
|
1007
|
+
},
|
|
1008
|
+
|
|
1009
|
+
$CHILD: (state, val, current)=>{
|
|
1010
|
+
const { mode, key, parent, keys, path } = state
|
|
1011
|
+
|
|
1012
|
+
if(S.MKEYPRE === mode) {
|
|
1013
|
+
const child = parent[key]
|
|
1014
|
+
const pkey = path[path.length-2]
|
|
1015
|
+
const tval = current[pkey]
|
|
1016
|
+
|
|
1017
|
+
const ckeys = keysof(tval)
|
|
1018
|
+
for(let ckey of ckeys) {
|
|
1019
|
+
parent[ckey] = clone(child)
|
|
1020
|
+
keys.push(ckey)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
delete parent[key]
|
|
1024
|
+
|
|
1025
|
+
}
|
|
1026
|
+
else if(S.MVAL === mode) {
|
|
1027
|
+
if(!islist(parent)) {
|
|
1028
|
+
state.errs.push('Invalid $CHILD as value')
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const child = parent[1]
|
|
1032
|
+
|
|
1033
|
+
if(UNDEF === current) {
|
|
1034
|
+
parent.length = 0
|
|
1035
|
+
return UNDEF
|
|
1036
|
+
}
|
|
1037
|
+
else if(!islist(current)) {
|
|
1038
|
+
state.errs.push(invalidTypeMsg(
|
|
1039
|
+
state.path.slice(0,state.path.length-1),S.array,typeof current,current))
|
|
1040
|
+
state.keyI = parent.length
|
|
1041
|
+
return current
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
current.map((n,i)=>parent[i]=clone(child))
|
|
1045
|
+
parent.length = current.length
|
|
1046
|
+
state.keyI = 0
|
|
1047
|
+
return current[0]
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
|
|
1052
|
+
$ONE: (state, val, current)=>{
|
|
1053
|
+
const { mode, key, parent, keys, path, nodes } = state
|
|
1054
|
+
|
|
1055
|
+
if(S.MVAL === mode) {
|
|
1056
|
+
state.keyI = state.keys.length
|
|
1057
|
+
|
|
1058
|
+
let tvals = parent.slice(1)
|
|
1059
|
+
|
|
1060
|
+
for(let tval of tvals) {
|
|
1061
|
+
let terrs = []
|
|
1062
|
+
validate(current,tval,UNDEF,terrs)
|
|
1063
|
+
|
|
1064
|
+
const grandparent = nodes[nodes.length-2]
|
|
1065
|
+
const grandkey = path[path.length-2]
|
|
1066
|
+
|
|
1067
|
+
if(isnode(grandparent)) {
|
|
1068
|
+
if(0===terrs.length) {
|
|
1069
|
+
setprop(grandparent, grandkey, current)
|
|
1070
|
+
return
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
setprop(grandparent, grandkey, UNDEF)
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const valdesc = tvals
|
|
1079
|
+
.map(v=>stringify(v))
|
|
1080
|
+
.join(', ')
|
|
1081
|
+
.replace(/`\$([A-Z]+)`/g, (m,p1)=>p1.toLowerCase())
|
|
1082
|
+
|
|
1083
|
+
state.errs.push(invalidTypeMsg(
|
|
1084
|
+
state.path.slice(0,state.path.length-1),
|
|
1085
|
+
'one of '+valdesc,
|
|
1086
|
+
typeof current, current))
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
|
|
1090
|
+
...(extra||{})
|
|
1091
|
+
},
|
|
1092
|
+
|
|
1093
|
+
(key,
|
|
1094
|
+
val,
|
|
1095
|
+
parent,
|
|
1096
|
+
state,
|
|
1097
|
+
current,
|
|
1098
|
+
store)=>{
|
|
1099
|
+
const cval = isnode(current) ? current[key] : UNDEF
|
|
1100
|
+
|
|
1101
|
+
if(UNDEF === cval) {
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const pval = parent[key]
|
|
1106
|
+
|
|
1107
|
+
const t = typeof pval
|
|
1108
|
+
|
|
1109
|
+
if(S.string === t && pval.includes('$')) {
|
|
1110
|
+
return
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
//const t = typeof val
|
|
1114
|
+
const ct = typeof cval
|
|
1115
|
+
|
|
1116
|
+
if(t !== ct && UNDEF !== pval) {
|
|
1117
|
+
state.errs.push(invalidTypeMsg(state.path,t,ct,cval))
|
|
1118
|
+
}
|
|
1119
|
+
else if(ismap(cval)) {
|
|
1120
|
+
const ckeys = keysof(cval)
|
|
1121
|
+
const pkeys = keysof(pval)
|
|
1122
|
+
|
|
1123
|
+
// Empty spec object {} means object can be open (any keys).
|
|
1124
|
+
if(0 < pkeys.length && true !== getprop(pval,'`$OPEN`')) {
|
|
1125
|
+
const badkeys = []
|
|
1126
|
+
for(ckey of ckeys) {
|
|
1127
|
+
if(!haskey(val, ckey)) {
|
|
1128
|
+
badkeys.push(ckey)
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if(0 < badkeys.length) {
|
|
1132
|
+
state.errs.push('Unexpected keys at '+pathify(state.path)+
|
|
1133
|
+
': '+badkeys.join(', '))
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
merge([pval, cval])
|
|
1138
|
+
if(isnode(pval)) {
|
|
1139
|
+
delete pval['`$OPEN`']
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
else if(islist(cval)) {
|
|
1144
|
+
if(!islist(val)) {
|
|
1145
|
+
state.errs.push(invalidTypeMsg(state.path,t,ct,cval))
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
// Spec value was a default, copy over data
|
|
1150
|
+
parent[key] = cval
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
if(0 < errs.length && null == collecterrs) {
|
|
1156
|
+
throw new Error('Invalid data: '+errs.join('\n'))
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return out
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
function invalidTypeMsg(path, type, vt, v) {
|
|
1164
|
+
return 'Expected '+type+' at '+pathify(path)+', found '+(null != v?vt+': ':'')+v
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function pathify(val, from) {
|
|
1168
|
+
from = null == from ? 1 : -1 < from ? from : 1
|
|
1169
|
+
if(Array.isArray(val)) {
|
|
1170
|
+
let path = val.slice(from)
|
|
1171
|
+
if(0 === path.length) {
|
|
1172
|
+
return '<root>'
|
|
1173
|
+
}
|
|
1174
|
+
return path.join('.')
|
|
1175
|
+
}
|
|
1176
|
+
return '<unknown-path>'
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
module.exports = {
|
|
1181
|
+
clone,
|
|
1182
|
+
escre,
|
|
1183
|
+
escurl,
|
|
1184
|
+
getpath,
|
|
1185
|
+
getprop,
|
|
1186
|
+
inject,
|
|
1187
|
+
isempty,
|
|
1188
|
+
iskey,
|
|
1189
|
+
islist,
|
|
1190
|
+
ismap,
|
|
1191
|
+
isnode,
|
|
1192
|
+
isfunc,
|
|
1193
|
+
haskey,
|
|
1194
|
+
keysof,
|
|
1195
|
+
items,
|
|
1196
|
+
merge,
|
|
1197
|
+
setprop,
|
|
1198
|
+
stringify,
|
|
1199
|
+
transform,
|
|
1200
|
+
walk,
|
|
1201
|
+
joinurl,
|
|
1202
|
+
validate,
|
|
1203
|
+
}
|