@voxgig/sdkgen 0.22.0 → 0.23.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/bin/voxgig-sdkgen +12 -4
- package/dist/action/action.d.ts +4 -0
- package/dist/action/action.js +35 -0
- package/dist/action/action.js.map +1 -0
- package/dist/action/feature.d.ts +4 -2
- package/dist/action/feature.js +57 -38
- package/dist/action/feature.js.map +1 -1
- package/dist/action/target.d.ts +4 -2
- package/dist/action/target.js +136 -45
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/Entity.js +5 -0
- package/dist/cmp/Entity.js.map +1 -1
- package/dist/cmp/Feature.js +5 -0
- package/dist/cmp/Feature.js.map +1 -1
- package/dist/cmp/Main.js +4 -1
- package/dist/cmp/Main.js.map +1 -1
- package/dist/cmp/Top.d.ts +2 -0
- package/dist/cmp/Top.js +23 -0
- package/dist/cmp/Top.js.map +1 -0
- package/dist/sdkgen.d.ts +10 -3
- package/dist/sdkgen.js +53 -8
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utility.d.ts +3 -3
- package/dist/utility.js +7 -8
- package/dist/utility.js.map +1 -1
- package/package.json +6 -10
- package/project/.sdk/model/feature/README.md +2 -0
- package/project/.sdk/model/feature/log.jsonic +7 -4
- package/project/.sdk/model/feature/test.jsonic +26 -0
- package/project/.sdk/model/target/ts.jsonic +1 -0
- package/project/.sdk/src/cmp/ts/Config_ts.ts +7 -2
- package/project/.sdk/src/cmp/ts/EntityOperation_ts.ts +67 -0
- package/project/.sdk/src/cmp/ts/EntityTest_ts.ts +180 -0
- package/project/.sdk/src/cmp/ts/Entity_ts.ts +41 -66
- package/project/.sdk/src/cmp/ts/MainEntity_ts.ts +22 -0
- package/project/.sdk/src/cmp/ts/Main_ts.ts +52 -53
- package/project/.sdk/src/cmp/ts/Package_ts.ts +12 -7
- package/project/.sdk/src/cmp/ts/ReadmeInstall_ts.ts +19 -0
- package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +24 -0
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +13 -0
- package/project/.sdk/src/cmp/ts/TestMain_ts.ts +19 -0
- package/project/.sdk/src/cmp/ts/Test_ts.ts +20 -0
- package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +8 -1
- package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.ts +164 -0
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +37 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +91 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +92 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +96 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +96 -0
- package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +95 -0
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +107 -0
- package/project/.sdk/src/cmp/ts/utility_ts.ts +10 -0
- package/project/.sdk/tm/ts/src/feature/base/BaseFeature.ts +43 -0
- package/project/.sdk/tm/ts/src/feature/log/LogFeature.ts +109 -0
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +159 -0
- package/project/.sdk/tm/ts/src/tsconfig.json +1 -1
- package/project/.sdk/tm/ts/src/types.ts +114 -0
- package/project/.sdk/tm/ts/src/utility/AddfeatureUtility.ts +47 -0
- package/project/.sdk/tm/ts/src/utility/AuthUtility.ts +42 -0
- package/project/.sdk/tm/ts/src/utility/BodyUtility.ts +29 -0
- package/project/.sdk/tm/ts/src/utility/CleanUtility.ts +50 -0
- package/project/.sdk/tm/ts/src/utility/ContextUtility.ts +67 -0
- package/project/.sdk/tm/ts/src/utility/DoneUtility.ts +28 -0
- package/project/.sdk/tm/ts/src/utility/ErrorUtility.ts +59 -0
- package/project/.sdk/tm/ts/src/utility/FeaturehookUtility.ts +26 -0
- package/project/.sdk/tm/ts/src/utility/FetcherUtility.ts +17 -0
- package/project/.sdk/tm/ts/src/utility/FindparamUtility.ts +54 -0
- package/project/.sdk/tm/ts/src/utility/FullurlUtility.ts +46 -0
- package/project/.sdk/tm/ts/src/utility/HeadersUtility.ts +24 -0
- package/project/.sdk/tm/ts/src/utility/InitfeatureUtility.ts +13 -0
- package/project/.sdk/tm/ts/src/utility/JoinurlUtility.ts +15 -0
- package/project/.sdk/tm/ts/src/utility/MethodUtility.ts +25 -0
- package/project/.sdk/tm/ts/src/utility/OperatorUtility.ts +90 -0
- package/project/.sdk/tm/ts/src/utility/OptionsUtility.ts +72 -0
- package/project/.sdk/tm/ts/src/utility/ParamsUtility.ts +37 -0
- package/project/.sdk/tm/ts/src/utility/QueryUtility.ts +27 -0
- package/project/.sdk/tm/ts/src/utility/ReqformUtility.ts +33 -0
- package/project/.sdk/tm/ts/src/utility/RequestUtility.ts +66 -0
- package/project/.sdk/tm/ts/src/utility/ResbasicUtility.ts +34 -0
- package/project/.sdk/tm/ts/src/utility/ResbodyUtility.ts +19 -0
- package/project/.sdk/tm/ts/src/utility/ResformUtility.ts +36 -0
- package/project/.sdk/tm/ts/src/utility/ResheadersUtility.ts +23 -0
- package/project/.sdk/tm/ts/src/utility/ResponseUtility.ts +30 -0
- package/project/.sdk/tm/ts/src/utility/ResultUtility.ts +36 -0
- package/project/.sdk/tm/ts/src/utility/SpecUtility.ts +61 -0
- package/project/.sdk/tm/ts/src/utility/StructUtility.ts +2499 -0
- package/project/.sdk/tm/ts/src/utility/Utility.ts +88 -0
- package/project/.sdk/tm/ts/test/exists.test.ts +17 -0
- package/project/.sdk/tm/ts/test/runner.ts +402 -0
- package/project/.sdk/tm/ts/test/tsconfig.json +1 -1
- package/project/.sdk/tm/ts/test/utility/Custom.test.ts +62 -0
- package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +244 -0
- package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +678 -0
- package/project/.sdk/tm/ts/test/utility/index.ts +9 -0
- package/project/.sdk/tm/ts/test/utility.ts +86 -0
- package/src/action/action.ts +54 -0
- package/src/action/feature.ts +83 -47
- package/src/action/target.ts +173 -53
- package/src/cmp/Entity.ts +6 -0
- package/src/cmp/Feature.ts +6 -3
- package/src/cmp/Main.ts +4 -1
- package/src/sdkgen.ts +86 -11
- package/src/types.ts +33 -0
- package/src/utility.ts +5 -3
- package/project/.sdk/model/feature/limit.jsonic +0 -12
- package/project/.sdk/model/feature/page.jsonic +0 -9
- package/project/.sdk/model/feature/telemetry.jsonic +0 -10
- package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.js +0 -79
- package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.js +0 -61
- package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.js +0 -57
- package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.js +0 -61
- package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.js +0 -61
- package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.js +0 -61
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.js +0 -67
- package/project/.sdk/tm/ts/test/README.md~ +0 -2
|
@@ -0,0 +1,2499 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Voxgig Ltd. MIT LICENSE. */
|
|
2
|
+
|
|
3
|
+
/* Voxgig Struct
|
|
4
|
+
* =============
|
|
5
|
+
*
|
|
6
|
+
* Utility functions to manipulate in-memory JSON-like data
|
|
7
|
+
* structures. These structures assumed to be composed of nested
|
|
8
|
+
* "nodes", where a node is a list or map, and has named or indexed
|
|
9
|
+
* fields. The general design principle is "by-example". Transform
|
|
10
|
+
* specifications mirror the desired output. This implementation is
|
|
11
|
+
* designed for porting to multiple language, and to be tolerant of
|
|
12
|
+
* undefined values.
|
|
13
|
+
*
|
|
14
|
+
* Main utilities
|
|
15
|
+
* - getpath: get the value at a key path deep inside an object.
|
|
16
|
+
* - merge: merge multiple nodes, overriding values in earlier nodes.
|
|
17
|
+
* - walk: walk a node tree, applying a function at each node and leaf.
|
|
18
|
+
* - inject: inject values from a data store into a new data structure.
|
|
19
|
+
* - transform: transform a data structure to an example structure.
|
|
20
|
+
* - validate: valiate a data structure against a shape specification.
|
|
21
|
+
*
|
|
22
|
+
* Minor utilities
|
|
23
|
+
* - isnode, islist, ismap, iskey, isfunc: identify value kinds.
|
|
24
|
+
* - isempty: undefined values, or empty nodes.
|
|
25
|
+
* - keysof: sorted list of node keys (ascending).
|
|
26
|
+
* - haskey: true if key value is defined.
|
|
27
|
+
* - clone: create a copy of a JSON-like data structure.
|
|
28
|
+
* - items: list entries of a map or list as [key, value] pairs.
|
|
29
|
+
* - getprop: safely get a property value by key.
|
|
30
|
+
* - setprop: safely set a property value by key.
|
|
31
|
+
* - stringify: human-friendly string version of a value.
|
|
32
|
+
* - escre: escape a regular expresion string.
|
|
33
|
+
* - escurl: escape a url.
|
|
34
|
+
* - joinurl: join parts of a url, merging forward slashes.
|
|
35
|
+
*
|
|
36
|
+
* This set of functions and supporting utilities is designed to work
|
|
37
|
+
* uniformly across many languages, meaning that some code that may be
|
|
38
|
+
* functionally redundant in specific languages is still retained to
|
|
39
|
+
* keep the code human comparable.
|
|
40
|
+
*
|
|
41
|
+
* NOTE: In this code JSON nulls are in general *not* considered the
|
|
42
|
+
* same as the undefined value in the given language. However most
|
|
43
|
+
* JSON parsers do use the undefined value to represent JSON
|
|
44
|
+
* null. This is ambiguous as JSON null is a separate value, not an
|
|
45
|
+
* undefined value. You should convert such values to a special value
|
|
46
|
+
* to represent JSON null, if this ambiguity creates issues
|
|
47
|
+
* (thankfully in most APIs, JSON nulls are not used). For example,
|
|
48
|
+
* the unit tests use the string "__NULL__" where necessary.
|
|
49
|
+
*
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
// String constants are explicitly defined.
|
|
54
|
+
|
|
55
|
+
// Mode value for inject step.
|
|
56
|
+
const S_MKEYPRE = 'key:pre'
|
|
57
|
+
const S_MKEYPOST = 'key:post'
|
|
58
|
+
const S_MVAL = 'val'
|
|
59
|
+
const S_MKEY = 'key'
|
|
60
|
+
|
|
61
|
+
// Special keys.
|
|
62
|
+
const S_BKEY = '`$KEY`'
|
|
63
|
+
const S_BANNO = '`$ANNO`'
|
|
64
|
+
const S_BEXACT = '`$EXACT`'
|
|
65
|
+
|
|
66
|
+
const S_DKEY = '$KEY'
|
|
67
|
+
const S_DTOP = '$TOP'
|
|
68
|
+
const S_DERRS = '$ERRS'
|
|
69
|
+
const S_DSPEC = '$SPEC'
|
|
70
|
+
|
|
71
|
+
// General strings.
|
|
72
|
+
const S_array = 'array'
|
|
73
|
+
const S_base = 'base'
|
|
74
|
+
const S_boolean = 'boolean'
|
|
75
|
+
const S_function = 'function'
|
|
76
|
+
const S_number = 'number'
|
|
77
|
+
const S_object = 'object'
|
|
78
|
+
const S_string = 'string'
|
|
79
|
+
const S_null = 'null'
|
|
80
|
+
const S_key = 'key'
|
|
81
|
+
const S_MT = ''
|
|
82
|
+
const S_BT = '`'
|
|
83
|
+
const S_DS = '$'
|
|
84
|
+
const S_DT = '.'
|
|
85
|
+
const S_CN = ':'
|
|
86
|
+
const S_FS = '/'
|
|
87
|
+
const S_OS = '['
|
|
88
|
+
const S_CS = ']'
|
|
89
|
+
const S_SP = ' '
|
|
90
|
+
const S_KEY = 'KEY'
|
|
91
|
+
const S_VIZ = ': '
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
// The standard undefined value for this language.
|
|
95
|
+
const UNDEF = undefined
|
|
96
|
+
|
|
97
|
+
// Private marker to indicate a skippable value.
|
|
98
|
+
const SKIP = { '`$SKIP`': true }
|
|
99
|
+
|
|
100
|
+
// Regular expression constants
|
|
101
|
+
const R_INTEGER_KEY = /^[-0-9]+$/ // Match integer keys (including <0).
|
|
102
|
+
const R_ESCAPE_REGEXP = /[.*+?^${}()|[\]\\]/g // Chars that need escaping in regexp.
|
|
103
|
+
const R_TRAILING_SLASH = /\/+$/ // Trailing slashes in URLs.
|
|
104
|
+
const R_LEADING_TRAILING_SLASH = /([^\/])\/+/ // Multiple slashes in URL middle.
|
|
105
|
+
const R_LEADING_SLASH = /^\/+/ // Leading slashes in URLs.
|
|
106
|
+
const R_QUOTES = /"/g // Double quotes for removal.
|
|
107
|
+
const R_DOT = /\./g // Dots in path strings.
|
|
108
|
+
const R_FUNCTION_REF = /^`\$FUNCTION:([0-9]+)`$/ // Function reference in clone.
|
|
109
|
+
const R_META_PATH = /^([^$]+)\$([=~])(.+)$/ // Meta path syntax.
|
|
110
|
+
const R_DOUBLE_DOLLAR = /\$\$/g // Double dollar escape sequence.
|
|
111
|
+
const R_TRANSFORM_NAME = /`\$([A-Z]+)`/g // Transform command names.
|
|
112
|
+
const R_INJECTION_FULL = /^`(\$[A-Z]+|[^`]*)[0-9]*`$/ // Full string injection pattern.
|
|
113
|
+
const R_BT_ESCAPE = /\$BT/g // Backtick escape sequence.
|
|
114
|
+
const R_DS_ESCAPE = /\$DS/g // Dollar sign escape sequence.
|
|
115
|
+
const R_INJECTION_PARTIAL = /`([^`]+)`/g // Partial string injection pattern.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
// Keys are strings for maps, or integers for lists.
|
|
119
|
+
type PropKey = string | number
|
|
120
|
+
|
|
121
|
+
// Type that can be indexed by both string and number keys.
|
|
122
|
+
type Indexable = { [key: string]: any } & { [key: number]: any }
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
// For each key in a node (map or list), perform value injections in
|
|
126
|
+
// three phases: on key value, before child, and then on key value again.
|
|
127
|
+
// This mode is passed via the Injection structure.
|
|
128
|
+
type InjectMode = 'key:pre' | 'key:post' | 'val'
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// Handle value injections using backtick escape sequences:
|
|
132
|
+
// - `a.b.c`: insert value at {a:{b:{c:1}}}
|
|
133
|
+
// - `$FOO`: apply transform FOO
|
|
134
|
+
type Injector = (
|
|
135
|
+
inj: Injection, // Injection state.
|
|
136
|
+
val: any, // Injection value specification.
|
|
137
|
+
ref: string, // Original injection reference string.
|
|
138
|
+
store: any, // Current source root value.
|
|
139
|
+
) => any
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
// Apply a custom modification to injections.
|
|
143
|
+
type Modify = (
|
|
144
|
+
val: any, // Value.
|
|
145
|
+
key?: PropKey, // Value key, if any,
|
|
146
|
+
parent?: any, // Parent node, if any.
|
|
147
|
+
inj?: Injection, // Injection state, if any.
|
|
148
|
+
store?: any, // Store, if any
|
|
149
|
+
) => void
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
// Function applied to each node and leaf when walking a node structure depth first.
|
|
153
|
+
// For {a:{b:1}} the call sequence args will be: b, 1, {b:1}, [a,b].
|
|
154
|
+
type WalkApply = (
|
|
155
|
+
// Map keys are strings, list keys are numbers, top key is UNDEF
|
|
156
|
+
key: string | number | undefined,
|
|
157
|
+
val: any,
|
|
158
|
+
parent: any,
|
|
159
|
+
path: string[]
|
|
160
|
+
) => any
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// Value is a node - defined, and a map (hash) or list (array).
|
|
165
|
+
// NOTE: typescript
|
|
166
|
+
// things
|
|
167
|
+
function isnode(val: any): val is Indexable {
|
|
168
|
+
return null != val && S_object == typeof val
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// Value is a defined map (hash) with string keys.
|
|
173
|
+
function ismap(val: any): val is { [key: string]: any } {
|
|
174
|
+
return null != val && S_object == typeof val && !Array.isArray(val)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// Value is a defined list (array) with integer keys (indexes).
|
|
179
|
+
function islist(val: any): val is any[] {
|
|
180
|
+
return Array.isArray(val)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
// Value is a defined string (non-empty) or integer key.
|
|
185
|
+
function iskey(key: any): key is PropKey {
|
|
186
|
+
const keytype = typeof key
|
|
187
|
+
return (S_string === keytype && S_MT !== key) || S_number === keytype
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
// Check for an "empty" value - undefined, empty string, array, object.
|
|
192
|
+
function isempty(val: any) {
|
|
193
|
+
return null == val || S_MT === val ||
|
|
194
|
+
(Array.isArray(val) && 0 === val.length) ||
|
|
195
|
+
(S_object === typeof val && 0 === Object.keys(val).length)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
// Value is a function.
|
|
200
|
+
function isfunc(val: any): val is Function {
|
|
201
|
+
return S_function === typeof val
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
// The integer size of the value. For arrays and strings, the length,
|
|
206
|
+
// for numbers, the integer part, for boolean, true is 1 and falso 0, for all other values, 0.
|
|
207
|
+
function size(val: any): number {
|
|
208
|
+
if (islist(val)) {
|
|
209
|
+
return val.length
|
|
210
|
+
}
|
|
211
|
+
else if (ismap(val)) {
|
|
212
|
+
return Object.keys(val).length
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const valtype = typeof val
|
|
216
|
+
|
|
217
|
+
if (S_string == valtype) {
|
|
218
|
+
return val.length
|
|
219
|
+
}
|
|
220
|
+
else if (S_number == typeof val) {
|
|
221
|
+
return Math.floor(val)
|
|
222
|
+
}
|
|
223
|
+
else if (S_boolean == typeof val) {
|
|
224
|
+
return true === val ? 1 : 0
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
return 0
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
// Extract part of an array or string into a new value, from the start point to the end point.
|
|
233
|
+
// If no end is specified, extract to the full length of the value. Negative arguments count
|
|
234
|
+
// from the end of the value. For numbers, perform min and max bounding, where start is
|
|
235
|
+
// inclusive, and end is *exclusive*.
|
|
236
|
+
function slice<V extends any>(val: V, start?: number, end?: number): V {
|
|
237
|
+
if (S_number === typeof val) {
|
|
238
|
+
start = null == start || S_number !== typeof start ? Number.MIN_SAFE_INTEGER : start
|
|
239
|
+
end = (null == end || S_number !== typeof end ? Number.MAX_SAFE_INTEGER : end) - 1
|
|
240
|
+
return Math.min(Math.max(val as number, start), end) as V
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const vlen = size(val)
|
|
244
|
+
|
|
245
|
+
if (null != end && null == start) {
|
|
246
|
+
start = 0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (null != start) {
|
|
250
|
+
if (start < 0) {
|
|
251
|
+
end = vlen + start
|
|
252
|
+
if (end < 0) {
|
|
253
|
+
end = 0
|
|
254
|
+
}
|
|
255
|
+
start = 0
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
else if (null != end) {
|
|
259
|
+
if (end < 0) {
|
|
260
|
+
end = vlen + end
|
|
261
|
+
if (end < 0) {
|
|
262
|
+
end = 0
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (vlen < end) {
|
|
266
|
+
end = vlen
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
else {
|
|
271
|
+
end = vlen
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (vlen < start) {
|
|
275
|
+
start = vlen
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (-1 < start && start <= end && end <= vlen) {
|
|
279
|
+
if (islist(val)) {
|
|
280
|
+
val = val.slice(start, end) as V
|
|
281
|
+
}
|
|
282
|
+
else if (S_string === typeof val) {
|
|
283
|
+
val = (val as string).substring(start, end) as V
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
if (islist(val)) {
|
|
288
|
+
val = [] as V
|
|
289
|
+
}
|
|
290
|
+
else if (S_string === typeof val) {
|
|
291
|
+
val = S_MT as V
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return val
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
function pad(str: any, padding?: number, padchar?: string): string {
|
|
301
|
+
str = S_string === typeof str ? str : stringify(str)
|
|
302
|
+
padding = null == padding ? 44 : padding
|
|
303
|
+
padchar = null == padchar ? S_SP : ((padchar + S_SP)[0])
|
|
304
|
+
return -1 < padding ? str.padEnd(padding, padchar) : str.padStart(0 - padding, padchar)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
// Determine the type of a value as a string.
|
|
309
|
+
// Returns one of: 'null', 'string', 'number', 'boolean', 'function', 'array', 'object'
|
|
310
|
+
// Normalizes and simplifies JavaScript's type system for consistency.
|
|
311
|
+
function typify(value: any): string {
|
|
312
|
+
if (value === null || value === undefined) {
|
|
313
|
+
return S_null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const type = typeof value
|
|
317
|
+
|
|
318
|
+
if (Array.isArray(value)) {
|
|
319
|
+
return S_array
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (type === 'object') {
|
|
323
|
+
return S_object
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return type
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
// Get a list element. The key should be an integer, or a string
|
|
331
|
+
// that can parse to an integer only. Negative integers count from the end of the list.
|
|
332
|
+
function getelem(val: any, key: any, alt?: any) {
|
|
333
|
+
let out = UNDEF
|
|
334
|
+
|
|
335
|
+
if (UNDEF === val || UNDEF === key) {
|
|
336
|
+
return alt
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (islist(val)) {
|
|
340
|
+
let nkey = parseInt(key)
|
|
341
|
+
if (Number.isInteger(nkey) && ('' + key).match(R_INTEGER_KEY)) {
|
|
342
|
+
if (nkey < 0) {
|
|
343
|
+
key = val.length + nkey
|
|
344
|
+
}
|
|
345
|
+
out = val[key]
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (UNDEF === out) {
|
|
350
|
+
return alt
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return out
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
// Safely get a property of a node. Undefined arguments return undefined.
|
|
358
|
+
// If the key is not found, return the alternative value, if any.
|
|
359
|
+
function getprop(val: any, key: any, alt?: any) {
|
|
360
|
+
let out = alt
|
|
361
|
+
|
|
362
|
+
if (UNDEF === val || UNDEF === key) {
|
|
363
|
+
return alt
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (isnode(val)) {
|
|
367
|
+
out = val[key]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (UNDEF === out) {
|
|
371
|
+
return alt
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return out
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
// Convert different types of keys to string representation.
|
|
379
|
+
// String keys are returned as is.
|
|
380
|
+
// Number keys are converted to strings.
|
|
381
|
+
// Floats are truncated to integers.
|
|
382
|
+
// Booleans, objects, arrays, null, undefined all return empty string.
|
|
383
|
+
function strkey(key: any = UNDEF): string {
|
|
384
|
+
if (UNDEF === key) {
|
|
385
|
+
return S_MT
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (typeof key === S_string) {
|
|
389
|
+
return key
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (typeof key === S_boolean) {
|
|
393
|
+
return S_MT
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (typeof key === S_number) {
|
|
397
|
+
return key % 1 === 0 ? String(key) : String(Math.floor(key))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return S_MT
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
// Sorted keys of a map, or indexes of a list.
|
|
405
|
+
function keysof(val: any): string[] {
|
|
406
|
+
return !isnode(val) ? [] :
|
|
407
|
+
ismap(val) ? Object.keys(val).sort() : (val as any).map((_n: any, i: number) => S_MT + i)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// Value of property with name key in node val is defined.
|
|
412
|
+
function haskey(val: any, key: any) {
|
|
413
|
+
return UNDEF !== getprop(val, key)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
// List the sorted keys of a map or list as an array of tuples of the form [key, value].
|
|
418
|
+
// NOTE: Unlike keysof, list indexes are returned as numbers.
|
|
419
|
+
function items(val: any): [number | string, any][] {
|
|
420
|
+
return keysof(val).map((k: any) => [k, val[k]])
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
// Escape regular expression.
|
|
425
|
+
function escre(s: string) {
|
|
426
|
+
s = null == s ? S_MT : s
|
|
427
|
+
return s.replace(R_ESCAPE_REGEXP, '\\$&')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
// Escape URLs.
|
|
432
|
+
function escurl(s: string) {
|
|
433
|
+
s = null == s ? S_MT : s
|
|
434
|
+
return encodeURIComponent(s)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
// Concatenate url part strings, merging forward slashes as needed.
|
|
439
|
+
function joinurl(sarr: any[]) {
|
|
440
|
+
return sarr
|
|
441
|
+
.filter(s => null != s && S_MT !== s)
|
|
442
|
+
.map((s, i) => 0 === i ? s.replace(R_TRAILING_SLASH, S_MT) :
|
|
443
|
+
s.replace(R_LEADING_TRAILING_SLASH, '$1/')
|
|
444
|
+
.replace(R_LEADING_SLASH, S_MT)
|
|
445
|
+
.replace(R_TRAILING_SLASH, S_MT))
|
|
446
|
+
.filter(s => S_MT !== s)
|
|
447
|
+
.join(S_FS)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
// Output JSON in a "standard" format, with 2 space indents, each property on a new line,
|
|
452
|
+
// and spaces after {[: and before ]}. Any "wierd" values (NaN, etc) are output as null.
|
|
453
|
+
// In general, the behaivor of of JavaScript's JSON.stringify(val,null,2) is followed.
|
|
454
|
+
function jsonify(val: any, flags?: { indent?: number, offset?: number }) {
|
|
455
|
+
let str = S_null
|
|
456
|
+
if (null != val) {
|
|
457
|
+
const indent = getprop(flags, 'indent', 2)
|
|
458
|
+
str = JSON.stringify(val, null, indent)
|
|
459
|
+
if (UNDEF === str) {
|
|
460
|
+
str = S_null
|
|
461
|
+
}
|
|
462
|
+
const offset = getprop(flags, 'offset', 0)
|
|
463
|
+
if (0 < offset) {
|
|
464
|
+
// Left offset entire indented JSON so that it aligns with surrounding code
|
|
465
|
+
// indented by offset.
|
|
466
|
+
str = '{\n' + str.split('\n').slice(1)
|
|
467
|
+
.map(n => pad(n, 0 - offset - size(n)))
|
|
468
|
+
.join('\n')
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return str
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
// Safely stringify a value for humans (NOT JSON!).
|
|
477
|
+
function stringify(val: any, maxlen?: number, pretty?: any): string {
|
|
478
|
+
let valstr = S_MT
|
|
479
|
+
pretty = !!pretty
|
|
480
|
+
|
|
481
|
+
if (UNDEF === val) {
|
|
482
|
+
return pretty ? '<>' : valstr
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (S_string === typeof val) {
|
|
486
|
+
valstr = val
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
try {
|
|
490
|
+
valstr = JSON.stringify(val, function(_key: string, val: any) {
|
|
491
|
+
if (
|
|
492
|
+
val !== null &&
|
|
493
|
+
typeof val === "object" &&
|
|
494
|
+
!Array.isArray(val)
|
|
495
|
+
) {
|
|
496
|
+
const sortedObj: any = {}
|
|
497
|
+
for (const k of Object.keys(val).sort()) {
|
|
498
|
+
sortedObj[k] = val[k]
|
|
499
|
+
}
|
|
500
|
+
return sortedObj
|
|
501
|
+
}
|
|
502
|
+
return val
|
|
503
|
+
})
|
|
504
|
+
valstr = valstr.replace(R_QUOTES, S_MT)
|
|
505
|
+
}
|
|
506
|
+
catch (err: any) {
|
|
507
|
+
valstr = S_MT + val
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (null != maxlen && -1 < maxlen) {
|
|
512
|
+
let js = valstr.substring(0, maxlen)
|
|
513
|
+
valstr = maxlen < valstr.length ? (js.substring(0, maxlen - 3) + '...') : valstr
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (pretty) {
|
|
517
|
+
// Indicate deeper JSON levels with different terminal colors (simplistic wrt strings).
|
|
518
|
+
let c = [81, 118, 213, 39, 208, 201, 45, 190, 129, 51, 160, 121, 226, 33, 207, 69]
|
|
519
|
+
.map(n => `\x1b[38;5;${n}m`),
|
|
520
|
+
r = '\x1b[0m', d = 0, o = c[0], t = o
|
|
521
|
+
for (const ch of valstr) {
|
|
522
|
+
if (ch === '{' || ch === '[') {
|
|
523
|
+
d++; o = c[d % c.length]; t += o + ch
|
|
524
|
+
} else if (ch === '}' || ch === ']') {
|
|
525
|
+
t += o + ch; d--; o = c[d % c.length]
|
|
526
|
+
} else {
|
|
527
|
+
t += o + ch
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return t + r
|
|
531
|
+
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return valstr
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
// Build a human friendly path string.
|
|
539
|
+
function pathify(val: any, startin?: number, endin?: number) {
|
|
540
|
+
let pathstr: string | undefined = UNDEF
|
|
541
|
+
|
|
542
|
+
let path: any[] | undefined = islist(val) ? val :
|
|
543
|
+
S_string == typeof val ? [val] :
|
|
544
|
+
S_number == typeof val ? [val] :
|
|
545
|
+
UNDEF
|
|
546
|
+
|
|
547
|
+
const start = null == startin ? 0 : -1 < startin ? startin : 0
|
|
548
|
+
const end = null == endin ? 0 : -1 < endin ? endin : 0
|
|
549
|
+
|
|
550
|
+
if (UNDEF != path && 0 <= start) {
|
|
551
|
+
path = slice(path, start, path.length - end)
|
|
552
|
+
if (0 === path.length) {
|
|
553
|
+
pathstr = '<root>'
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
pathstr = path
|
|
557
|
+
// .filter((p: any, t: any) => (t = typeof p, S_string === t || S_number === t))
|
|
558
|
+
.filter((p: any) => iskey(p))
|
|
559
|
+
.map((p: any) =>
|
|
560
|
+
S_number === typeof p ? S_MT + Math.floor(p) :
|
|
561
|
+
p.replace(R_DOT, S_MT))
|
|
562
|
+
.join(S_DT)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (UNDEF === pathstr) {
|
|
567
|
+
pathstr = '<unknown-path' + (UNDEF === val ? S_MT : S_CN + stringify(val, 47)) + '>'
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return pathstr
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
// Clone a JSON-like data structure.
|
|
575
|
+
// NOTE: function value references are copied, *not* cloned.
|
|
576
|
+
function clone(val: any): any {
|
|
577
|
+
const refs: any[] = []
|
|
578
|
+
const replacer: any = (_k: any, v: any) => S_function === typeof v ?
|
|
579
|
+
(refs.push(v), '`$FUNCTION:' + (refs.length - 1) + '`') : v
|
|
580
|
+
const reviver: any = (_k: any, v: any, m: any) => S_string === typeof v ?
|
|
581
|
+
(m = v.match(R_FUNCTION_REF), m ? refs[m[1]] : v) : v
|
|
582
|
+
return UNDEF === val ? UNDEF : JSON.parse(JSON.stringify(val, replacer), reviver)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
// Define a JSON Object using function arguments.
|
|
587
|
+
function jo(...kv: any[]): Record<string, any> {
|
|
588
|
+
const kvsize = size(kv)
|
|
589
|
+
const o: any = {}
|
|
590
|
+
for (let i = 0; i < kvsize; i += 2) {
|
|
591
|
+
let k = getprop(kv, i, '$KEY' + i)
|
|
592
|
+
k = 'string' === typeof k ? k : stringify(k)
|
|
593
|
+
o[k] = getprop(kv, i + 1, null)
|
|
594
|
+
}
|
|
595
|
+
return o
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
// Define a JSON Array using function arguments.
|
|
600
|
+
function ja(...v: any[]): any[] {
|
|
601
|
+
const vsize = size(v)
|
|
602
|
+
const a: any = new Array(vsize)
|
|
603
|
+
for (let i = 0; i < vsize; i++) {
|
|
604
|
+
a[i] = getprop(v, i, null)
|
|
605
|
+
}
|
|
606
|
+
return a
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
// Safely delete a property from an object or array element.
|
|
612
|
+
// Undefined arguments and invalid keys are ignored.
|
|
613
|
+
// Returns the (possibly modified) parent.
|
|
614
|
+
// For objects, the property is deleted using the delete operator.
|
|
615
|
+
// For arrays, the element at the index is removed and remaining elements are shifted down.
|
|
616
|
+
function delprop<PARENT>(parent: PARENT, key: any): PARENT {
|
|
617
|
+
if (!iskey(key)) {
|
|
618
|
+
return parent
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (ismap(parent)) {
|
|
622
|
+
// key = S_MT + key
|
|
623
|
+
key = strkey(key)
|
|
624
|
+
delete (parent as any)[key]
|
|
625
|
+
}
|
|
626
|
+
else if (islist(parent)) {
|
|
627
|
+
// Ensure key is an integer.
|
|
628
|
+
let keyI = +key
|
|
629
|
+
|
|
630
|
+
if (isNaN(keyI)) {
|
|
631
|
+
return parent
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
keyI = Math.floor(keyI)
|
|
635
|
+
|
|
636
|
+
// Delete list element at position keyI, shifting later elements down.
|
|
637
|
+
if (0 <= keyI && keyI < parent.length) {
|
|
638
|
+
for (let pI = keyI; pI < parent.length - 1; pI++) {
|
|
639
|
+
parent[pI] = parent[pI + 1]
|
|
640
|
+
}
|
|
641
|
+
parent.length = parent.length - 1
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return parent
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
// Safely set a property. Undefined arguments and invalid keys are ignored.
|
|
650
|
+
// Returns the (possibly modified) parent.
|
|
651
|
+
// If the parent is a list, and the key is negative, prepend the value.
|
|
652
|
+
// NOTE: If the key is above the list size, append the value; below, prepend.
|
|
653
|
+
function setprop<PARENT>(parent: PARENT, key: any, val: any): PARENT {
|
|
654
|
+
if (!iskey(key)) {
|
|
655
|
+
return parent
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (ismap(parent)) {
|
|
659
|
+
key = S_MT + key
|
|
660
|
+
const pany = parent as any
|
|
661
|
+
pany[key] = val
|
|
662
|
+
}
|
|
663
|
+
else if (islist(parent)) {
|
|
664
|
+
// Ensure key is an integer.
|
|
665
|
+
let keyI = +key
|
|
666
|
+
|
|
667
|
+
if (isNaN(keyI)) {
|
|
668
|
+
return parent
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
keyI = Math.floor(keyI)
|
|
672
|
+
|
|
673
|
+
// Set or append value at position keyI, or append if keyI out of bounds.
|
|
674
|
+
if (0 <= keyI) {
|
|
675
|
+
parent[parent.length < keyI ? parent.length : keyI] = val
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Prepend value if keyI is negative
|
|
679
|
+
else {
|
|
680
|
+
parent.unshift(val)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return parent
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
// Walk a data structure depth first, applying a function to each value.
|
|
689
|
+
function walk(
|
|
690
|
+
// These arguments are the public interface.
|
|
691
|
+
val: any,
|
|
692
|
+
apply: WalkApply,
|
|
693
|
+
|
|
694
|
+
// These areguments are used for recursive state.
|
|
695
|
+
key?: string | number,
|
|
696
|
+
parent?: any,
|
|
697
|
+
path?: string[]
|
|
698
|
+
): any {
|
|
699
|
+
if (isnode(val)) {
|
|
700
|
+
for (let [ckey, child] of items(val)) {
|
|
701
|
+
setprop(val, ckey, walk(child, apply, ckey, val, [...(path || []), S_MT + ckey]))
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Nodes are applied *after* their children.
|
|
706
|
+
// For the root node, key and parent will be undefined.
|
|
707
|
+
return apply(key, val, parent, path || [])
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
// Merge a list of values into each other. Later values have
|
|
712
|
+
// precedence. Nodes override scalars. Node kinds (list or map)
|
|
713
|
+
// override each other, and do *not* merge. The first element is
|
|
714
|
+
// modified.
|
|
715
|
+
function merge(val: any): any {
|
|
716
|
+
let out: any = UNDEF
|
|
717
|
+
|
|
718
|
+
// Handle edge cases.
|
|
719
|
+
if (!islist(val)) {
|
|
720
|
+
return val
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const list = val as any[]
|
|
724
|
+
const lenlist = list.length
|
|
725
|
+
|
|
726
|
+
if (0 === lenlist) {
|
|
727
|
+
return UNDEF
|
|
728
|
+
}
|
|
729
|
+
else if (1 === lenlist) {
|
|
730
|
+
return list[0]
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Merge a list of values.
|
|
734
|
+
out = getprop(list, 0, {})
|
|
735
|
+
|
|
736
|
+
for (let oI = 1; oI < lenlist; oI++) {
|
|
737
|
+
let obj = list[oI]
|
|
738
|
+
|
|
739
|
+
if (!isnode(obj)) {
|
|
740
|
+
// Nodes win.
|
|
741
|
+
out = obj
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// Nodes win, also over nodes of a different kind.
|
|
745
|
+
if (!isnode(out) || (ismap(obj) && islist(out)) || (islist(obj) && ismap(out))) {
|
|
746
|
+
out = obj
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
// Node stack. walking down the current obj.
|
|
750
|
+
let cur: any[] = [out]
|
|
751
|
+
let cI = 0
|
|
752
|
+
|
|
753
|
+
function merger(
|
|
754
|
+
key: string | number | undefined,
|
|
755
|
+
val: any,
|
|
756
|
+
parent: any,
|
|
757
|
+
path: string[]
|
|
758
|
+
) {
|
|
759
|
+
if (null == key) {
|
|
760
|
+
return val
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Get the curent value at the current path in obj.
|
|
764
|
+
// NOTE: this is not exactly efficient, and should be optimised.
|
|
765
|
+
let lenpath = path.length
|
|
766
|
+
cI = lenpath - 1
|
|
767
|
+
if (UNDEF === cur[cI]) {
|
|
768
|
+
cur[cI] = getpath(out, slice(path, 0, lenpath - 1))
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Create node if needed.
|
|
772
|
+
if (!isnode(cur[cI])) {
|
|
773
|
+
cur[cI] = islist(parent) ? [] : {}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Node child is just ahead of us on the stack, since
|
|
777
|
+
// `walk` traverses leaves before nodes.
|
|
778
|
+
if (isnode(val) && !isempty(val)) {
|
|
779
|
+
setprop(cur[cI], key, cur[cI + 1])
|
|
780
|
+
cur[cI + 1] = UNDEF
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Scalar child.
|
|
784
|
+
else {
|
|
785
|
+
setprop(cur[cI], key, val)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return val
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Walk overriding node, creating paths in output as needed.
|
|
792
|
+
walk(obj, merger)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return out
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
function getpath(store: any, path: number | string | string[], injdef?: Partial<Injection>) {
|
|
802
|
+
|
|
803
|
+
// Operate on a string array.
|
|
804
|
+
const parts = islist(path) ? path :
|
|
805
|
+
'string' === typeof path ? path.split(S_DT) :
|
|
806
|
+
'number' === typeof path ? [strkey(path)] : UNDEF
|
|
807
|
+
|
|
808
|
+
if (UNDEF === parts) {
|
|
809
|
+
return UNDEF
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// let root = store
|
|
813
|
+
let val = store
|
|
814
|
+
const base = getprop(injdef, S_base)
|
|
815
|
+
const src = getprop(store, base, store)
|
|
816
|
+
const numparts = size(parts)
|
|
817
|
+
const dparent = getprop(injdef, 'dparent')
|
|
818
|
+
|
|
819
|
+
// An empty path (incl empty string) just finds the store.
|
|
820
|
+
if (null == path || null == store || (1 === numparts && S_MT === parts[0])) {
|
|
821
|
+
val = src
|
|
822
|
+
}
|
|
823
|
+
else if (0 < numparts) {
|
|
824
|
+
|
|
825
|
+
// Check for $ACTIONs
|
|
826
|
+
if (1 === numparts) {
|
|
827
|
+
val = getprop(store, parts[0])
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!isfunc(val)) {
|
|
831
|
+
val = src
|
|
832
|
+
|
|
833
|
+
const m = parts[0].match(R_META_PATH)
|
|
834
|
+
if (m && injdef && injdef.meta) {
|
|
835
|
+
val = getprop(injdef.meta, m[1])
|
|
836
|
+
parts[0] = m[3]
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const dpath = getprop(injdef, 'dpath')
|
|
840
|
+
|
|
841
|
+
for (let pI = 0; UNDEF !== val && pI < parts.length; pI++) {
|
|
842
|
+
let part = parts[pI]
|
|
843
|
+
|
|
844
|
+
if (injdef && S_DKEY === part) {
|
|
845
|
+
part = getprop(injdef, S_key)
|
|
846
|
+
}
|
|
847
|
+
else if (injdef && part.startsWith('$GET:')) {
|
|
848
|
+
// $GET:path$ -> get store value, use as path part (string)
|
|
849
|
+
part = stringify(getpath(src, part.substring(5, part.length - 1)))
|
|
850
|
+
}
|
|
851
|
+
else if (injdef && part.startsWith('$REF:')) {
|
|
852
|
+
// $REF:refpath$ -> get spec value, use as path part (string)
|
|
853
|
+
part = stringify(getpath(getprop(store, S_DSPEC), part.substring(5, part.length - 1)))
|
|
854
|
+
}
|
|
855
|
+
else if (injdef && part.startsWith('$META:')) {
|
|
856
|
+
// $META:metapath$ -> get meta value, use as path part (string)
|
|
857
|
+
part = stringify(getpath(getprop(injdef, 'meta'), part.substring(6, part.length - 1)))
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// $$ escapes $
|
|
861
|
+
part = part.replace(R_DOUBLE_DOLLAR, '$')
|
|
862
|
+
|
|
863
|
+
if (S_MT === part) {
|
|
864
|
+
|
|
865
|
+
let ascends = 0
|
|
866
|
+
while (S_MT === parts[1 + pI]) {
|
|
867
|
+
ascends++
|
|
868
|
+
pI++
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (injdef && 0 < ascends) {
|
|
872
|
+
if (pI === parts.length - 1) {
|
|
873
|
+
ascends--
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (0 === ascends) {
|
|
877
|
+
val = dparent
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
const fullpath = slice(dpath, 0 - ascends).concat(parts.slice(pI + 1))
|
|
881
|
+
|
|
882
|
+
if (ascends <= size(dpath)) {
|
|
883
|
+
val = getpath(store, fullpath)
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
val = UNDEF
|
|
887
|
+
}
|
|
888
|
+
break
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
val = dparent
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
val = getprop(val, part)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Inj may provide a custom handler to modify found value.
|
|
903
|
+
const handler = getprop(injdef, 'handler')
|
|
904
|
+
if (null != injdef && isfunc(handler)) {
|
|
905
|
+
const ref = pathify(path)
|
|
906
|
+
val = handler(injdef, val, ref, store)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return val
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
// Inject values from a data store into a node recursively, resolving
|
|
915
|
+
// paths against the store, or current if they are local. THe modify
|
|
916
|
+
// argument allows custom modification of the result. The inj
|
|
917
|
+
// (Injection) argument is used to maintain recursive state.
|
|
918
|
+
function inject(
|
|
919
|
+
val: any,
|
|
920
|
+
store: any,
|
|
921
|
+
injdef?: Partial<Injection>,
|
|
922
|
+
) {
|
|
923
|
+
const valtype = typeof val
|
|
924
|
+
let inj: Injection = injdef as Injection
|
|
925
|
+
|
|
926
|
+
// Create state if at root of injection. The input value is placed
|
|
927
|
+
// inside a virtual parent holder to simplify edge cases.
|
|
928
|
+
if (UNDEF === injdef || null == injdef.mode) {
|
|
929
|
+
// Set up state assuming we are starting in the virtual parent.
|
|
930
|
+
inj = new Injection(val, { [S_DTOP]: val })
|
|
931
|
+
inj.dparent = store
|
|
932
|
+
inj.errs = getprop(store, S_DERRS, [])
|
|
933
|
+
inj.meta.__d = 0
|
|
934
|
+
|
|
935
|
+
if (UNDEF !== injdef) {
|
|
936
|
+
inj.modify = null == injdef.modify ? inj.modify : injdef.modify
|
|
937
|
+
inj.extra = null == injdef.extra ? inj.extra : injdef.extra
|
|
938
|
+
inj.meta = null == injdef.meta ? inj.meta : injdef.meta
|
|
939
|
+
inj.handler = null == injdef.handler ? inj.handler : injdef.handler
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
inj.descend()
|
|
944
|
+
|
|
945
|
+
// Descend into node.
|
|
946
|
+
if (isnode(val)) {
|
|
947
|
+
|
|
948
|
+
// Keys are sorted alphanumerically to ensure determinism.
|
|
949
|
+
// Injection transforms ($FOO) are processed *after* other keys.
|
|
950
|
+
// NOTE: the optional digits suffix of the transform can thus be
|
|
951
|
+
// used to order the transforms.
|
|
952
|
+
let nodekeys = ismap(val) ? [
|
|
953
|
+
...Object.keys(val).filter(k => !k.includes(S_DS)).sort(),
|
|
954
|
+
...Object.keys(val).filter(k => k.includes(S_DS)).sort(),
|
|
955
|
+
] : (val as any).map((_n: any, i: number) => i)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
// Each child key-value pair is processed in three injection phases:
|
|
959
|
+
// 1. inj.mode='key:pre' - Key string is injected, returning a possibly altered key.
|
|
960
|
+
// 2. inj.mode='val' - The child value is injected.
|
|
961
|
+
// 3. inj.mode='key:post' - Key string is injected again, allowing child mutation.
|
|
962
|
+
for (let nkI = 0; nkI < nodekeys.length; nkI++) {
|
|
963
|
+
|
|
964
|
+
const childinj = inj.child(nkI, nodekeys)
|
|
965
|
+
const nodekey = childinj.key
|
|
966
|
+
childinj.mode = S_MKEYPRE
|
|
967
|
+
|
|
968
|
+
// Peform the key:pre mode injection on the child key.
|
|
969
|
+
const prekey = _injectstr(nodekey, store, childinj)
|
|
970
|
+
|
|
971
|
+
// The injection may modify child processing.
|
|
972
|
+
nkI = childinj.keyI
|
|
973
|
+
nodekeys = childinj.keys
|
|
974
|
+
|
|
975
|
+
// Prevent further processing by returning an undefined prekey
|
|
976
|
+
if (UNDEF !== prekey) {
|
|
977
|
+
childinj.val = getprop(val, prekey)
|
|
978
|
+
childinj.mode = S_MVAL as InjectMode
|
|
979
|
+
|
|
980
|
+
// Perform the val mode injection on the child value.
|
|
981
|
+
// NOTE: return value is not used.
|
|
982
|
+
inject(childinj.val, store, childinj)
|
|
983
|
+
|
|
984
|
+
// The injection may modify child processing.
|
|
985
|
+
nkI = childinj.keyI
|
|
986
|
+
nodekeys = childinj.keys
|
|
987
|
+
|
|
988
|
+
// Peform the key:post mode injection on the child key.
|
|
989
|
+
childinj.mode = S_MKEYPOST as InjectMode
|
|
990
|
+
_injectstr(nodekey, store, childinj)
|
|
991
|
+
|
|
992
|
+
// The injection may modify child processing.
|
|
993
|
+
nkI = childinj.keyI
|
|
994
|
+
nodekeys = childinj.keys
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Inject paths into string scalars.
|
|
1000
|
+
else if (S_string === valtype) {
|
|
1001
|
+
inj.mode = S_MVAL as InjectMode
|
|
1002
|
+
val = _injectstr(val, store, inj)
|
|
1003
|
+
if (SKIP !== val) {
|
|
1004
|
+
inj.setval(val)
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Custom modification.
|
|
1009
|
+
if (inj.modify && SKIP !== val) {
|
|
1010
|
+
let mkey = inj.key
|
|
1011
|
+
let mparent = inj.parent
|
|
1012
|
+
let mval = getprop(mparent, mkey)
|
|
1013
|
+
|
|
1014
|
+
inj.modify(
|
|
1015
|
+
mval,
|
|
1016
|
+
mkey,
|
|
1017
|
+
mparent,
|
|
1018
|
+
inj,
|
|
1019
|
+
store
|
|
1020
|
+
)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
inj.val = val
|
|
1024
|
+
|
|
1025
|
+
// Original val reference may no longer be correct.
|
|
1026
|
+
// This return value is only used as the top level result.
|
|
1027
|
+
return getprop(inj.parent, S_DTOP)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
// The transform_* functions are special command inject handlers (see Injector).
|
|
1032
|
+
|
|
1033
|
+
// Delete a key from a map or list.
|
|
1034
|
+
const transform_DELETE: Injector = (inj: Injection) => {
|
|
1035
|
+
inj.setval(UNDEF)
|
|
1036
|
+
return UNDEF
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
// Copy value from source data.
|
|
1041
|
+
const transform_COPY: Injector = (inj: Injection, _val: any) => {
|
|
1042
|
+
const { mode, key } = inj
|
|
1043
|
+
|
|
1044
|
+
let out = key
|
|
1045
|
+
if (!mode.startsWith(S_MKEY)) {
|
|
1046
|
+
out = getprop(inj.dparent, key)
|
|
1047
|
+
inj.setval(out)
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return out
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
// As a value, inject the key of the parent node.
|
|
1055
|
+
// As a key, defined the name of the key property in the source object.
|
|
1056
|
+
const transform_KEY: Injector = (inj: Injection) => {
|
|
1057
|
+
const { mode, path, parent } = inj
|
|
1058
|
+
|
|
1059
|
+
// Do nothing in val mode.
|
|
1060
|
+
if (S_MVAL !== mode) {
|
|
1061
|
+
return UNDEF
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Key is defined by $KEY meta property.
|
|
1065
|
+
const keyspec = getprop(parent, S_BKEY)
|
|
1066
|
+
if (UNDEF !== keyspec) {
|
|
1067
|
+
delprop(parent, S_BKEY)
|
|
1068
|
+
return getprop(inj.dparent, keyspec)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Key is defined within general purpose $META object.
|
|
1072
|
+
return getprop(getprop(parent, S_BANNO), S_KEY, getprop(path, path.length - 2))
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
// Annotatea node. Does nothing itself, just used by
|
|
1077
|
+
// other injectors, and is removed when called.
|
|
1078
|
+
const transform_ANNO: Injector = (inj: Injection) => {
|
|
1079
|
+
const { parent } = inj
|
|
1080
|
+
delprop(parent, S_BANNO)
|
|
1081
|
+
return UNDEF
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
// Merge a list of objects into the current object.
|
|
1086
|
+
// Must be a key in an object. The value is merged over the current object.
|
|
1087
|
+
// If the value is an array, the elements are first merged using `merge`.
|
|
1088
|
+
// If the value is the empty string, merge the top level store.
|
|
1089
|
+
// Format: { '`$MERGE`': '`source-path`' | ['`source-paths`', ...] }
|
|
1090
|
+
const transform_MERGE: Injector = (inj: Injection) => {
|
|
1091
|
+
const { mode, key, parent } = inj
|
|
1092
|
+
|
|
1093
|
+
// Ensures $MERGE is removed from parent list (val mode).
|
|
1094
|
+
let out: any = UNDEF
|
|
1095
|
+
|
|
1096
|
+
if (S_MKEYPRE === mode) {
|
|
1097
|
+
out = key
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Operate after child values have been transformed.
|
|
1101
|
+
else if (S_MKEYPOST === mode) {
|
|
1102
|
+
out = key
|
|
1103
|
+
|
|
1104
|
+
let args = getprop(parent, key)
|
|
1105
|
+
args = Array.isArray(args) ? args : [args]
|
|
1106
|
+
|
|
1107
|
+
// Remove the $MERGE command from a parent map.
|
|
1108
|
+
inj.setval(UNDEF)
|
|
1109
|
+
|
|
1110
|
+
// Literals in the parent have precedence, but we still merge onto
|
|
1111
|
+
// the parent object, so that node tree references are not changed.
|
|
1112
|
+
const mergelist = [parent, ...args, clone(parent)]
|
|
1113
|
+
|
|
1114
|
+
merge(mergelist)
|
|
1115
|
+
|
|
1116
|
+
// return key
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return out
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
// Convert a node to a list.
|
|
1124
|
+
// Format: ['`$EACH`', '`source-path-of-node`', child-template]
|
|
1125
|
+
const transform_EACH: Injector = (
|
|
1126
|
+
inj: Injection,
|
|
1127
|
+
_val: any,
|
|
1128
|
+
_ref: string,
|
|
1129
|
+
store: any
|
|
1130
|
+
) => {
|
|
1131
|
+
|
|
1132
|
+
// Remove arguments to avoid spurious processing.
|
|
1133
|
+
if (null != inj.keys) {
|
|
1134
|
+
inj.keys.length = 1
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (S_MVAL !== inj.mode) {
|
|
1138
|
+
return UNDEF
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Get arguments: ['`$EACH`', 'source-path', child-template].
|
|
1142
|
+
const srcpath = getprop(inj.parent, 1)
|
|
1143
|
+
const child = clone(getprop(inj.parent, 2))
|
|
1144
|
+
|
|
1145
|
+
// Source data.
|
|
1146
|
+
const srcstore = getprop(store, inj.base, store)
|
|
1147
|
+
|
|
1148
|
+
const src = getpath(srcstore, srcpath, inj)
|
|
1149
|
+
|
|
1150
|
+
// Create parallel data structures:
|
|
1151
|
+
// source entries :: child templates
|
|
1152
|
+
let tcur: any = []
|
|
1153
|
+
let tval: any = []
|
|
1154
|
+
|
|
1155
|
+
const tkey = inj.path[inj.path.length - 2]
|
|
1156
|
+
const target = inj.nodes[inj.nodes.length - 2] || inj.nodes[inj.nodes.length - 1]
|
|
1157
|
+
|
|
1158
|
+
// Create clones of the child template for each value of the current soruce.
|
|
1159
|
+
if (islist(src)) {
|
|
1160
|
+
tval = src.map(() => clone(child))
|
|
1161
|
+
}
|
|
1162
|
+
else if (ismap(src)) {
|
|
1163
|
+
tval = Object.entries(src).map(n => ({
|
|
1164
|
+
...clone(child),
|
|
1165
|
+
|
|
1166
|
+
// Make a note of the key for $KEY transforms.
|
|
1167
|
+
[S_BANNO]: { KEY: n[0] }
|
|
1168
|
+
}))
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
let rval = []
|
|
1172
|
+
|
|
1173
|
+
if (0 < size(tval)) {
|
|
1174
|
+
tcur = null == src ? UNDEF : Object.values(src)
|
|
1175
|
+
|
|
1176
|
+
const ckey = getelem(inj.path, -2)
|
|
1177
|
+
|
|
1178
|
+
const tpath = slice(inj.path, -1)
|
|
1179
|
+
const dpath = [S_DTOP, ...srcpath.split(S_DT), '$:' + ckey]
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
// Parent structure.
|
|
1183
|
+
|
|
1184
|
+
// const ckey = getelem(cpath, -1)
|
|
1185
|
+
tcur = { [ckey]: tcur }
|
|
1186
|
+
|
|
1187
|
+
if (1 < tpath.length) {
|
|
1188
|
+
const pkey = getelem(inj.path, -3, S_DTOP)
|
|
1189
|
+
// const pkey = getelem(cpath, -2, S_DTOP)
|
|
1190
|
+
tcur = { [pkey]: tcur }
|
|
1191
|
+
dpath.push('$:' + pkey)
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const tinj = inj.child(0, [ckey])
|
|
1195
|
+
tinj.path = tpath
|
|
1196
|
+
tinj.nodes = slice(inj.nodes, -1)
|
|
1197
|
+
|
|
1198
|
+
tinj.parent = getelem(tinj.nodes, -1)
|
|
1199
|
+
setprop(tinj.parent, ckey, tval)
|
|
1200
|
+
|
|
1201
|
+
tinj.val = tval
|
|
1202
|
+
tinj.dpath = dpath
|
|
1203
|
+
tinj.dparent = tcur
|
|
1204
|
+
|
|
1205
|
+
inject(tval, store, tinj)
|
|
1206
|
+
rval = tinj.val
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
_updateAncestors(inj, target, tkey, rval)
|
|
1210
|
+
|
|
1211
|
+
// Prevent callee from damaging first list entry (since we are in `val` mode).
|
|
1212
|
+
return rval[0]
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
// Convert a node to a map.
|
|
1217
|
+
// Format: { '`$PACK`':['`source-path`', child-template]}
|
|
1218
|
+
const transform_PACK: Injector = (
|
|
1219
|
+
inj: Injection,
|
|
1220
|
+
_val: any,
|
|
1221
|
+
_ref: string,
|
|
1222
|
+
store: any
|
|
1223
|
+
) => {
|
|
1224
|
+
const { mode, key, path, parent, nodes } = inj
|
|
1225
|
+
|
|
1226
|
+
// Defensive context checks.
|
|
1227
|
+
if (S_MKEYPRE !== mode || S_string !== typeof key || null == path || null == nodes) {
|
|
1228
|
+
return UNDEF
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Get arguments.
|
|
1232
|
+
const args = parent[key]
|
|
1233
|
+
const srcpath = args[0] // Path to source data.
|
|
1234
|
+
const child = clone(args[1]) // Child template.
|
|
1235
|
+
|
|
1236
|
+
// Find key and target node.
|
|
1237
|
+
const keyprop = child[S_BKEY]
|
|
1238
|
+
const tkey = getelem(path, -2)
|
|
1239
|
+
const target = nodes[path.length - 2] || nodes[path.length - 1]
|
|
1240
|
+
|
|
1241
|
+
// Source data
|
|
1242
|
+
const srcstore = getprop(store, inj.base, store)
|
|
1243
|
+
|
|
1244
|
+
let src = getpath(srcstore, srcpath, inj)
|
|
1245
|
+
|
|
1246
|
+
// Prepare source as a list.
|
|
1247
|
+
src = islist(src) ? src :
|
|
1248
|
+
ismap(src) ? Object.entries(src)
|
|
1249
|
+
.reduce((a: any[], n: any) =>
|
|
1250
|
+
(n[1][S_BANNO] = { KEY: n[0] }, a.push(n[1]), a), []) :
|
|
1251
|
+
UNDEF
|
|
1252
|
+
|
|
1253
|
+
if (null == src) {
|
|
1254
|
+
return UNDEF
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Get key if specified.
|
|
1258
|
+
let childkey: PropKey | undefined = getprop(child, S_BKEY)
|
|
1259
|
+
let keyname = UNDEF === childkey ? keyprop : childkey
|
|
1260
|
+
delprop(child, S_BKEY)
|
|
1261
|
+
|
|
1262
|
+
// Build parallel target object.
|
|
1263
|
+
let tval: any = {}
|
|
1264
|
+
tval = src.reduce((a: any, n: any) => {
|
|
1265
|
+
let kn = getprop(n, keyname)
|
|
1266
|
+
setprop(a, kn, clone(child))
|
|
1267
|
+
const nchild = getprop(a, kn)
|
|
1268
|
+
const mval = getprop(n, S_BANNO)
|
|
1269
|
+
if (UNDEF === mval) {
|
|
1270
|
+
delprop(nchild, S_BANNO)
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
setprop(nchild, S_BANNO, mval)
|
|
1274
|
+
}
|
|
1275
|
+
return a
|
|
1276
|
+
}, tval)
|
|
1277
|
+
|
|
1278
|
+
let rval = {}
|
|
1279
|
+
|
|
1280
|
+
if (0 < size(tval)) {
|
|
1281
|
+
|
|
1282
|
+
// Build parallel source object.
|
|
1283
|
+
let tcur: any = {}
|
|
1284
|
+
src.reduce((a: any, n: any) => {
|
|
1285
|
+
let kn = getprop(n, keyname)
|
|
1286
|
+
setprop(a, kn, n)
|
|
1287
|
+
return a
|
|
1288
|
+
}, tcur)
|
|
1289
|
+
|
|
1290
|
+
const tpath = slice(inj.path, -1)
|
|
1291
|
+
|
|
1292
|
+
const ckey = getelem(inj.path, -2)
|
|
1293
|
+
const dpath = [S_DTOP, ...srcpath.split(S_DT), '$:' + ckey]
|
|
1294
|
+
|
|
1295
|
+
tcur = { [ckey]: tcur }
|
|
1296
|
+
|
|
1297
|
+
if (1 < tpath.length) {
|
|
1298
|
+
const pkey = getelem(inj.path, -3, S_DTOP)
|
|
1299
|
+
tcur = { [pkey]: tcur }
|
|
1300
|
+
dpath.push('$:' + pkey)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const tinj = inj.child(0, [ckey])
|
|
1304
|
+
tinj.path = tpath
|
|
1305
|
+
tinj.nodes = slice(inj.nodes, -1)
|
|
1306
|
+
|
|
1307
|
+
// tinj.parent = tcur
|
|
1308
|
+
tinj.parent = getelem(tinj.nodes, -1)
|
|
1309
|
+
tinj.val = tval
|
|
1310
|
+
|
|
1311
|
+
tinj.dpath = dpath
|
|
1312
|
+
tinj.dparent = tcur
|
|
1313
|
+
|
|
1314
|
+
inject(tval, store, tinj)
|
|
1315
|
+
rval = tinj.val
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
_updateAncestors(inj, target, tkey, rval)
|
|
1319
|
+
|
|
1320
|
+
// Drop transform key.
|
|
1321
|
+
return UNDEF
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
// TODO: not found ref should removed key (setprop UNDEF)
|
|
1326
|
+
// Reference original spec (enables recursice transformations)
|
|
1327
|
+
// Format: ['`$REF`', '`spec-path`']
|
|
1328
|
+
const transform_REF: Injector = (
|
|
1329
|
+
inj: Injection,
|
|
1330
|
+
val: any,
|
|
1331
|
+
_ref: string,
|
|
1332
|
+
store: any
|
|
1333
|
+
) => {
|
|
1334
|
+
const { nodes } = inj
|
|
1335
|
+
|
|
1336
|
+
if (S_MVAL !== inj.mode) {
|
|
1337
|
+
return UNDEF
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Get arguments: ['`$REF`', 'ref-path'].
|
|
1341
|
+
const refpath = getprop(inj.parent, 1)
|
|
1342
|
+
inj.keyI = inj.keys.length
|
|
1343
|
+
|
|
1344
|
+
// Spec reference.
|
|
1345
|
+
const spec = getprop(store, S_DSPEC)()
|
|
1346
|
+
|
|
1347
|
+
const ref = getpath(spec, refpath, {
|
|
1348
|
+
// TODO: test relative refs
|
|
1349
|
+
dpath: inj.path.slice(1),
|
|
1350
|
+
dparent: getpath(spec, inj.path.slice(1))
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1353
|
+
let hasSubRef = false
|
|
1354
|
+
if (isnode(ref)) {
|
|
1355
|
+
walk(ref, (_k: any, v: any) => {
|
|
1356
|
+
if ('`$REF`' === v) {
|
|
1357
|
+
hasSubRef = true
|
|
1358
|
+
}
|
|
1359
|
+
return v
|
|
1360
|
+
})
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
let tref = clone(ref)
|
|
1364
|
+
|
|
1365
|
+
const cpath = slice(inj.path, -3)
|
|
1366
|
+
const tpath = slice(inj.path, -1)
|
|
1367
|
+
let tcur = getpath(store, cpath)
|
|
1368
|
+
let tval = getpath(store, tpath)
|
|
1369
|
+
let rval = UNDEF
|
|
1370
|
+
|
|
1371
|
+
if (!hasSubRef || UNDEF !== tval) {
|
|
1372
|
+
const tinj = inj.child(0, [getelem(tpath, -1)])
|
|
1373
|
+
|
|
1374
|
+
tinj.path = tpath
|
|
1375
|
+
tinj.nodes = slice(inj.nodes, -1)
|
|
1376
|
+
tinj.parent = getelem(nodes, -2)
|
|
1377
|
+
tinj.val = tref
|
|
1378
|
+
|
|
1379
|
+
tinj.dpath = [...cpath]
|
|
1380
|
+
tinj.dparent = tcur
|
|
1381
|
+
|
|
1382
|
+
inject(tref, store, tinj)
|
|
1383
|
+
|
|
1384
|
+
rval = tinj.val
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
rval = UNDEF
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const grandparent = inj.setval(rval, 2)
|
|
1391
|
+
|
|
1392
|
+
if (islist(grandparent) && inj.prior) {
|
|
1393
|
+
inj.prior.keyI--
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return val
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
// Transform data using spec.
|
|
1401
|
+
// Only operates on static JSON-like data.
|
|
1402
|
+
// Arrays are treated as if they are objects with indices as keys.
|
|
1403
|
+
function transform(
|
|
1404
|
+
data: any, // Source data to transform into new data (original not mutated)
|
|
1405
|
+
spec: any, // Transform specification; output follows this shape
|
|
1406
|
+
injdef?: Partial<Injection>
|
|
1407
|
+
) {
|
|
1408
|
+
// Clone the spec so that the clone can be modified in place as the transform result.
|
|
1409
|
+
const origspec = spec
|
|
1410
|
+
spec = clone(origspec)
|
|
1411
|
+
|
|
1412
|
+
const extra = injdef?.extra
|
|
1413
|
+
// const modify = injdef?.modify
|
|
1414
|
+
|
|
1415
|
+
const extraTransforms: any = {}
|
|
1416
|
+
const extraData = null == extra ? UNDEF : items(extra)
|
|
1417
|
+
.reduce((a: any, n: any[]) =>
|
|
1418
|
+
(n[0].startsWith(S_DS) ? extraTransforms[n[0]] = n[1] : (a[n[0]] = n[1]), a), {})
|
|
1419
|
+
|
|
1420
|
+
const dataClone = merge([
|
|
1421
|
+
isempty(extraData) ? UNDEF : clone(extraData),
|
|
1422
|
+
clone(data),
|
|
1423
|
+
])
|
|
1424
|
+
|
|
1425
|
+
// Define a top level store that provides transform operations.
|
|
1426
|
+
const store = {
|
|
1427
|
+
|
|
1428
|
+
// The inject function recognises this special location for the root of the source data.
|
|
1429
|
+
// NOTE: to escape data that contains "`$FOO`" keys at the top level,
|
|
1430
|
+
// place that data inside a holding map: { myholder: mydata }.
|
|
1431
|
+
$TOP: dataClone,
|
|
1432
|
+
|
|
1433
|
+
$SPEC: () => origspec,
|
|
1434
|
+
|
|
1435
|
+
// Escape backtick (this also works inside backticks).
|
|
1436
|
+
$BT: () => S_BT,
|
|
1437
|
+
|
|
1438
|
+
// Escape dollar sign (this also works inside backticks).
|
|
1439
|
+
$DS: () => S_DS,
|
|
1440
|
+
|
|
1441
|
+
// Insert current date and time as an ISO string.
|
|
1442
|
+
$WHEN: () => new Date().toISOString(),
|
|
1443
|
+
|
|
1444
|
+
$DELETE: transform_DELETE,
|
|
1445
|
+
$COPY: transform_COPY,
|
|
1446
|
+
$KEY: transform_KEY,
|
|
1447
|
+
$ANNO: transform_ANNO,
|
|
1448
|
+
$MERGE: transform_MERGE,
|
|
1449
|
+
$EACH: transform_EACH,
|
|
1450
|
+
$PACK: transform_PACK,
|
|
1451
|
+
$REF: transform_REF,
|
|
1452
|
+
|
|
1453
|
+
// Custom extra transforms, if any.
|
|
1454
|
+
...extraTransforms,
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const out = inject(spec, store, injdef)
|
|
1458
|
+
return out
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
// A required string value. NOTE: Rejects empty strings.
|
|
1463
|
+
const validate_STRING: Injector = (inj: Injection) => {
|
|
1464
|
+
let out = getprop(inj.dparent, inj.key)
|
|
1465
|
+
|
|
1466
|
+
const t = typify(out)
|
|
1467
|
+
if (S_string !== t) {
|
|
1468
|
+
let msg = _invalidTypeMsg(inj.path, S_string, t, out, 'V1010')
|
|
1469
|
+
inj.errs.push(msg)
|
|
1470
|
+
return UNDEF
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
if (S_MT === out) {
|
|
1474
|
+
let msg = 'Empty string at ' + pathify(inj.path, 1)
|
|
1475
|
+
inj.errs.push(msg)
|
|
1476
|
+
return UNDEF
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
return out
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
// A required number value (int or float).
|
|
1484
|
+
const validate_NUMBER: Injector = (inj: Injection) => {
|
|
1485
|
+
let out = getprop(inj.dparent, inj.key)
|
|
1486
|
+
|
|
1487
|
+
const t = typify(out)
|
|
1488
|
+
if (S_number !== t) {
|
|
1489
|
+
inj.errs.push(_invalidTypeMsg(inj.path, S_number, t, out, 'V1020'))
|
|
1490
|
+
return UNDEF
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return out
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
// A required boolean value.
|
|
1498
|
+
const validate_BOOLEAN: Injector = (inj: Injection) => {
|
|
1499
|
+
let out = getprop(inj.dparent, inj.key)
|
|
1500
|
+
|
|
1501
|
+
const t = typify(out)
|
|
1502
|
+
if (S_boolean !== t) {
|
|
1503
|
+
inj.errs.push(_invalidTypeMsg(inj.path, S_boolean, t, out, 'V1030'))
|
|
1504
|
+
return UNDEF
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return out
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
// A required object (map) value (contents not validated).
|
|
1512
|
+
const validate_OBJECT: Injector = (inj: Injection) => {
|
|
1513
|
+
let out = getprop(inj.dparent, inj.key)
|
|
1514
|
+
|
|
1515
|
+
const t = typify(out)
|
|
1516
|
+
if (t !== S_object) {
|
|
1517
|
+
inj.errs.push(_invalidTypeMsg(inj.path, S_object, t, out, 'V1040'))
|
|
1518
|
+
return UNDEF
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
return out
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
// A required array (list) value (contents not validated).
|
|
1526
|
+
const validate_ARRAY: Injector = (inj: Injection) => {
|
|
1527
|
+
let out = getprop(inj.dparent, inj.key)
|
|
1528
|
+
|
|
1529
|
+
const t = typify(out)
|
|
1530
|
+
if (t !== S_array) {
|
|
1531
|
+
inj.errs.push(_invalidTypeMsg(inj.path, S_array, t, out, 'V1050'))
|
|
1532
|
+
return UNDEF
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
return out
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
// A required function value.
|
|
1540
|
+
const validate_FUNCTION: Injector = (inj: Injection) => {
|
|
1541
|
+
let out = getprop(inj.dparent, inj.key)
|
|
1542
|
+
|
|
1543
|
+
const t = typify(out)
|
|
1544
|
+
if (S_function !== t) {
|
|
1545
|
+
inj.errs.push(_invalidTypeMsg(inj.path, S_function, t, out, 'V1060'))
|
|
1546
|
+
return UNDEF
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return out
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
// Allow any value.
|
|
1554
|
+
const validate_ANY: Injector = (inj: Injection) => {
|
|
1555
|
+
let out = getprop(inj.dparent, inj.key)
|
|
1556
|
+
return out
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
// Specify child values for map or list.
|
|
1562
|
+
// Map syntax: {'`$CHILD`': child-template }
|
|
1563
|
+
// List syntax: ['`$CHILD`', child-template ]
|
|
1564
|
+
const validate_CHILD: Injector = (inj: Injection) => {
|
|
1565
|
+
const { mode, key, parent, keys, path } = inj
|
|
1566
|
+
|
|
1567
|
+
// Setup data structures for validation by cloning child template.
|
|
1568
|
+
|
|
1569
|
+
// Map syntax.
|
|
1570
|
+
if (S_MKEYPRE === mode) {
|
|
1571
|
+
const childtm = getprop(parent, key)
|
|
1572
|
+
|
|
1573
|
+
// Get corresponding current object.
|
|
1574
|
+
const pkey = getprop(path, path.length - 2)
|
|
1575
|
+
let tval = getprop(inj.dparent, pkey)
|
|
1576
|
+
|
|
1577
|
+
if (UNDEF == tval) {
|
|
1578
|
+
tval = {}
|
|
1579
|
+
}
|
|
1580
|
+
else if (!ismap(tval)) {
|
|
1581
|
+
inj.errs.push(_invalidTypeMsg(
|
|
1582
|
+
slice(inj.path, -1), S_object, typify(tval), tval), 'V0220')
|
|
1583
|
+
return UNDEF
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const ckeys = keysof(tval)
|
|
1587
|
+
for (let ckey of ckeys) {
|
|
1588
|
+
setprop(parent, ckey, clone(childtm))
|
|
1589
|
+
|
|
1590
|
+
// NOTE: modifying inj! This extends the child value loop in inject.
|
|
1591
|
+
keys.push(ckey)
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Remove $CHILD to cleanup ouput.
|
|
1595
|
+
inj.setval(UNDEF)
|
|
1596
|
+
return UNDEF
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// List syntax.
|
|
1600
|
+
if (S_MVAL === mode) {
|
|
1601
|
+
|
|
1602
|
+
if (!islist(parent)) {
|
|
1603
|
+
// $CHILD was not inside a list.
|
|
1604
|
+
inj.errs.push('Invalid $CHILD as value')
|
|
1605
|
+
return UNDEF
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const childtm = getprop(parent, 1)
|
|
1609
|
+
|
|
1610
|
+
if (UNDEF === inj.dparent) {
|
|
1611
|
+
// Empty list as default.
|
|
1612
|
+
parent.length = 0
|
|
1613
|
+
return UNDEF
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (!islist(inj.dparent)) {
|
|
1617
|
+
const msg = _invalidTypeMsg(
|
|
1618
|
+
slice(inj.path, -1), S_array, typify(inj.dparent), inj.dparent, 'V0230')
|
|
1619
|
+
inj.errs.push(msg)
|
|
1620
|
+
inj.keyI = parent.length
|
|
1621
|
+
return inj.dparent
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Clone children abd reset inj key index.
|
|
1625
|
+
// The inject child loop will now iterate over the cloned children,
|
|
1626
|
+
// validating them againt the current list values.
|
|
1627
|
+
|
|
1628
|
+
inj.dparent.map((_n, i) => parent[i] = clone(childtm))
|
|
1629
|
+
parent.length = inj.dparent.length
|
|
1630
|
+
inj.keyI = 0
|
|
1631
|
+
const out = getprop(inj.dparent, 0)
|
|
1632
|
+
return out
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
return UNDEF
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
// Match at least one of the specified shapes.
|
|
1640
|
+
// Syntax: ['`$ONE`', alt0, alt1, ...]okI
|
|
1641
|
+
const validate_ONE: Injector = (
|
|
1642
|
+
inj: Injection,
|
|
1643
|
+
_val: any,
|
|
1644
|
+
_ref: string,
|
|
1645
|
+
store: any
|
|
1646
|
+
) => {
|
|
1647
|
+
const { mode, parent, keyI } = inj
|
|
1648
|
+
|
|
1649
|
+
// Only operate in val mode, since parent is a list.
|
|
1650
|
+
if (S_MVAL === mode) {
|
|
1651
|
+
if (!islist(parent) || 0 !== keyI) {
|
|
1652
|
+
inj.errs.push('The $ONE validator at field ' +
|
|
1653
|
+
pathify(inj.path, 1, 1) +
|
|
1654
|
+
' must be the first element of an array.')
|
|
1655
|
+
return
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
inj.keyI = inj.keys.length
|
|
1659
|
+
|
|
1660
|
+
// Clean up structure, replacing [$ONE, ...] with current
|
|
1661
|
+
inj.setval(inj.dparent, 2)
|
|
1662
|
+
|
|
1663
|
+
inj.path = slice(inj.path, -1)
|
|
1664
|
+
inj.key = getelem(inj.path, -1)
|
|
1665
|
+
|
|
1666
|
+
let tvals = slice(parent, 1)
|
|
1667
|
+
if (0 === tvals.length) {
|
|
1668
|
+
inj.errs.push('The $ONE validator at field ' +
|
|
1669
|
+
pathify(inj.path, 1, 1) +
|
|
1670
|
+
' must have at least one argument.')
|
|
1671
|
+
return
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// See if we can find a match.
|
|
1675
|
+
for (let tval of tvals) {
|
|
1676
|
+
|
|
1677
|
+
// If match, then errs.length = 0
|
|
1678
|
+
let terrs: any[] = []
|
|
1679
|
+
|
|
1680
|
+
const vstore = { ...store }
|
|
1681
|
+
vstore.$TOP = inj.dparent
|
|
1682
|
+
|
|
1683
|
+
const vcurrent = validate(inj.dparent, tval, {
|
|
1684
|
+
extra: vstore,
|
|
1685
|
+
errs: terrs,
|
|
1686
|
+
meta: inj.meta,
|
|
1687
|
+
})
|
|
1688
|
+
|
|
1689
|
+
inj.setval(vcurrent, -2)
|
|
1690
|
+
|
|
1691
|
+
// Accept current value if there was a match
|
|
1692
|
+
if (0 === terrs.length) {
|
|
1693
|
+
return
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// There was no match.
|
|
1698
|
+
|
|
1699
|
+
const valdesc = tvals
|
|
1700
|
+
.map((v: any) => stringify(v))
|
|
1701
|
+
.join(', ')
|
|
1702
|
+
.replace(R_TRANSFORM_NAME, (_m: any, p1: string) => p1.toLowerCase())
|
|
1703
|
+
|
|
1704
|
+
inj.errs.push(_invalidTypeMsg(
|
|
1705
|
+
inj.path,
|
|
1706
|
+
(1 < tvals.length ? 'one of ' : '') + valdesc,
|
|
1707
|
+
typify(inj.dparent), inj.dparent, 'V0210'))
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
const validate_EXACT: Injector = (inj: Injection) => {
|
|
1713
|
+
const { mode, parent, key, keyI } = inj
|
|
1714
|
+
|
|
1715
|
+
// Only operate in val mode, since parent is a list.
|
|
1716
|
+
if (S_MVAL === mode) {
|
|
1717
|
+
if (!islist(parent) || 0 !== keyI) {
|
|
1718
|
+
inj.errs.push('The $EXACT validator at field ' +
|
|
1719
|
+
pathify(inj.path, 1, 1) +
|
|
1720
|
+
' must be the first element of an array.')
|
|
1721
|
+
return
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
inj.keyI = inj.keys.length
|
|
1725
|
+
|
|
1726
|
+
// Clean up structure, replacing [$EXACT, ...] with current data parent
|
|
1727
|
+
inj.setval(inj.dparent, 2)
|
|
1728
|
+
|
|
1729
|
+
inj.path = slice(inj.path, 0, inj.path.length - 1)
|
|
1730
|
+
inj.key = getelem(inj.path, -1)
|
|
1731
|
+
|
|
1732
|
+
let tvals = slice(parent, 1)
|
|
1733
|
+
if (0 === tvals.length) {
|
|
1734
|
+
inj.errs.push('The $EXACT validator at field ' +
|
|
1735
|
+
pathify(inj.path, 1, 1) +
|
|
1736
|
+
' must have at least one argument.')
|
|
1737
|
+
return
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// See if we can find an exact value match.
|
|
1741
|
+
let currentstr: string | undefined = undefined
|
|
1742
|
+
for (let tval of tvals) {
|
|
1743
|
+
let exactmatch = tval === inj.dparent
|
|
1744
|
+
|
|
1745
|
+
if (!exactmatch && isnode(tval)) {
|
|
1746
|
+
currentstr = undefined === currentstr ? stringify(inj.dparent) : currentstr
|
|
1747
|
+
const tvalstr = stringify(tval)
|
|
1748
|
+
exactmatch = tvalstr === currentstr
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (exactmatch) {
|
|
1752
|
+
return
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const valdesc = tvals
|
|
1757
|
+
.map((v: any) => stringify(v))
|
|
1758
|
+
.join(', ')
|
|
1759
|
+
.replace(R_TRANSFORM_NAME, (_m: any, p1: string) => p1.toLowerCase())
|
|
1760
|
+
|
|
1761
|
+
inj.errs.push(_invalidTypeMsg(
|
|
1762
|
+
inj.path,
|
|
1763
|
+
(1 < inj.path.length ? '' : 'value ') +
|
|
1764
|
+
'exactly equal to ' + (1 === tvals.length ? '' : 'one of ') + valdesc,
|
|
1765
|
+
typify(inj.dparent), inj.dparent, 'V0110'))
|
|
1766
|
+
}
|
|
1767
|
+
else {
|
|
1768
|
+
delprop(parent, key)
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
// This is the "modify" argument to inject. Use this to perform
|
|
1774
|
+
// generic validation. Runs *after* any special commands.
|
|
1775
|
+
const _validation: Modify = (
|
|
1776
|
+
pval: any,
|
|
1777
|
+
key?: any,
|
|
1778
|
+
parent?: any,
|
|
1779
|
+
inj?: Injection,
|
|
1780
|
+
) => {
|
|
1781
|
+
|
|
1782
|
+
if (UNDEF === inj) {
|
|
1783
|
+
return
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (SKIP === pval) {
|
|
1787
|
+
return
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// select needs exact matches
|
|
1791
|
+
const exact = getprop(inj.meta, S_BEXACT, false)
|
|
1792
|
+
|
|
1793
|
+
// Current val to verify.
|
|
1794
|
+
const cval = getprop(inj.dparent, key)
|
|
1795
|
+
|
|
1796
|
+
if (UNDEF === inj || (!exact && UNDEF === cval)) {
|
|
1797
|
+
return
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const ptype = typify(pval)
|
|
1801
|
+
|
|
1802
|
+
// Delete any special commands remaining.
|
|
1803
|
+
if (S_string === ptype && pval.includes(S_DS)) {
|
|
1804
|
+
return
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const ctype = typify(cval)
|
|
1808
|
+
|
|
1809
|
+
// Type mismatch.
|
|
1810
|
+
if (ptype !== ctype && UNDEF !== pval) {
|
|
1811
|
+
inj.errs.push(_invalidTypeMsg(inj.path, ptype, ctype, cval, 'V0010'))
|
|
1812
|
+
return
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
if (ismap(cval)) {
|
|
1816
|
+
if (!ismap(pval)) {
|
|
1817
|
+
inj.errs.push(_invalidTypeMsg(inj.path, ptype, ctype, cval, 'V0020'))
|
|
1818
|
+
return
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const ckeys = keysof(cval)
|
|
1822
|
+
const pkeys = keysof(pval)
|
|
1823
|
+
|
|
1824
|
+
// Empty spec object {} means object can be open (any keys).
|
|
1825
|
+
if (0 < pkeys.length && true !== getprop(pval, '`$OPEN`')) {
|
|
1826
|
+
const badkeys = []
|
|
1827
|
+
for (let ckey of ckeys) {
|
|
1828
|
+
if (!haskey(pval, ckey)) {
|
|
1829
|
+
badkeys.push(ckey)
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Closed object, so reject extra keys not in shape.
|
|
1834
|
+
if (0 < badkeys.length) {
|
|
1835
|
+
const msg =
|
|
1836
|
+
'Unexpected keys at field ' + pathify(inj.path, 1) + S_VIZ + badkeys.join(', ')
|
|
1837
|
+
inj.errs.push(msg)
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
// Object is open, so merge in extra keys.
|
|
1842
|
+
merge([pval, cval])
|
|
1843
|
+
if (isnode(pval)) {
|
|
1844
|
+
delprop(pval, '`$OPEN`')
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
else if (islist(cval)) {
|
|
1849
|
+
if (!islist(pval)) {
|
|
1850
|
+
inj.errs.push(_invalidTypeMsg(inj.path, ptype, ctype, cval, 'V0030'))
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
else if (exact) {
|
|
1854
|
+
if (cval !== pval) {
|
|
1855
|
+
const pathmsg = 1 < size(inj.path) ? 'at field ' + pathify(inj.path, 1) + S_VIZ : S_MT
|
|
1856
|
+
inj.errs.push('Value ' + pathmsg + cval +
|
|
1857
|
+
' should equal ' + pval + S_DT)
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
// Spec value was a default, copy over data
|
|
1862
|
+
setprop(parent, key, cval)
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
return
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
// Validate a data structure against a shape specification. The shape
|
|
1871
|
+
// specification follows the "by example" principle. Plain data in
|
|
1872
|
+
// teh shape is treated as default values that also specify the
|
|
1873
|
+
// required type. Thus shape {a:1} validates {a:2}, since the types
|
|
1874
|
+
// (number) match, but not {a:'A'}. Shape {a;1} against data {}
|
|
1875
|
+
// returns {a:1} as a=1 is the default value of the a key. Special
|
|
1876
|
+
// validation commands (in the same syntax as transform ) are also
|
|
1877
|
+
// provided to specify required values. Thus shape {a:'`$STRING`'}
|
|
1878
|
+
// validates {a:'A'} but not {a:1}. Empty map or list means the node
|
|
1879
|
+
// is open, and if missing an empty default is inserted.
|
|
1880
|
+
function validate(
|
|
1881
|
+
data: any, // Source data to transform into new data (original not mutated)
|
|
1882
|
+
spec: any, // Transform specification; output follows this shape
|
|
1883
|
+
injdef?: Partial<Injection>
|
|
1884
|
+
) {
|
|
1885
|
+
const extra = injdef?.extra
|
|
1886
|
+
|
|
1887
|
+
const collect = null != injdef?.errs
|
|
1888
|
+
const errs = injdef?.errs || []
|
|
1889
|
+
|
|
1890
|
+
const store = {
|
|
1891
|
+
// Remove the transform commands.
|
|
1892
|
+
$DELETE: null,
|
|
1893
|
+
$COPY: null,
|
|
1894
|
+
$KEY: null,
|
|
1895
|
+
$META: null,
|
|
1896
|
+
$MERGE: null,
|
|
1897
|
+
$EACH: null,
|
|
1898
|
+
$PACK: null,
|
|
1899
|
+
|
|
1900
|
+
$STRING: validate_STRING,
|
|
1901
|
+
$NUMBER: validate_NUMBER,
|
|
1902
|
+
$BOOLEAN: validate_BOOLEAN,
|
|
1903
|
+
$OBJECT: validate_OBJECT,
|
|
1904
|
+
$ARRAY: validate_ARRAY,
|
|
1905
|
+
$FUNCTION: validate_FUNCTION,
|
|
1906
|
+
$ANY: validate_ANY,
|
|
1907
|
+
$CHILD: validate_CHILD,
|
|
1908
|
+
$ONE: validate_ONE,
|
|
1909
|
+
$EXACT: validate_EXACT,
|
|
1910
|
+
|
|
1911
|
+
...(extra || {}),
|
|
1912
|
+
|
|
1913
|
+
// A special top level value to collect errors.
|
|
1914
|
+
// NOTE: collecterrs paramter always wins.
|
|
1915
|
+
$ERRS: errs,
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
let meta = { [S_BEXACT]: false }
|
|
1919
|
+
|
|
1920
|
+
if (injdef?.meta) {
|
|
1921
|
+
meta = merge([meta, injdef.meta])
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
const out = transform(data, spec, {
|
|
1925
|
+
meta,
|
|
1926
|
+
extra: store,
|
|
1927
|
+
modify: _validation,
|
|
1928
|
+
handler: _validatehandler
|
|
1929
|
+
})
|
|
1930
|
+
|
|
1931
|
+
const generr = (0 < errs.length && !collect)
|
|
1932
|
+
if (generr) {
|
|
1933
|
+
throw new Error('Invalid data: ' + errs.join(' | '))
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
return out
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
const select_AND: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
|
|
1941
|
+
if (S_MKEYPRE === inj.mode) {
|
|
1942
|
+
const terms = getprop(inj.parent, inj.key)
|
|
1943
|
+
|
|
1944
|
+
const ppath = slice(inj.path, -1)
|
|
1945
|
+
const point = getpath(store, ppath)
|
|
1946
|
+
|
|
1947
|
+
const vstore = { ...store }
|
|
1948
|
+
vstore.$TOP = point
|
|
1949
|
+
|
|
1950
|
+
for (let term of terms) {
|
|
1951
|
+
// setprop(term, '`$OPEN`', getprop(term, '`$OPEN`', true))
|
|
1952
|
+
|
|
1953
|
+
let terrs: any[] = []
|
|
1954
|
+
|
|
1955
|
+
validate(point, term, {
|
|
1956
|
+
extra: vstore,
|
|
1957
|
+
errs: terrs,
|
|
1958
|
+
meta: inj.meta,
|
|
1959
|
+
})
|
|
1960
|
+
|
|
1961
|
+
if (0 != terrs.length) {
|
|
1962
|
+
inj.errs.push(
|
|
1963
|
+
'AND:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(terms))
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const gkey = getelem(inj.path, -2)
|
|
1968
|
+
const gp = getelem(inj.nodes, -2)
|
|
1969
|
+
setprop(gp, gkey, point)
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
const select_OR: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
|
|
1975
|
+
if (S_MKEYPRE === inj.mode) {
|
|
1976
|
+
const terms = getprop(inj.parent, inj.key)
|
|
1977
|
+
|
|
1978
|
+
const ppath = slice(inj.path, -1)
|
|
1979
|
+
const point = getpath(store, ppath)
|
|
1980
|
+
|
|
1981
|
+
const vstore = { ...store }
|
|
1982
|
+
vstore.$TOP = point
|
|
1983
|
+
|
|
1984
|
+
for (let term of terms) {
|
|
1985
|
+
let terrs: any[] = []
|
|
1986
|
+
|
|
1987
|
+
validate(point, term, {
|
|
1988
|
+
extra: vstore,
|
|
1989
|
+
errs: terrs,
|
|
1990
|
+
meta: inj.meta,
|
|
1991
|
+
})
|
|
1992
|
+
|
|
1993
|
+
if (0 === terrs.length) {
|
|
1994
|
+
const gkey = getelem(inj.path, -2)
|
|
1995
|
+
const gp = getelem(inj.nodes, -2)
|
|
1996
|
+
setprop(gp, gkey, point)
|
|
1997
|
+
|
|
1998
|
+
return
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
inj.errs.push(
|
|
2003
|
+
'OR:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(terms))
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
const select_NOT: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
|
|
2009
|
+
if (S_MKEYPRE === inj.mode) {
|
|
2010
|
+
const term = getprop(inj.parent, inj.key)
|
|
2011
|
+
|
|
2012
|
+
const ppath = slice(inj.path, -1)
|
|
2013
|
+
const point = getpath(store, ppath)
|
|
2014
|
+
|
|
2015
|
+
const vstore = { ...store }
|
|
2016
|
+
vstore.$TOP = point
|
|
2017
|
+
|
|
2018
|
+
let terrs: any[] = []
|
|
2019
|
+
|
|
2020
|
+
validate(point, term, {
|
|
2021
|
+
extra: vstore,
|
|
2022
|
+
errs: terrs,
|
|
2023
|
+
meta: inj.meta,
|
|
2024
|
+
})
|
|
2025
|
+
|
|
2026
|
+
if (0 == terrs.length) {
|
|
2027
|
+
inj.errs.push(
|
|
2028
|
+
'NOT:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(term))
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const gkey = getelem(inj.path, -2)
|
|
2032
|
+
const gp = getelem(inj.nodes, -2)
|
|
2033
|
+
setprop(gp, gkey, point)
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
const select_CMP: Injector = (inj: Injection, _val: any, ref: string, store: any) => {
|
|
2039
|
+
if (S_MKEYPRE === inj.mode) {
|
|
2040
|
+
const term = getprop(inj.parent, inj.key)
|
|
2041
|
+
// const src = getprop(store, inj.base, store)
|
|
2042
|
+
const gkey = getelem(inj.path, -2)
|
|
2043
|
+
|
|
2044
|
+
// const tval = getprop(src, gkey)
|
|
2045
|
+
|
|
2046
|
+
const ppath = slice(inj.path, -1)
|
|
2047
|
+
const point = getpath(store, ppath)
|
|
2048
|
+
|
|
2049
|
+
let pass = false
|
|
2050
|
+
|
|
2051
|
+
if ('$GT' === ref && point > term) {
|
|
2052
|
+
pass = true
|
|
2053
|
+
}
|
|
2054
|
+
else if ('$LT' === ref && point < term) {
|
|
2055
|
+
pass = true
|
|
2056
|
+
}
|
|
2057
|
+
else if ('$GTE' === ref && point >= term) {
|
|
2058
|
+
pass = true
|
|
2059
|
+
}
|
|
2060
|
+
else if ('$LTE' === ref && point <= term) {
|
|
2061
|
+
pass = true
|
|
2062
|
+
}
|
|
2063
|
+
else if ('$LIKE' === ref && stringify(point).match(RegExp(term))) {
|
|
2064
|
+
pass = true
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
if (pass) {
|
|
2068
|
+
// Update spec to match found value so that _validate does not complain.
|
|
2069
|
+
const gp = getelem(inj.nodes, -2)
|
|
2070
|
+
setprop(gp, gkey, point)
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
inj.errs.push('CMP: ' + pathify(ppath) + S_VIZ + stringify(point) +
|
|
2074
|
+
' fail:' + ref + ' ' + stringify(term))
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
return UNDEF
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
|
|
2082
|
+
// Select children from a top-level object that match a MongoDB-style query.
|
|
2083
|
+
// Supports $and, $or, and equality comparisons.
|
|
2084
|
+
// For arrays, children are elements; for objects, children are values.
|
|
2085
|
+
// TODO: swap arg order for consistency
|
|
2086
|
+
function select(children: any, query: any): any[] {
|
|
2087
|
+
if (!isnode(children)) {
|
|
2088
|
+
return []
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (ismap(children)) {
|
|
2092
|
+
children = items(children).map(n => (n[1][S_DKEY] = n[0], n[1]))
|
|
2093
|
+
}
|
|
2094
|
+
else {
|
|
2095
|
+
children = (children as any[]).map((n, i) => ((ismap(n) ? n[S_DKEY] = i : null), n))
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const results: any[] = []
|
|
2099
|
+
const injdef = {
|
|
2100
|
+
errs: [],
|
|
2101
|
+
meta: { [S_BEXACT]: true },
|
|
2102
|
+
extra: {
|
|
2103
|
+
$AND: select_AND,
|
|
2104
|
+
$OR: select_OR,
|
|
2105
|
+
$NOT: select_NOT,
|
|
2106
|
+
$GT: select_CMP,
|
|
2107
|
+
$LT: select_CMP,
|
|
2108
|
+
$GTE: select_CMP,
|
|
2109
|
+
$LTE: select_CMP,
|
|
2110
|
+
$LIKE: select_CMP,
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const q = clone(query)
|
|
2115
|
+
|
|
2116
|
+
walk(q, (_k: PropKey | undefined, v: any) => {
|
|
2117
|
+
if (ismap(v)) {
|
|
2118
|
+
setprop(v, '`$OPEN`', getprop(v, '`$OPEN`', true))
|
|
2119
|
+
}
|
|
2120
|
+
return v
|
|
2121
|
+
})
|
|
2122
|
+
|
|
2123
|
+
for (const child of children) {
|
|
2124
|
+
injdef.errs = []
|
|
2125
|
+
|
|
2126
|
+
validate(child, clone(q), injdef)
|
|
2127
|
+
|
|
2128
|
+
if (0 === size(injdef.errs)) {
|
|
2129
|
+
results.push(child)
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
return results
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
// Injection state used for recursive injection into JSON - like data structures.
|
|
2139
|
+
class Injection {
|
|
2140
|
+
mode: InjectMode // Injection mode: key:pre, val, key:post.
|
|
2141
|
+
full: boolean // Transform escape was full key name.
|
|
2142
|
+
keyI: number // Index of parent key in list of parent keys.
|
|
2143
|
+
keys: string[] // List of parent keys.
|
|
2144
|
+
key: string // Current parent key.
|
|
2145
|
+
val: any // Current child value.
|
|
2146
|
+
parent: any // Current parent (in transform specification).
|
|
2147
|
+
path: string[] // Path to current node.
|
|
2148
|
+
nodes: any[] // Stack of ancestor nodes.
|
|
2149
|
+
handler: Injector // Custom handler for injections.
|
|
2150
|
+
errs: any[] // Error collector.
|
|
2151
|
+
meta: Record<string, any> // Custom meta data.
|
|
2152
|
+
dparent: any // Current data parent node (contains current data value).
|
|
2153
|
+
dpath: string[] // Current data value path
|
|
2154
|
+
base?: string // Base key for data in store, if any.
|
|
2155
|
+
modify?: Modify // Modify injection output.
|
|
2156
|
+
prior?: Injection // Parent (aka prior) injection.
|
|
2157
|
+
extra?: any
|
|
2158
|
+
|
|
2159
|
+
constructor(val: any, parent: any) {
|
|
2160
|
+
this.val = val
|
|
2161
|
+
this.parent = parent
|
|
2162
|
+
this.errs = []
|
|
2163
|
+
|
|
2164
|
+
this.dparent = UNDEF
|
|
2165
|
+
this.dpath = [S_DTOP]
|
|
2166
|
+
|
|
2167
|
+
this.mode = S_MVAL as InjectMode
|
|
2168
|
+
this.full = false
|
|
2169
|
+
this.keyI = 0
|
|
2170
|
+
this.keys = [S_DTOP]
|
|
2171
|
+
this.key = S_DTOP
|
|
2172
|
+
this.path = [S_DTOP]
|
|
2173
|
+
this.nodes = [parent]
|
|
2174
|
+
this.handler = _injecthandler
|
|
2175
|
+
this.base = S_DTOP
|
|
2176
|
+
this.meta = {}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
toString(prefix?: string) {
|
|
2181
|
+
return 'INJ' + (null == prefix ? '' : S_FS + prefix) + S_CN +
|
|
2182
|
+
pad(pathify(this.path, 1)) +
|
|
2183
|
+
this.mode + (this.full ? '/full' : '') + S_CN +
|
|
2184
|
+
'key=' + this.keyI + S_FS + this.key + S_FS + S_OS + this.keys + S_CS +
|
|
2185
|
+
' p=' + stringify(this.parent, -1, 1) +
|
|
2186
|
+
' m=' + stringify(this.meta, -1, 1) +
|
|
2187
|
+
' d/' + pathify(this.dpath, 1) + '=' + stringify(this.dparent, -1, 1) +
|
|
2188
|
+
' r=' + stringify(this.nodes[0]?.[S_DTOP], -1, 1)
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
descend() {
|
|
2193
|
+
this.meta.__d++
|
|
2194
|
+
const parentkey = getelem(this.path, -2)
|
|
2195
|
+
|
|
2196
|
+
// Resolve current node in store for local paths.
|
|
2197
|
+
if (UNDEF === this.dparent) {
|
|
2198
|
+
|
|
2199
|
+
// Even if there's no data, dpath should continue to match path, so that
|
|
2200
|
+
// relative paths work properly.
|
|
2201
|
+
if (1 < this.dpath.length) {
|
|
2202
|
+
this.dpath = [...this.dpath, parentkey]
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
// this.dparent is the containing node of the current store value.
|
|
2207
|
+
if (null != parentkey) {
|
|
2208
|
+
this.dparent = getprop(this.dparent, parentkey)
|
|
2209
|
+
|
|
2210
|
+
let lastpart = getelem(this.dpath, -1)
|
|
2211
|
+
if (lastpart === '$:' + parentkey) {
|
|
2212
|
+
this.dpath = slice(this.dpath, -1)
|
|
2213
|
+
}
|
|
2214
|
+
else {
|
|
2215
|
+
this.dpath = [...this.dpath, parentkey]
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
return this.dparent
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
child(keyI: number, keys: string[]) {
|
|
2225
|
+
const key = strkey(keys[keyI])
|
|
2226
|
+
const val = this.val
|
|
2227
|
+
|
|
2228
|
+
const cinj = new Injection(getprop(val, key), val)
|
|
2229
|
+
cinj.keyI = keyI
|
|
2230
|
+
cinj.keys = keys
|
|
2231
|
+
cinj.key = key
|
|
2232
|
+
|
|
2233
|
+
cinj.path = [...(this.path || []), key]
|
|
2234
|
+
cinj.nodes = [...(this.nodes || []), val]
|
|
2235
|
+
|
|
2236
|
+
cinj.mode = this.mode
|
|
2237
|
+
cinj.handler = this.handler
|
|
2238
|
+
cinj.modify = this.modify
|
|
2239
|
+
cinj.base = this.base
|
|
2240
|
+
cinj.meta = this.meta
|
|
2241
|
+
cinj.errs = this.errs
|
|
2242
|
+
cinj.prior = this
|
|
2243
|
+
|
|
2244
|
+
cinj.dpath = [...this.dpath]
|
|
2245
|
+
cinj.dparent = this.dparent
|
|
2246
|
+
|
|
2247
|
+
return cinj
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
|
|
2251
|
+
setval(val: any, ancestor?: number) {
|
|
2252
|
+
if (null == ancestor || ancestor < 2) {
|
|
2253
|
+
return UNDEF === val ?
|
|
2254
|
+
delprop(this.parent, this.key) :
|
|
2255
|
+
setprop(this.parent, this.key, val)
|
|
2256
|
+
}
|
|
2257
|
+
else {
|
|
2258
|
+
const aval = getelem(this.nodes, 0 - ancestor)
|
|
2259
|
+
const akey = getelem(this.path, 0 - ancestor)
|
|
2260
|
+
return UNDEF === val ?
|
|
2261
|
+
delprop(aval, akey) :
|
|
2262
|
+
setprop(aval, akey, val)
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
// Internal utilities
|
|
2269
|
+
// ==================
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
// Update all references to target in inj.nodes.
|
|
2273
|
+
function _updateAncestors(_inj: Injection, target: any, tkey: any, tval: any) {
|
|
2274
|
+
// SetProp is sufficient in TypeScript as target reference remains consistent even for lists.
|
|
2275
|
+
setprop(target, tkey, tval)
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
|
|
2279
|
+
// Build a type validation error message.
|
|
2280
|
+
function _invalidTypeMsg(path: any, needtype: string, vt: string, v: any, _whence?: string) {
|
|
2281
|
+
let vs = null == v ? 'no value' : stringify(v)
|
|
2282
|
+
|
|
2283
|
+
return 'Expected ' +
|
|
2284
|
+
(1 < path.length ? ('field ' + pathify(path, 1) + ' to be ') : '') +
|
|
2285
|
+
needtype + ', but found ' +
|
|
2286
|
+
(null != v ? vt + S_VIZ : '') + vs +
|
|
2287
|
+
|
|
2288
|
+
// Uncomment to help debug validation errors.
|
|
2289
|
+
// ' [' + _whence + ']' +
|
|
2290
|
+
|
|
2291
|
+
'.'
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
|
|
2295
|
+
// Default inject handler for transforms. If the path resolves to a function,
|
|
2296
|
+
// call the function passing the injection inj. This is how transforms operate.
|
|
2297
|
+
const _injecthandler: Injector = (
|
|
2298
|
+
inj: Injection,
|
|
2299
|
+
val: any,
|
|
2300
|
+
ref: string,
|
|
2301
|
+
store: any
|
|
2302
|
+
): any => {
|
|
2303
|
+
let out = val
|
|
2304
|
+
const iscmd = isfunc(val) && (UNDEF === ref || ref.startsWith(S_DS))
|
|
2305
|
+
|
|
2306
|
+
// Only call val function if it is a special command ($NAME format).
|
|
2307
|
+
if (iscmd) {
|
|
2308
|
+
out = (val as Injector)(inj, val, ref, store)
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// Update parent with value. Ensures references remain in node tree.
|
|
2312
|
+
else if (S_MVAL === inj.mode && inj.full) {
|
|
2313
|
+
inj.setval(val)
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
return out
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
|
|
2320
|
+
const _validatehandler: Injector = (
|
|
2321
|
+
inj: Injection,
|
|
2322
|
+
val: any,
|
|
2323
|
+
ref: string,
|
|
2324
|
+
store: any
|
|
2325
|
+
): any => {
|
|
2326
|
+
let out = val
|
|
2327
|
+
|
|
2328
|
+
const m = ref.match(R_META_PATH)
|
|
2329
|
+
const ismetapath = null != m
|
|
2330
|
+
|
|
2331
|
+
if (ismetapath) {
|
|
2332
|
+
if ('=' === m[2]) {
|
|
2333
|
+
inj.setval([S_BEXACT, val])
|
|
2334
|
+
}
|
|
2335
|
+
else {
|
|
2336
|
+
inj.setval(val)
|
|
2337
|
+
}
|
|
2338
|
+
inj.keyI = -1
|
|
2339
|
+
|
|
2340
|
+
out = SKIP
|
|
2341
|
+
}
|
|
2342
|
+
else {
|
|
2343
|
+
out = _injecthandler(inj, val, ref, store)
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
return out
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
// Inject values from a data store into a string. Not a public utility - used by
|
|
2351
|
+
// `inject`. Inject are marked with `path` where path is resolved
|
|
2352
|
+
// with getpath against the store or current (if defined)
|
|
2353
|
+
// arguments. See `getpath`. Custom injection handling can be
|
|
2354
|
+
// provided by inj.handler (this is used for transform functions).
|
|
2355
|
+
// The path can also have the special syntax $NAME999 where NAME is
|
|
2356
|
+
// upper case letters only, and 999 is any digits, which are
|
|
2357
|
+
// discarded. This syntax specifies the name of a transform, and
|
|
2358
|
+
// optionally allows transforms to be ordered by alphanumeric sorting.
|
|
2359
|
+
function _injectstr(
|
|
2360
|
+
val: string,
|
|
2361
|
+
store: any,
|
|
2362
|
+
inj?: Injection
|
|
2363
|
+
): any {
|
|
2364
|
+
// Can't inject into non-strings
|
|
2365
|
+
if (S_string !== typeof val || S_MT === val) {
|
|
2366
|
+
return S_MT
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
let out: any = val
|
|
2370
|
+
|
|
2371
|
+
// Pattern examples: "`a.b.c`", "`$NAME`", "`$NAME1`"
|
|
2372
|
+
const m = val.match(R_INJECTION_FULL)
|
|
2373
|
+
|
|
2374
|
+
// Full string of the val is an injection.
|
|
2375
|
+
if (m) {
|
|
2376
|
+
if (null != inj) {
|
|
2377
|
+
inj.full = true
|
|
2378
|
+
}
|
|
2379
|
+
let pathref = m[1]
|
|
2380
|
+
|
|
2381
|
+
// Special escapes inside injection.
|
|
2382
|
+
pathref = 3 < pathref.length ?
|
|
2383
|
+
pathref.replace(R_BT_ESCAPE, S_BT).replace(R_DS_ESCAPE, S_DS) :
|
|
2384
|
+
pathref
|
|
2385
|
+
|
|
2386
|
+
// Get the extracted path reference.
|
|
2387
|
+
out = getpath(store, pathref, inj)
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
else {
|
|
2391
|
+
// Check for injections within the string.
|
|
2392
|
+
const partial = (_m: string, ref: string) => {
|
|
2393
|
+
// Special escapes inside injection.
|
|
2394
|
+
ref = 3 < ref.length ? ref.replace(R_BT_ESCAPE, S_BT).replace(R_DS_ESCAPE, S_DS) : ref
|
|
2395
|
+
if (inj) {
|
|
2396
|
+
inj.full = false
|
|
2397
|
+
}
|
|
2398
|
+
const found = getpath(store, ref, inj)
|
|
2399
|
+
|
|
2400
|
+
// Ensure inject value is a string.
|
|
2401
|
+
return UNDEF === found ? S_MT : S_string === typeof found ? found : JSON.stringify(found)
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
out = val.replace(R_INJECTION_PARTIAL, partial)
|
|
2405
|
+
|
|
2406
|
+
// Also call the inj handler on the entire string, providing the
|
|
2407
|
+
// option for custom injection.
|
|
2408
|
+
if (null != inj && isfunc(inj.handler)) {
|
|
2409
|
+
inj.full = true
|
|
2410
|
+
out = inj.handler(inj, out, val, store)
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
return out
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
class StructUtility {
|
|
2419
|
+
clone = clone
|
|
2420
|
+
delprop = delprop
|
|
2421
|
+
escre = escre
|
|
2422
|
+
escurl = escurl
|
|
2423
|
+
getelem = getelem
|
|
2424
|
+
getpath = getpath
|
|
2425
|
+
getprop = getprop
|
|
2426
|
+
haskey = haskey
|
|
2427
|
+
inject = inject
|
|
2428
|
+
isempty = isempty
|
|
2429
|
+
isfunc = isfunc
|
|
2430
|
+
iskey = iskey
|
|
2431
|
+
islist = islist
|
|
2432
|
+
ismap = ismap
|
|
2433
|
+
isnode = isnode
|
|
2434
|
+
items = items
|
|
2435
|
+
joinurl = joinurl
|
|
2436
|
+
jsonify = jsonify
|
|
2437
|
+
keysof = keysof
|
|
2438
|
+
merge = merge
|
|
2439
|
+
pad = pad
|
|
2440
|
+
pathify = pathify
|
|
2441
|
+
select = select
|
|
2442
|
+
setprop = setprop
|
|
2443
|
+
size = size
|
|
2444
|
+
slice = slice
|
|
2445
|
+
strkey = strkey
|
|
2446
|
+
stringify = stringify
|
|
2447
|
+
transform = transform
|
|
2448
|
+
typify = typify
|
|
2449
|
+
validate = validate
|
|
2450
|
+
walk = walk
|
|
2451
|
+
|
|
2452
|
+
jo = jo
|
|
2453
|
+
ja = ja
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
export {
|
|
2457
|
+
StructUtility,
|
|
2458
|
+
clone,
|
|
2459
|
+
delprop,
|
|
2460
|
+
escre,
|
|
2461
|
+
escurl,
|
|
2462
|
+
getelem,
|
|
2463
|
+
getpath,
|
|
2464
|
+
getprop,
|
|
2465
|
+
haskey,
|
|
2466
|
+
inject,
|
|
2467
|
+
isempty,
|
|
2468
|
+
isfunc,
|
|
2469
|
+
iskey,
|
|
2470
|
+
islist,
|
|
2471
|
+
ismap,
|
|
2472
|
+
isnode,
|
|
2473
|
+
items,
|
|
2474
|
+
joinurl,
|
|
2475
|
+
jsonify,
|
|
2476
|
+
keysof,
|
|
2477
|
+
merge,
|
|
2478
|
+
pad,
|
|
2479
|
+
pathify,
|
|
2480
|
+
select,
|
|
2481
|
+
setprop,
|
|
2482
|
+
size,
|
|
2483
|
+
slice,
|
|
2484
|
+
strkey,
|
|
2485
|
+
stringify,
|
|
2486
|
+
transform,
|
|
2487
|
+
typify,
|
|
2488
|
+
validate,
|
|
2489
|
+
walk,
|
|
2490
|
+
|
|
2491
|
+
jo,
|
|
2492
|
+
ja,
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
export type {
|
|
2496
|
+
Injection,
|
|
2497
|
+
Injector,
|
|
2498
|
+
WalkApply
|
|
2499
|
+
}
|