@muze-nl/assert 0.5.0 → 0.6.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/LICENSE +1 -1
- package/README.md +41 -121
- package/dist/assert.js +473 -0
- package/dist/assert.min.js +4 -0
- package/dist/assert.min.js.map +7 -0
- package/package.json +21 -8
- package/src/assert-core.mjs +550 -0
- package/src/assert.mjs +3 -307
- package/dist/browser.js +0 -252
- package/dist/browser.min.js +0 -2
- package/dist/browser.min.js.map +0 -7
- package/src/browser.mjs +0 -5
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TODO: add assertExplain global flag, so that if assert() fails, you can call explain() with
|
|
3
|
+
the same pattern and it will return text explanation of why it failed, each assertion function must
|
|
4
|
+
then check assertExplain, and return a text explanation of what fails or succeeds
|
|
5
|
+
top level can then filter to show only the failures
|
|
6
|
+
(so that not(x) can show the succeeds message of x)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let assertEnabled = false
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Enables assertion testing with assert()
|
|
13
|
+
*/
|
|
14
|
+
export function enable() {
|
|
15
|
+
assertEnabled = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Disables assertion testing with assert()
|
|
20
|
+
*/
|
|
21
|
+
export function disable() {
|
|
22
|
+
assertEnabled = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function appendPath(path='', key) {
|
|
26
|
+
if (typeof path == 'undefined' || path == null) {
|
|
27
|
+
path = ''
|
|
28
|
+
}
|
|
29
|
+
if (typeof key == 'number') {
|
|
30
|
+
return `${path}[${key}]`
|
|
31
|
+
}
|
|
32
|
+
return `${path}.${key}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pathToArray(path='') {
|
|
36
|
+
if (Array.isArray(path)) {
|
|
37
|
+
return path
|
|
38
|
+
}
|
|
39
|
+
if (!path) {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
let result = []
|
|
43
|
+
let matcher = /(?:^|\.)([^.\[\]]+)|\[(\d+)\]/g
|
|
44
|
+
let match
|
|
45
|
+
while ((match = matcher.exec(path))) {
|
|
46
|
+
if (typeof match[1] != 'undefined') {
|
|
47
|
+
result.push(match[1])
|
|
48
|
+
} else if (typeof match[2] != 'undefined') {
|
|
49
|
+
result.push(Number(match[2]))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pathToString(path=[]) {
|
|
56
|
+
if (typeof path == 'string') {
|
|
57
|
+
return path.startsWith('.') ? path.slice(1) : path
|
|
58
|
+
}
|
|
59
|
+
return path.map((part, index) => {
|
|
60
|
+
if (typeof part == 'number') {
|
|
61
|
+
return `[${part}]`
|
|
62
|
+
}
|
|
63
|
+
return `${index ? '.' : ''}${part}`
|
|
64
|
+
}).join('')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
function describeFunction(value) {
|
|
69
|
+
if (value === String) {
|
|
70
|
+
return 'string'
|
|
71
|
+
}
|
|
72
|
+
if (value === Number) {
|
|
73
|
+
return 'number'
|
|
74
|
+
}
|
|
75
|
+
if (value === Boolean) {
|
|
76
|
+
return 'boolean'
|
|
77
|
+
}
|
|
78
|
+
return value.name || 'function'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clip(text, maxLength=60) {
|
|
82
|
+
if (text.length <= maxLength) {
|
|
83
|
+
return text
|
|
84
|
+
}
|
|
85
|
+
return text.slice(0, maxLength - 1)+'…'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function quoteString(value) {
|
|
89
|
+
return `'${clip(String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n'))}'`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function jsonSummary(value) {
|
|
93
|
+
try {
|
|
94
|
+
let json = JSON.stringify(value)
|
|
95
|
+
if (typeof json == 'string') {
|
|
96
|
+
return clip(json)
|
|
97
|
+
}
|
|
98
|
+
} catch(e) {
|
|
99
|
+
// fall through to the generic object description below
|
|
100
|
+
}
|
|
101
|
+
let name = value?.constructor?.name
|
|
102
|
+
if (name && name != 'Object') {
|
|
103
|
+
return name
|
|
104
|
+
}
|
|
105
|
+
return Object.prototype.toString.call(value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatValue(value) {
|
|
109
|
+
if (typeof value == 'string') {
|
|
110
|
+
return quoteString(value)
|
|
111
|
+
}
|
|
112
|
+
if (typeof value == 'undefined') {
|
|
113
|
+
return 'undefined'
|
|
114
|
+
}
|
|
115
|
+
if (value === null) {
|
|
116
|
+
return 'null'
|
|
117
|
+
}
|
|
118
|
+
if (typeof value == 'function') {
|
|
119
|
+
return describeFunction(value)
|
|
120
|
+
}
|
|
121
|
+
if (value instanceof RegExp) {
|
|
122
|
+
return value.toString()
|
|
123
|
+
}
|
|
124
|
+
if (typeof value == 'number' || typeof value == 'boolean' || typeof value == 'bigint') {
|
|
125
|
+
return String(value)
|
|
126
|
+
}
|
|
127
|
+
if (typeof value == 'symbol') {
|
|
128
|
+
return value.toString()
|
|
129
|
+
}
|
|
130
|
+
return jsonSummary(value)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function describeExpected(value) {
|
|
134
|
+
if (value === String || value === Number || value === Boolean) {
|
|
135
|
+
return describeFunction(value)
|
|
136
|
+
}
|
|
137
|
+
if (typeof value == 'function') {
|
|
138
|
+
return describeFunction(value)
|
|
139
|
+
}
|
|
140
|
+
if (value instanceof RegExp) {
|
|
141
|
+
return value.toString()
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return '['+value.map(describeExpected).join(', ')+']'
|
|
145
|
+
}
|
|
146
|
+
return formatValue(value)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function describeOneOf(patterns) {
|
|
150
|
+
return patterns.map(describeExpected).join(', ')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function conciseMessage(message, actual, expected) {
|
|
154
|
+
if (message == 'data and pattern are not equal') {
|
|
155
|
+
return `expected ${formatValue(expected)}, found ${formatValue(actual)}`
|
|
156
|
+
}
|
|
157
|
+
if (message == 'data does not match pattern' || /^data\[\d+\] does not match pattern$/.test(message)) {
|
|
158
|
+
return `expected ${describeExpected(expected)}, found ${formatValue(actual)}`
|
|
159
|
+
}
|
|
160
|
+
if (message == 'data is undefined, should match pattern') {
|
|
161
|
+
return `missing; expected ${describeExpected(expected)}`
|
|
162
|
+
}
|
|
163
|
+
if (message == 'data is required') {
|
|
164
|
+
return 'required'
|
|
165
|
+
}
|
|
166
|
+
if (message == 'data is an empty string, which is not allowed') {
|
|
167
|
+
return 'empty string is not allowed'
|
|
168
|
+
}
|
|
169
|
+
if (message == 'data is not an object, pattern is') {
|
|
170
|
+
return 'data is not an object'
|
|
171
|
+
}
|
|
172
|
+
if (message == 'data is not an instanceof pattern') {
|
|
173
|
+
return `expected instance of ${describeExpected(expected)}, found ${formatValue(actual)}`
|
|
174
|
+
}
|
|
175
|
+
if (message == 'data does not match oneOf patterns' || message == 'data does not match anyOf patterns') {
|
|
176
|
+
return `expected one of ${describeOneOf(expected)}, found ${formatValue(actual)}`
|
|
177
|
+
}
|
|
178
|
+
if (message == 'data matches pattern, when required not to') {
|
|
179
|
+
return `must not match ${describeExpected(expected)}`
|
|
180
|
+
}
|
|
181
|
+
return message
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function formatIssue(issue, options={}) {
|
|
185
|
+
if (!issue || typeof issue != 'object') {
|
|
186
|
+
return String(issue)
|
|
187
|
+
}
|
|
188
|
+
let path = issue.pathString || pathToString(issue.path || []) || 'value'
|
|
189
|
+
let indent = options.indent ?? ''
|
|
190
|
+
return `${indent}${path}: ${issue.message}`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function formatIssues(issues, options={}) {
|
|
194
|
+
if (!issues) {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
let indent = options.indent ?? ' - '
|
|
198
|
+
return (Array.isArray(issues) ? issues : [issues]).map(issue => formatIssue(issue, { ...options, indent }))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function issueFromProblem(problem) {
|
|
202
|
+
if (!problem || typeof problem != 'object') {
|
|
203
|
+
return {
|
|
204
|
+
path: [],
|
|
205
|
+
pathString: '',
|
|
206
|
+
message: String(problem),
|
|
207
|
+
expected: undefined,
|
|
208
|
+
actual: undefined
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let path = pathToArray(problem.path)
|
|
212
|
+
let pathString = pathToString(path)
|
|
213
|
+
let actual = problem.actual ?? problem.found
|
|
214
|
+
let expected = describeExpected(problem.expected)
|
|
215
|
+
let message = conciseMessage(problem.message, actual, problem.expected)
|
|
216
|
+
return {
|
|
217
|
+
path,
|
|
218
|
+
pathString,
|
|
219
|
+
message,
|
|
220
|
+
expected,
|
|
221
|
+
actual
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function problemsToIssues(problems) {
|
|
226
|
+
if (!problems) {
|
|
227
|
+
return []
|
|
228
|
+
}
|
|
229
|
+
let result = []
|
|
230
|
+
for (let problem of Array.isArray(problems) ? problems : [problems]) {
|
|
231
|
+
if (!problem) {
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (problem && typeof problem == 'object' && problem.problems) {
|
|
235
|
+
let nested = problemsToIssues(problem.problems)
|
|
236
|
+
if (nested.length) {
|
|
237
|
+
result = result.concat(nested)
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
result.push(issueFromProblem(problem))
|
|
242
|
+
}
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* This function will check the source for the assertions in test, if
|
|
248
|
+
* assertion checking is enabled.
|
|
249
|
+
* If it is, and any assertion fails, it will throw an assertError
|
|
250
|
+
* with a list of problems and other details.
|
|
251
|
+
*/
|
|
252
|
+
export function assert(source, test) {
|
|
253
|
+
if (assertEnabled) {
|
|
254
|
+
let problems = fails(source,test)
|
|
255
|
+
if (problems) {
|
|
256
|
+
let assertionIssues = problemsToIssues(problems)
|
|
257
|
+
let formattedIssues = formatIssues(assertionIssues)
|
|
258
|
+
let message = 'Assertions failed:\n'+formattedIssues.join('\n')
|
|
259
|
+
console.error('🅰️ '+message)
|
|
260
|
+
throw new Error(message, {
|
|
261
|
+
cause: { problems, issues: assertionIssues, source }
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Tests a given value against a pattern, only if the value is not null or undefined
|
|
269
|
+
*/
|
|
270
|
+
export function Optional(pattern) {
|
|
271
|
+
return function _Optional(data, root, path) {
|
|
272
|
+
if (typeof data != 'undefined' && data!=null && typeof pattern != 'undefined' ) {
|
|
273
|
+
return fails(data, pattern, root, path)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Tests a given value against a pattern, always.
|
|
280
|
+
*/
|
|
281
|
+
export function Required(pattern) {
|
|
282
|
+
return function _Required(data, root, path) {
|
|
283
|
+
if (data==null || typeof data == 'undefined') {
|
|
284
|
+
return error('data is required', data, pattern || 'any value', path)
|
|
285
|
+
} else if (typeof pattern != 'undefined') {
|
|
286
|
+
return fails(data, pattern, root, path)
|
|
287
|
+
} else {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Tests a given value against a pattern, only if the value is not null or undefined
|
|
295
|
+
* If null or undefined, it does print a warning to the console.
|
|
296
|
+
*/
|
|
297
|
+
export function Recommended(pattern) {
|
|
298
|
+
return function _Recommended(data, root, path) {
|
|
299
|
+
if (data==null || typeof data == 'undefined') {
|
|
300
|
+
warn('data does not contain recommended value', data, pattern, path)
|
|
301
|
+
return false
|
|
302
|
+
} else {
|
|
303
|
+
return fails(data, pattern, root, path)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Tests a given value against a set of patterns, untill one succeeds
|
|
310
|
+
* Returns an error if none succeed
|
|
311
|
+
*/
|
|
312
|
+
export function oneOf(...patterns) {
|
|
313
|
+
return function _oneOf(data, root, path) {
|
|
314
|
+
for(let pattern of patterns) {
|
|
315
|
+
if (!fails(data, pattern, root, path)) {
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return error('data does not match oneOf patterns', data, patterns, path)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Tests a given array of values against a set of patterns
|
|
325
|
+
* If any value does not match one of the patterns, it will return an error
|
|
326
|
+
* If not given an array to test, it will return an error
|
|
327
|
+
*/
|
|
328
|
+
export function anyOf(...patterns) {
|
|
329
|
+
return function _anyOf(data, root, path) {
|
|
330
|
+
if (!Array.isArray(data)) {
|
|
331
|
+
return error('data is not an array',data,'anyOf',path)
|
|
332
|
+
}
|
|
333
|
+
for (let [index, value] of data.entries()) {
|
|
334
|
+
let itemPath = appendPath(path, index)
|
|
335
|
+
if (oneOf(...patterns)(value, root, itemPath)) {
|
|
336
|
+
return error('data does not match anyOf patterns',value,patterns,itemPath)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return false
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function allOf(...patterns) {
|
|
344
|
+
return function _allOf(data, root, path) {
|
|
345
|
+
let problems = []
|
|
346
|
+
for (let pattern of patterns) {
|
|
347
|
+
problems = problems.concat(fails(data, pattern, root, path))
|
|
348
|
+
}
|
|
349
|
+
problems = problems.filter(Boolean)
|
|
350
|
+
if (problems.length) {
|
|
351
|
+
return error('data does not match all given patterns', data, patterns, path, problems)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Tests a given value to see if it is a valid (and absolute) URL, by
|
|
358
|
+
* parsing it with the URL() constructor, and then testing the href
|
|
359
|
+
* value to be equal to the initial value.
|
|
360
|
+
*/
|
|
361
|
+
export function validURL(data, root, path) {
|
|
362
|
+
try {
|
|
363
|
+
if (data instanceof URL) {
|
|
364
|
+
data = data.href
|
|
365
|
+
}
|
|
366
|
+
let url = new URL(data)
|
|
367
|
+
if (url.href!=data) {
|
|
368
|
+
if (!(url.href+'/'==data || url.href==data+'/')) {
|
|
369
|
+
// new URL() always adds a / as path
|
|
370
|
+
return error('data is not a valid url',data,'validURL',path)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch(e) {
|
|
374
|
+
return error('data is not a valid url',data,'validURL',path)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Tests a given value to see if it looks like a valid email address, by
|
|
380
|
+
* testing it against a regular expression. So there are no guarantees that
|
|
381
|
+
* it is an actual working email address, just that it looks like one.
|
|
382
|
+
*/
|
|
383
|
+
export function validEmail(data, root, path) {
|
|
384
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data)) {
|
|
385
|
+
return error('data is not a valid email',data,'validEmail',path)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Tests a given value to see if it is an object which is an instance of the given
|
|
391
|
+
* constructor
|
|
392
|
+
*/
|
|
393
|
+
export function instanceOf(constructor) {
|
|
394
|
+
return function _instanceOf(data, root, path) {
|
|
395
|
+
if (!(data instanceof constructor)) {
|
|
396
|
+
return error('data is not an instanceof pattern',data,constructor,path)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Runs the given test pattern on a value, if the test succeeds, it fails
|
|
403
|
+
* the not() test.
|
|
404
|
+
*/
|
|
405
|
+
export function not(pattern) {
|
|
406
|
+
return function _not(data, root, path) {
|
|
407
|
+
if (!fails(data, pattern, root, path)) {
|
|
408
|
+
return error('data matches pattern, when required not to', data, pattern, path)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Returns structured, path-aware issues if the data fails to satisfy
|
|
415
|
+
* the assertions in the given pattern, or false otherwise.
|
|
416
|
+
* @param {any} data The data to match
|
|
417
|
+
* @param {any} pattern The pattern to match
|
|
418
|
+
* @param {any} root Root object for assertions, set to data by default
|
|
419
|
+
* @return {Array|false} Array with structured issues if the pattern fails, false otherwise
|
|
420
|
+
*/
|
|
421
|
+
export function issues(data, pattern, root) {
|
|
422
|
+
let problems = fails(data, pattern, root)
|
|
423
|
+
if (!problems) {
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
return problemsToIssues(problems)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* returns an array of problems if the data fails to satisfy
|
|
431
|
+
* the assertions in the given pattern, false otherwise
|
|
432
|
+
* @param {any} data The data to match
|
|
433
|
+
* @param {any} pattern The pattern to match
|
|
434
|
+
* @param {any} root Root object for assertions, set to data by default
|
|
435
|
+
* @return {Array|false} Array with problems if the pattern fails, false otherwise
|
|
436
|
+
*/
|
|
437
|
+
export function fails(data, pattern, root, path='') {
|
|
438
|
+
if (typeof root == 'undefined') {
|
|
439
|
+
root = data
|
|
440
|
+
}
|
|
441
|
+
let problems = []
|
|
442
|
+
if (pattern === Boolean) {
|
|
443
|
+
if (typeof data != 'boolean' && !(data instanceof Boolean)) {
|
|
444
|
+
problems.push(error('data is not a boolean', data, pattern, path))
|
|
445
|
+
}
|
|
446
|
+
} else if (pattern === Number) {
|
|
447
|
+
if (typeof data != 'number' && !(data instanceof Number)) {
|
|
448
|
+
problems.push(error('data is not a number', data, pattern, path))
|
|
449
|
+
}
|
|
450
|
+
} else if (pattern === String) {
|
|
451
|
+
if (typeof data != 'string' && !(data instanceof String)) {
|
|
452
|
+
problems.push(error('data is not a string', data, pattern, path))
|
|
453
|
+
}
|
|
454
|
+
if (data == "") {
|
|
455
|
+
problems.push(error('data is an empty string, which is not allowed', data, pattern, path))
|
|
456
|
+
}
|
|
457
|
+
} else if (pattern instanceof RegExp) {
|
|
458
|
+
if (Array.isArray(data)) {
|
|
459
|
+
let index = data.findIndex((element,index) => fails(element,pattern,root,appendPath(path, index)))
|
|
460
|
+
if (index>-1) {
|
|
461
|
+
problems.push(error('data['+index+'] does not match pattern', data[index], pattern, appendPath(path, index)))
|
|
462
|
+
}
|
|
463
|
+
} else if (typeof data == 'undefined') {
|
|
464
|
+
problems.push(error('data is undefined, should match pattern', data, pattern, path))
|
|
465
|
+
} else if (!pattern.test(data)) {
|
|
466
|
+
problems.push(error('data does not match pattern', data, pattern, path))
|
|
467
|
+
}
|
|
468
|
+
} else if (pattern instanceof Function) {
|
|
469
|
+
let problem = pattern(data, root, path)
|
|
470
|
+
if (problem) {
|
|
471
|
+
if (Array.isArray(problem)) {
|
|
472
|
+
problems = problems.concat(problem)
|
|
473
|
+
} else {
|
|
474
|
+
problems.push(problem)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} else if (Array.isArray(pattern)) {
|
|
478
|
+
if (!Array.isArray(data)) {
|
|
479
|
+
problems.push(error('data is not an array',data,[],path))
|
|
480
|
+
} else {
|
|
481
|
+
for (let p of pattern) {
|
|
482
|
+
for (let index of data.keys()) {
|
|
483
|
+
let problem = fails(data[index], p, root, appendPath(path, index))
|
|
484
|
+
if (Array.isArray(problem)) {
|
|
485
|
+
problems = problems.concat(problem)
|
|
486
|
+
} else if (problem) {
|
|
487
|
+
problems.push(problem)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else if (pattern && typeof pattern == 'object') {
|
|
493
|
+
if (Array.isArray(data)) {
|
|
494
|
+
let index = data.findIndex((element,index) => fails(element,pattern,root,appendPath(path, index)))
|
|
495
|
+
if (index>-1) {
|
|
496
|
+
problems.push(error('data['+index+'] does not match pattern', data[index], pattern, appendPath(path, index)))
|
|
497
|
+
}
|
|
498
|
+
} else if (!data || typeof data != 'object') {
|
|
499
|
+
problems.push(error('data is not an object, pattern is', data, pattern, path))
|
|
500
|
+
} else {
|
|
501
|
+
if (data instanceof URLSearchParams) {
|
|
502
|
+
data = Object.fromEntries(data)
|
|
503
|
+
}
|
|
504
|
+
if (pattern instanceof Function) {
|
|
505
|
+
let result = fails(data, pattern, root, path)
|
|
506
|
+
if (result) {
|
|
507
|
+
problems = problems.concat(result)
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
for (const [patternKey, subpattern] of Object.entries(pattern)) {
|
|
511
|
+
let result = fails(data[patternKey], subpattern, root, appendPath(path, patternKey))
|
|
512
|
+
if (result) {
|
|
513
|
+
problems = problems.concat(result)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
if (pattern!=data) {
|
|
520
|
+
problems.push(error('data and pattern are not equal', data, pattern, path))
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (problems.length) {
|
|
524
|
+
return problems
|
|
525
|
+
}
|
|
526
|
+
return false
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Returns an object with message, found and expected properties
|
|
531
|
+
*/
|
|
532
|
+
export function error(message, found, expected, path='', problems) {
|
|
533
|
+
let pathParts = pathToArray(path)
|
|
534
|
+
let result = {
|
|
535
|
+
path,
|
|
536
|
+
pathString: pathToString(pathParts),
|
|
537
|
+
pathParts,
|
|
538
|
+
message,
|
|
539
|
+
found,
|
|
540
|
+
expected
|
|
541
|
+
}
|
|
542
|
+
if (problems) {
|
|
543
|
+
result.problems = problems
|
|
544
|
+
}
|
|
545
|
+
return result
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function warn(message, data, pattern, path) {
|
|
549
|
+
console.warn('🅰️ Assert: '+path, message, pattern, data)
|
|
550
|
+
}
|