@tremho/mist-lift 2.2.8 → 2.4.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.
Files changed (66) hide show
  1. package/README.md +9 -1
  2. package/build/commands/actions/updateDeployedPermissions.js +109 -0
  3. package/build/commands/actions/updateDeployedPermissions.js.map +1 -0
  4. package/build/commands/builtin/ApiDocMaker.js +18 -10
  5. package/build/commands/builtin/ApiDocMaker.js.map +1 -1
  6. package/build/commands/builtin/BuiltInHandler.js +6 -3
  7. package/build/commands/builtin/BuiltInHandler.js.map +1 -1
  8. package/build/commands/builtin/ExportWebroot.js +242 -0
  9. package/build/commands/builtin/ExportWebroot.js.map +1 -0
  10. package/build/commands/builtin/StageWebrootZip.js +10 -6
  11. package/build/commands/builtin/StageWebrootZip.js.map +1 -1
  12. package/build/commands/builtin/prebuilt-zips/API.zip +0 -0
  13. package/build/commands/builtin/prebuilt-zips/FileServe.zip +0 -0
  14. package/build/commands/builtin/prebuilt-zips/Webroot.zip +0 -0
  15. package/build/commands/builtin/webroot-export/s3webroot.js +117 -0
  16. package/build/commands/builtin/webroot-export/s3webroot.js.map +1 -0
  17. package/build/commands/deploy.js +6 -4
  18. package/build/commands/deploy.js.map +1 -1
  19. package/build/commands/package.js +31 -1
  20. package/build/commands/package.js.map +1 -1
  21. package/build/commands/publish.js +40 -13
  22. package/build/commands/publish.js.map +1 -1
  23. package/build/commands/start.js +2 -1
  24. package/build/commands/start.js.map +1 -1
  25. package/build/commands/update.js +1 -0
  26. package/build/commands/update.js.map +1 -1
  27. package/build/expressRoutes/all.js +1 -0
  28. package/build/expressRoutes/all.js.map +1 -1
  29. package/build/expressRoutes/functionBinder.js +159 -17
  30. package/build/expressRoutes/functionBinder.js.map +1 -1
  31. package/build/lib/DirectoryUtils.js +2 -1
  32. package/build/lib/DirectoryUtils.js.map +1 -1
  33. package/build/lib/IdSrc.js +29 -5
  34. package/build/lib/IdSrc.js.map +1 -1
  35. package/build/lib/TypeCheck.js +1204 -0
  36. package/build/lib/TypeCheck.js.map +1 -0
  37. package/build/lib/executeCommand.js +1 -1
  38. package/build/lib/executeCommand.js.map +1 -1
  39. package/build/lib/openAPI/openApiConstruction.js +238 -54
  40. package/build/lib/openAPI/openApiConstruction.js.map +1 -1
  41. package/build/lift.js +1 -1
  42. package/build/lift.js.map +1 -1
  43. package/package.json +5 -2
  44. package/src/commands/actions/updateDeployedPermissions.ts +80 -0
  45. package/src/commands/builtin/ApiDocMaker.ts +17 -10
  46. package/src/commands/builtin/BuiltInHandler.ts +7 -2
  47. package/src/commands/builtin/ExportWebroot.ts +195 -0
  48. package/src/commands/builtin/StageWebrootZip.ts +13 -5
  49. package/src/commands/builtin/prebuilt-zips/API.zip +0 -0
  50. package/src/commands/builtin/prebuilt-zips/FileServe.zip +0 -0
  51. package/src/commands/builtin/prebuilt-zips/Webroot.zip +0 -0
  52. package/src/commands/builtin/webroot-export/s3webroot.ts +78 -0
  53. package/src/commands/deploy.ts +6 -4
  54. package/src/commands/package.ts +33 -2
  55. package/src/commands/publish.ts +37 -12
  56. package/src/commands/start.ts +2 -1
  57. package/src/commands/update.ts +1 -0
  58. package/src/expressRoutes/all.ts +1 -0
  59. package/src/expressRoutes/functionBinder.ts +152 -16
  60. package/src/lib/DirectoryUtils.ts +2 -1
  61. package/src/lib/IdSrc.ts +17 -4
  62. package/src/lib/TypeCheck.ts +1168 -0
  63. package/src/lib/executeCommand.ts +1 -1
  64. package/src/lib/openAPI/openApiConstruction.ts +225 -41
  65. package/src/lift.ts +1 -1
  66. package/templateData/function-main-ts +8 -1
@@ -0,0 +1,1168 @@
1
+ /*
2
+ Module for Constraint definitions and TypeCheck support
3
+ */
4
+
5
+ /**
6
+ * Enumeration of basic types
7
+ *
8
+ * - see [stringFromValueType](#module_TypeCheck..stringFromValueType)
9
+ * - see [valueTypeFromString](#module_TypeCheck..valueTypeFromString)
10
+ */
11
+ export enum ValueType {
12
+ none,
13
+ number,
14
+ string,
15
+ boolean,
16
+ object,
17
+ array,
18
+ regex
19
+ }
20
+
21
+ /**
22
+ * Base for all Constraint errors.
23
+ * Defines the identifying class archetype and consistent error message prefix
24
+ */
25
+ class ConstraintError extends Error {
26
+ constructor () {
27
+ super()
28
+ this.message = 'Constraint Error: '
29
+ }
30
+ }
31
+
32
+ /**
33
+ * An error message for when a value fails validation.
34
+ */
35
+ class ConstraintFail extends ConstraintError {
36
+ constructor (failType: string, value: any) {
37
+ super()
38
+ this.message += `Failed ${failType}: ${value}`
39
+ }
40
+ }
41
+
42
+ /**
43
+ * An error for when the basic type is wrong
44
+ */
45
+ class ConstraintBasicTypeError extends ConstraintError {
46
+ constructor (value: any, expType: string) {
47
+ super()
48
+ this.message += `Incorrect type ${typeof value}, (${expType} expected) ${value}`
49
+ }
50
+ }
51
+
52
+ /**
53
+ * An error for when we expected null or undefined
54
+ */
55
+ // class NullConstraintError extends ConstraintError {
56
+ // constructor() {
57
+ // super();
58
+ // this.message += 'Expected NULL or undefined'
59
+ // }
60
+ // }
61
+
62
+ /**
63
+ * An error for when a min/max range has been violated, including what type of range.
64
+ */
65
+ class RangeConstraintError extends ConstraintError {
66
+ constructor (value: number, comp: number, rangeType: string = 'Number') {
67
+ super()
68
+ // we don't need to test both range ends, because we know we are here because of an error one way
69
+ // or the other.
70
+ if (value < comp) {
71
+ this.message += `${rangeType} ${value} is less than range minimum of ${comp}`
72
+ } else {
73
+ this.message += `${rangeType} ${value} exceeds range maximum of ${comp}`
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * An error for when an integer was expected
80
+ */
81
+ class IntegerConstraintError extends ConstraintError {
82
+ constructor (value: any) {
83
+ super()
84
+ if (value === undefined) {
85
+ this.message += 'Integer expected'
86
+ } else {
87
+ this.message += `Value ${value} is not an integer`
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * An error for when a positive value was expected
94
+ */
95
+ class PositiveConstraintError extends ConstraintError {
96
+ constructor (value: any) {
97
+ super()
98
+ if (value === undefined) {
99
+ this.message += 'Positive value expected'
100
+ } else {
101
+ this.message += `Value ${value} is not positive`
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * An error for when a negative value was expected
108
+ */
109
+ class NegativeConstraintError extends ConstraintError {
110
+ constructor (value: any) {
111
+ super()
112
+ if (value === undefined) {
113
+ this.message += 'Positive value expected'
114
+ } else {
115
+ this.message += `Value ${value} is not negative`
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * An error for when zero was not expected.
122
+ */
123
+ class ZeroValueConstraintError extends ConstraintError {
124
+ constructor () {
125
+ super()
126
+ this.message += 'Zero is not an allowable value'
127
+ }
128
+ }
129
+
130
+ /**
131
+ * An error for declaring both ! and not ! variants of the same expression
132
+ */
133
+ class ConstraintConflictError extends ConstraintError {
134
+ constructor (conflictType: string) {
135
+ super()
136
+ this.message += `Both ${conflictType} and !${conflictType} declared`
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Base form of TypeConstraint.
142
+ * Defines the base type and the test method.
143
+ */
144
+ export class TypeConstraint {
145
+ /** The type this constraint applies to */
146
+ public readonly type: string
147
+
148
+ public badName?: string // defined only if we encounter an unrecognized constraint keyword
149
+
150
+ /** a freeform note that appears in comments. No runtime verification. */
151
+ public note?: string
152
+
153
+ constructor (typeString: string = '') {
154
+ this.type = typeString.trim().toLowerCase()
155
+ }
156
+
157
+ /**
158
+ * Perform a runtime test of the value
159
+
160
+ * returns without throw if test was okay, otherwise throws a ConstraintError explaining the violation.
161
+ *
162
+ * @param value - value to test against this constraint
163
+ *
164
+ * @throws {ConstraintError} Error is thrown if test fails its constraints
165
+ */
166
+ test (value: any): void | never {
167
+ if (typeof value !== this.type) {
168
+ throw new ConstraintBasicTypeError(value, this.type)
169
+ }
170
+ }
171
+
172
+ // Describes the constraint in printable terms (not really used, a bit redundant to describe)
173
+ toString () {
174
+ if (this.badName) return `"${this.badName}" is not a recognized constraint for ${this.type}`
175
+ if (this.note) return this.note
176
+ return '- No Constraint'
177
+ }
178
+
179
+ // describe the constraints in human terms.
180
+ describe () {
181
+ if (this.badName) return `"${this.badName}" is not a recognized constraint for ${this.type}`
182
+ if (this.note) return this.note
183
+ return 'No Constraint'
184
+ }
185
+ }
186
+
187
+ // /**
188
+ // * Enumeration of recognized status for a parameter or return constraint
189
+ // */
190
+ // enum ConstraintStatus {
191
+ // None = "", // not parsed
192
+ // NotConstraint = "NotConstraint", // doesn't start with '-', treat as description
193
+ // Error = "Error", // parsing error
194
+ // NotProvided = "NotProvided", // no constraint block
195
+ //
196
+ //
197
+ // }
198
+
199
+ /**
200
+ * Null only applies to objects.
201
+ */
202
+ // class NullConstraint extends TypeConstraint {
203
+ // test(value) {
204
+ // if(value || typeof value !== 'object') {
205
+ // throw new NullConstraintError()
206
+ // }
207
+ // }
208
+ // }
209
+
210
+ /**
211
+ * Constraints recorded on a number
212
+ * Integer, Positive, Negative, NotZero, min, max
213
+ */
214
+ class NumberConstraint extends TypeConstraint {
215
+ public min?: number // min range
216
+ public max?: number // max range, inclusive
217
+ public maxx?: number // max, exclusive
218
+ public isInteger?: boolean = false // number must be an integer
219
+ public isPositive?: boolean = false // number must be positive
220
+ public isNegative?: boolean = false // number must be negative
221
+ public notZero?: boolean = false // number must not be zero
222
+
223
+ constructor () {
224
+ super('number')
225
+ }
226
+
227
+ test (value: any) {
228
+ super.test(value)
229
+ if (this.isInteger) {
230
+ if (Math.floor(value) !== value) {
231
+ throw new IntegerConstraintError(value)
232
+ }
233
+ }
234
+ if (this.notZero) {
235
+ if (value === 0) {
236
+ throw new ZeroValueConstraintError()
237
+ }
238
+ }
239
+ if (this.isPositive && this.isNegative) {
240
+ throw new ConstraintConflictError('positive')
241
+ }
242
+ if (this.isPositive) {
243
+ if (value < 0) {
244
+ throw new PositiveConstraintError(value)
245
+ }
246
+ }
247
+ if (this.isNegative) {
248
+ if (value < 0) {
249
+ throw new NegativeConstraintError(value)
250
+ }
251
+ }
252
+ if (this.min !== undefined) {
253
+ if (value < this.min) {
254
+ throw new RangeConstraintError(value, this.min)
255
+ }
256
+ }
257
+ if (this.max !== undefined) {
258
+ if (value > this.max) {
259
+ throw new RangeConstraintError(value, this.max)
260
+ }
261
+ }
262
+ if (this.maxx !== undefined) {
263
+ if (value >= this.maxx) {
264
+ throw new RangeConstraintError(value, this.maxx)
265
+ }
266
+ }
267
+ }
268
+
269
+ toString () {
270
+ const keys: string[] = []
271
+ if (this.isInteger) keys.push('Integer')
272
+ if (this.notZero) keys.push('Not Zero')
273
+ if (this.isPositive) keys.push('Positive')
274
+ if (this.isNegative) keys.push('Negative')
275
+ if (this.min !== undefined) keys.push(`Min = ${this.min}`)
276
+ if (this.max !== undefined) keys.push(`Max = ${this.max}`)
277
+ if (this.maxx !== undefined) keys.push(`Maxx = ${this.maxx}`)
278
+ if (this.note) keys.push(this.note)
279
+ return (keys.length > 0) ? '- ' + keys.join(',') : super.toString()
280
+ }
281
+
282
+ describe (): string {
283
+ const keys: string[] = []
284
+ if (this.isInteger) keys.push('number must be an integer')
285
+ if (this.notZero) keys.push('number must not be zero')
286
+ if (this.isPositive) keys.push('number must be positive')
287
+ if (this.isNegative) keys.push('number must be negative')
288
+ if (this.min !== undefined) keys.push(`Minimum value is ${this.min}`)
289
+ if (this.max !== undefined) keys.push(`Maximum value is ${this.max}`)
290
+ if (this.maxx !== undefined) keys.push(`Maximum value is less than ${this.maxx}`)
291
+ if (this.note || this.badName) keys.push(super.describe())
292
+ return (keys.length > 0) ? keys.join('\n') : super.describe()
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Constraints recorded on a string
298
+ * minLength, maxLength, (!)startsWith, (!)endsWith, (!)contains, (!)match
299
+ */
300
+ class StringConstraint extends TypeConstraint {
301
+ public minLength?: number // minimum length of allowed string
302
+ public maxLength?: number // maximum length of allowed string
303
+ public startsWith?: string // string must start with this substring
304
+ public notStartsWith?: string // string must not start with this substring
305
+ public endsWith?: string // string must end with this substring
306
+ public notEndsWith?: string // string must not end with this substring
307
+ public contains?: string // string contains this substring
308
+ public notContains?: string // string does not contain this substring
309
+ public match?: string // regular expression (as string) that must be matched
310
+ public notMatch?: string // regular expression (as string) that must not be matched
311
+
312
+ constructor () {
313
+ super('string')
314
+ }
315
+
316
+ test (value: any) {
317
+ super.test(value)
318
+ if (this.minLength) {
319
+ if (value.length < this.minLength) {
320
+ throw new RangeConstraintError(value.length, this.minLength, 'String Length')
321
+ }
322
+ }
323
+ if (this.maxLength) {
324
+ if (value.length > this.maxLength) {
325
+ throw new RangeConstraintError(value.length, this.maxLength, 'String Length')
326
+ }
327
+ }
328
+ if (this.startsWith && this.notStartsWith) {
329
+ throw new ConstraintConflictError('startsWith')
330
+ }
331
+ if (this.startsWith || this.notStartsWith) {
332
+ const comp = this.startsWith || this.notStartsWith || ''
333
+ const not = !!this.notStartsWith
334
+ if (value.substring(0, comp.length) === comp) {
335
+ if (not) throw new ConstraintFail('!startsWith', value)
336
+ } else {
337
+ if (!not) throw new ConstraintFail('startsWith', value)
338
+ }
339
+ }
340
+ if (this.endsWith && this.notEndsWith) {
341
+ throw new ConstraintConflictError('endsWith')
342
+ }
343
+ if (this.endsWith || this.notEndsWith) {
344
+ const comp = this.endsWith || this.notEndsWith || ''
345
+ const not = !!this.notEndsWith
346
+ if (value.substring(value.length - comp.length) === comp) {
347
+ if (not) throw new ConstraintFail('!endsWith', value)
348
+ } else {
349
+ if (!not) throw new ConstraintFail('endsWith', value)
350
+ }
351
+ }
352
+ if (this.contains && this.notContains) {
353
+ throw new ConstraintConflictError('contains')
354
+ }
355
+ if (this.contains || this.notContains) {
356
+ const comp = this.contains || this.notContains
357
+ const not = !!this.notContains
358
+ if (value.indexOf(comp) !== -1) {
359
+ if (not) throw new ConstraintFail('!contains', value)
360
+ } else {
361
+ if (!not) throw new ConstraintFail('contains', value)
362
+ }
363
+ }
364
+ if (this.match && this.notMatch) {
365
+ throw new ConstraintConflictError('match')
366
+ }
367
+ if (this.match || this.notMatch) {
368
+ const comp = this.match || this.notMatch
369
+ const not = !!this.notMatch
370
+ const re = new RegExp(comp || '')
371
+ if (re.test(value)) {
372
+ if (not) throw new ConstraintFail('!match', value)
373
+ } else {
374
+ if (!not) throw new ConstraintFail('match', value)
375
+ }
376
+ }
377
+ }
378
+
379
+ toString () {
380
+ const keys: string[] = []
381
+ if (this.minLength) keys.push(`Min Length = ${this.minLength}`)
382
+ if (this.maxLength) keys.push(`Max Length = ${this.maxLength}`)
383
+ if (this.startsWith) keys.push(`Starts With = ${this.startsWith}`)
384
+ if (this.notStartsWith) keys.push(`!StartsWith = ${this.startsWith}`)
385
+ if (this.endsWith) keys.push(`Ends With = ${this.endsWith}`)
386
+ if (this.notEndsWith) keys.push(`!EndsWith = ${this.endsWith}`)
387
+ if (this.contains) keys.push(`Contains = ${this.contains}`)
388
+ if (this.notContains) keys.push(`!Contains = ${this.notContains}`)
389
+ if (this.match) keys.push(`Match = ${this.match}`)
390
+ if (this.notMatch) keys.push(`!Match = ${this.notMatch}`)
391
+ if (this.note) keys.push(this.note)
392
+ return (keys.length > 0) ? '- ' + keys.join(',') : super.toString()
393
+ }
394
+
395
+ describe () {
396
+ const keys: string[] = []
397
+ if (this.minLength) keys.push(`string must be at least ${this.minLength} characters long`)
398
+ if (this.maxLength) keys.push(`string must consist of less than ${this.maxLength} characters`)
399
+ if (this.startsWith) keys.push(`string must start with "${this.startsWith}"`)
400
+ if (this.notStartsWith) keys.push(`string must NOT start with "${this.startsWith}"`)
401
+ if (this.endsWith) keys.push(`string must end with "${this.endsWith}"`)
402
+ if (this.notEndsWith) keys.push(`string must NOT end with "${this.endsWith}"`)
403
+ if (this.contains) keys.push(`must contain substring "${this.contains}"`)
404
+ if (this.notContains) keys.push(`must NOT contain substring "${this.notContains}"`)
405
+ if (this.match) keys.push(`must match Regular Expression "${this.match}"`)
406
+ if (this.notMatch) keys.push(`must NOT match RegExp "${this.notMatch}"`)
407
+ if (this.note || this.badName) keys.push(super.describe())
408
+ return (keys.length > 0) ? keys.join('\n') : super.describe()
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Constraints recorded on an object
414
+ * (!)empty, (!)hasProperties, notNested, noPrototype, canSerialize, noUndefinedProps
415
+ */
416
+ class ObjectConstraint extends TypeConstraint {
417
+ public empty?: boolean // object must have no properties
418
+ public notEmpty?: boolean // object must have some properties
419
+ public hasProperties?: string[] // object must have these specific properties defined
420
+ public notHasProperties?: string[] // object must note have these specific properties defined
421
+ public notNested?: boolean // object must not contain object members (arrays are okay)
422
+ public noPrototype?: boolean // object must not have a prototype other than Object.
423
+ public canSerialize?: boolean // object must survive standard js (structured clone) serialization (i.e. stringify w/o error)
424
+ public noFalseyProps?: boolean // all property values must evaluate at truthy
425
+ public noTruthyProps?: boolean // all property values must evaluate as falsey
426
+ public instanceOf?: string // object must have been constructed as this object name
427
+ public notInstanceOf?: string // object must not be of this instance type name
428
+
429
+ constructor () {
430
+ super('object')
431
+ }
432
+
433
+ test (value: any) {
434
+ super.test(value)
435
+
436
+ if (this.empty && this.notEmpty) {
437
+ throw new ConstraintConflictError('empty')
438
+ }
439
+ if ((this.hasProperties != null) && (this.notHasProperties != null)) {
440
+ const collisions: string[] = []
441
+ for (const has of this.hasProperties) {
442
+ if (this.notHasProperties.includes(has)) {
443
+ collisions.push(has)
444
+ }
445
+ }
446
+ if (collisions.length > 0) {
447
+ throw new ConstraintConflictError('hasProperties "' + collisions.join(',') + '"')
448
+ }
449
+ }
450
+ if (this.empty) {
451
+ if (Object.getOwnPropertyNames(value).length > 0) {
452
+ throw new ConstraintFail('empty', 'object contains ' + Object.getOwnPropertyNames(value).length + ' props')
453
+ }
454
+ }
455
+ if (this.notEmpty) {
456
+ if (Object.getOwnPropertyNames(value).length === 0) {
457
+ throw new ConstraintFail('!empty', value)
458
+ }
459
+ }
460
+ if (this.hasProperties != null) {
461
+ for (const has of this.hasProperties) {
462
+ if (!value.hasOwnProperty(has)) {
463
+ throw new ConstraintFail('hasProperties', has)
464
+ }
465
+ }
466
+ }
467
+ if (this.notHasProperties != null) {
468
+ for (const hasnot of this.notHasProperties) {
469
+ if (value.hasOwnProperty(hasnot)) {
470
+ throw new ConstraintFail('!hasProperties', hasnot)
471
+ }
472
+ }
473
+ }
474
+ if (this.notNested) {
475
+ for (const p of Object.getOwnPropertyNames(value)) {
476
+ const v = value[p]
477
+ if (typeof v === 'object') {
478
+ if (!Array.isArray(v)) {
479
+ throw new ConstraintFail('notNested', p)
480
+ }
481
+ }
482
+ }
483
+ }
484
+ if (this.noPrototype) {
485
+ const prot = Object.getPrototypeOf(value)
486
+ const name = prot && prot.constructor.name
487
+ if (name && name !== 'Object') {
488
+ throw new ConstraintFail('noPrototype', value)
489
+ }
490
+ }
491
+ if (this.canSerialize) {
492
+ let json
493
+ try {
494
+ json = JSON.stringify(value)
495
+ } catch (e) {
496
+ }
497
+ if (!json) {
498
+ throw new ConstraintFail('canSerialize', value)
499
+ }
500
+ }
501
+ if (this.noFalseyProps) {
502
+ for (const p of Object.getOwnPropertyNames(value)) {
503
+ const v = value[p]
504
+ if (!v) {
505
+ throw new ConstraintFail('noFalseyProps', p)
506
+ }
507
+ }
508
+ }
509
+ if (this.noTruthyProps) {
510
+ for (const p of Object.getOwnPropertyNames(value)) {
511
+ const v = value[p]
512
+ if (v) {
513
+ throw new ConstraintFail('noTruthyProps', p)
514
+ }
515
+ }
516
+ }
517
+ if (this.instanceOf) {
518
+ if (value.constructor.name !== this.instanceOf) {
519
+ throw new ConstraintFail('instanceOf (' + this.instanceOf + ')', value.constructor.name)
520
+ }
521
+ }
522
+ if (this.notInstanceOf) {
523
+ if (value.constructor.name === this.notInstanceOf) {
524
+ throw new ConstraintFail('!instanceOf', this.notInstanceOf)
525
+ }
526
+ }
527
+ }
528
+
529
+ toString () {
530
+ const keys: string[] = []
531
+ if (this.empty) keys.push('Empty')
532
+ if (this.notEmpty) keys.push('!Empty')
533
+ if (this.hasProperties != null) keys.push(`Has Properties =${this.hasProperties.join(',')}`)
534
+ if (this.notHasProperties != null) keys.push(`!Has Properties =${this.notHasProperties}`)
535
+ if (this.notNested) keys.push('Not Nested')
536
+ if (this.noPrototype) keys.push('No Prototype')
537
+ if (this.canSerialize) keys.push('Can Serialize')
538
+ if (this.noFalseyProps) keys.push('No Falsey Props')
539
+ if (this.noTruthyProps) keys.push('No Truthy Props')
540
+ if (this.instanceOf) keys.push(`Instance Of = ${this.instanceOf}`)
541
+ if (this.notInstanceOf) keys.push(`Not an instance of ${this.notInstanceOf}`)
542
+ if (this.note) keys.push(this.note)
543
+ return (keys.length > 0) ? '- ' + keys.join(',') : super.toString()
544
+ }
545
+
546
+ describe () {
547
+ const keys: string[] = []
548
+ if (this.empty) keys.push('object must be empty')
549
+ if (this.notEmpty) keys.push('object must not be empty')
550
+ if (this.hasProperties != null) keys.push(`object must contain properties "${this.hasProperties.join(',')}"`)
551
+ if (this.notHasProperties != null) keys.push(`object must not contain properties "${this.notHasProperties.join(',')}"`)
552
+ if (this.notNested) keys.push('object must not contain nested objects')
553
+ if (this.noPrototype) keys.push('object must not derive from a prototype')
554
+ if (this.canSerialize) keys.push('object can be serialized')
555
+ if (this.noFalseyProps) keys.push('object can contain no properties that evaluate as false')
556
+ if (this.noTruthyProps) keys.push('object can contain no properties that evaluate as true')
557
+ if (this.instanceOf) keys.push(`object must be an instance of "${this.instanceOf}"`)
558
+ if (this.notInstanceOf) keys.push(`object must not be an instance of "${this.notInstanceOf}"`)
559
+ if (this.note || this.badName) keys.push(super.describe())
560
+ return (keys.length > 0) ? keys.join('\n') : super.describe()
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Enumeration of checkType parsed results.
566
+ *
567
+ * parameters (p1, p2) are parsed at same time, and meaning does vary per checkType.
568
+ */
569
+ export enum ElementCheckType {
570
+ none, // don't test the elements
571
+ all, // test all the elements
572
+ random, // test up to a given number (p1) of elements, randomly chosen
573
+ step, // test every (p1) elements
574
+ first, // test all up to (p1) elements, then stop
575
+ last, // test all of the last (p1) elements
576
+ firstThenLast, // Test the first (p1) elements, and the last (p2) elements
577
+ firstThenStep, // test all up to (p1) elements, then every (p2) thereafter
578
+ firstThenRandom// test all up to (p1) elements, then up to (p2) of the remaining, chosen at random
579
+ }
580
+
581
+ /**
582
+ * Constraints recorded on an array
583
+ * minLength, maxLength, (!)contains, checkType, each
584
+ */
585
+ class ArrayConstraint extends TypeConstraint {
586
+ public minLength?: number // minimum array length
587
+ public maxLength?: number // maximum array length
588
+ public contains?: any // at least one element must be of this value
589
+ public notContains?: any // no elements can be of this value
590
+ public elementConstraints: TypeConstraint[] = [] // elements are tested for compliance under these rules
591
+ public elementCheckType: ElementCheckType = ElementCheckType.none // defines the extent of runtime coverage on elements
592
+ public elementCheckParameter: number = 0 // defined by elementCheckType
593
+ public elementCheckParameter2: number = 0 // defined by elementCheckType
594
+ constructor () {
595
+ super('array')
596
+ }
597
+
598
+ test (value: any) {
599
+ if (!Array.isArray(value)) {
600
+ throw new ConstraintBasicTypeError(value, 'array')
601
+ }
602
+ const length = value.length
603
+ if (this.minLength) {
604
+ if (length < this.minLength) {
605
+ throw new RangeConstraintError(length, this.minLength, 'Array Length')
606
+ }
607
+ }
608
+ if (this.maxLength) {
609
+ if (length > this.maxLength) {
610
+ throw new RangeConstraintError(length, this.maxLength, 'Array Length')
611
+ }
612
+ }
613
+ if (this.contains && this.notContains) {
614
+ throw new ConstraintConflictError('contains')
615
+ }
616
+ if (this.contains || this.notContains) {
617
+ const comp = this.contains || this.notContains
618
+ const not = !!this.notContains
619
+ if (value.includes(comp)) {
620
+ if (not) throw new ConstraintFail('!contains', this.notContains)
621
+ } else {
622
+ if (!not) throw new ConstraintFail('contains', this.contains)
623
+ }
624
+ }
625
+ if (this.elementConstraints || this.elementCheckType) {
626
+ const checkType = this.elementCheckType === undefined ? ElementCheckType.all : this.elementCheckType
627
+ let i = 0
628
+ let count = 0
629
+ let step = 1
630
+ let firstCount = 0
631
+ let thenCount = 0
632
+ let counting = false
633
+ const tested: any = {}
634
+ switch (checkType) {
635
+ case ElementCheckType.none:
636
+ firstCount = 0
637
+ thenCount = 0
638
+ counting = false
639
+ break
640
+ case ElementCheckType.all:
641
+ firstCount = length
642
+ step = 1
643
+ thenCount = 0
644
+ counting = true
645
+ break
646
+ case ElementCheckType.first:
647
+ firstCount = parseInt('' + this.elementCheckParameter)
648
+ step = 1
649
+ thenCount = 0
650
+ counting = true
651
+ break
652
+ case ElementCheckType.last:
653
+ firstCount = 0
654
+ step = 1
655
+ thenCount = length - this.elementCheckParameter
656
+ counting = false
657
+ break
658
+ case ElementCheckType.firstThenLast:
659
+ firstCount = parseInt('' + this.elementCheckParameter)
660
+ thenCount = length - this.elementCheckParameter
661
+ if (thenCount < 0) thenCount = length
662
+ step = 1
663
+ counting = true
664
+ break
665
+ case ElementCheckType.step:
666
+ firstCount = length
667
+ thenCount = 0
668
+ counting = true
669
+ step = parseInt('' + this.elementCheckParameter)
670
+ break
671
+ case ElementCheckType.random:
672
+ firstCount = 0
673
+ thenCount = parseInt('' + this.elementCheckParameter)
674
+ step = 0
675
+ counting = true
676
+ break
677
+ case ElementCheckType.firstThenStep:
678
+ firstCount = parseInt('' + this.elementCheckParameter)
679
+ thenCount = length - firstCount
680
+ step = parseInt('' + this.elementCheckParameter2)
681
+ counting = true
682
+ break
683
+ case ElementCheckType.firstThenRandom:
684
+ firstCount = parseInt('' + this.elementCheckParameter)
685
+ thenCount = parseInt('' + this.elementCheckParameter2)
686
+ step = 0
687
+ counting = true
688
+ break
689
+ }
690
+ while (i < length) {
691
+ if (counting) {
692
+ const ev = value[i]
693
+ let t: string = typeof ev
694
+ if (Array.isArray(ev)) t = 'array'
695
+ const m = (this.elementConstraints as unknown as Map<string, TypeConstraint>)
696
+ const c = m && m.get(t)
697
+ const tc = (c != null) || parseConstraints(t, '')
698
+ if (tc !== true && tc?.test != undefined) tc.test(ev)
699
+ count++
700
+ }
701
+ if ((checkType === ElementCheckType.last || checkType === ElementCheckType.firstThenLast) && i === thenCount) {
702
+ counting = true
703
+ }
704
+ if (checkType === ElementCheckType.firstThenLast && i === firstCount) {
705
+ counting = false
706
+ }
707
+
708
+ if (count >= firstCount) {
709
+ if (count >= firstCount + thenCount) {
710
+ break
711
+ }
712
+ }
713
+ if (step) {
714
+ i += step
715
+ } else {
716
+ while (true) {
717
+ const rr = Math.floor(Math.random() * (length - count))
718
+ i = count + rr
719
+ if (i < length && !tested[i]) {
720
+ tested[i] = true
721
+ break
722
+ }
723
+ }
724
+ }
725
+ }
726
+ }
727
+ }
728
+
729
+ toString () {
730
+ const keys: string[] = []
731
+ if (this.minLength) keys.push(`Min Length = ${this.minLength}`)
732
+ if (this.maxLength) keys.push(`Max Length = ${this.maxLength}`)
733
+ if (this.contains) keys.push(`Contains = ${this.contains}`)
734
+ if (this.notContains) keys.push(`!Contains = ${this.notContains}`)
735
+ if (this.elementConstraints) keys.push(`each element of the array has the following constraints by type ${listEachConstraints(this.elementConstraints)}`)
736
+ if (this.elementCheckType) keys.push(`(elements will be tested using the ${checkTypeToString(this.elementCheckType, this.elementCheckParameter, this.elementCheckParameter2)} method)`)
737
+ if (this.note) keys.push(this.note)
738
+ return (keys.length > 0) ? '- ' + keys.join(',') : super.toString()
739
+ }
740
+
741
+ describe () {
742
+ const keys: string[] = []
743
+ if (this.minLength) keys.push(`array must contain at least ${this.minLength} elements`)
744
+ if (this.maxLength) keys.push(`array must contain no more than ${this.maxLength} elements`)
745
+ if (this.contains) keys.push(`array must contain element value "${this.contains}"`)
746
+ if (this.notContains) keys.push(`array must not contain an element value "${this.notContains}"`)
747
+ if (this.elementConstraints) keys.push(`each element of the array has the following constraints by type ${listEachConstraints(this.elementConstraints)}`)
748
+ if (this.elementCheckType) keys.push(`(elements will be tested using the ${checkTypeToString(this.elementCheckType, this.elementCheckParameter, this.elementCheckParameter2)} method)`)
749
+ if (this.note || this.badName) keys.push(super.describe())
750
+ return (keys.length > 0) ? keys.join('\n') : super.describe()
751
+ }
752
+ }
753
+
754
+ function listEachConstraints (cmap: any) {
755
+ let out = ''
756
+ const types = cmap.keys()
757
+ const entries = cmap.entries()
758
+ let entry
759
+ while ((entry = entries.next().value)) {
760
+ out += '<br/><b>' + entry[0] + ' elements:</b><br/>&nbsp;&nbsp; -'
761
+ out += entry[1].describe().replace(/\n/g, '<br/>&nbsp;&nbsp; - ')
762
+ }
763
+ return out
764
+ }
765
+
766
+ /**
767
+ * Translates a type string (number, string, boolean, object, array, regex) into the corresponding ValueType enum
768
+ * Note that strings beside none, array, and regex are synonymous with the `typeof` operator value
769
+ * @param str
770
+ */
771
+ export function valueTypeFromString (str: string): ValueType {
772
+ switch (str.trim().toLowerCase()) {
773
+ default: return str.trim().length ? str.includes('[]') ? ValueType.array : ValueType.object : ValueType.none
774
+ case 'number': return ValueType.number
775
+ case 'string': return ValueType.string
776
+ case 'boolean': return ValueType.boolean
777
+ case 'object': return ValueType.object
778
+ case 'array': return ValueType.array
779
+ case 'regex': return ValueType.regex
780
+ case 'regexp': return ValueType.regex
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Translates a ValueType enum value into the corresponding string.
786
+ * Note that strings beside none, array, and regex are synonymous with the `typeof` operator value
787
+ * @param vt
788
+ */
789
+ export function stringFromValueType (vt: ValueType): string {
790
+ switch (vt) {
791
+ case ValueType.none: return ''
792
+ case ValueType.number: return 'number'
793
+ case ValueType.string: return 'string'
794
+ case ValueType.boolean: return 'boolean'
795
+ case ValueType.object: return 'object'
796
+ case ValueType.array: return 'array'
797
+ case ValueType.regex: return 'regex'
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Read either a value or a list from an expression value
803
+ * @param str
804
+ */
805
+ function constraintListParse (str = '') {
806
+ str.trim()
807
+ if (str.charAt(0) === '"' || str.charAt(0) === "'") {
808
+ str = str.substring(1, str.length - 1)
809
+ }
810
+ if (str.includes(',')) {
811
+ return str.split(',') // return the split array
812
+ }
813
+ if (isFinite(Number(str))) {
814
+ return Number(str)
815
+ }
816
+ return str // return the unquoted string value
817
+ }
818
+
819
+ /**
820
+ * Used to parse the type+constraints blocks from an "each" directive list
821
+ * @param str
822
+ */
823
+ function eachListParse (str = '') {
824
+ const map = new Map<string, TypeConstraint>()
825
+ const esplit = str.split('|')
826
+ for (const tblock of esplit) {
827
+ const ci = tblock.indexOf(',')
828
+ if (ci !== -1) {
829
+ const type = tblock.substring(0, ci).trim()
830
+ const cdef = tblock.substring(ci + 1)
831
+ const constraint = (parseConstraints(type, cdef) != null) || new TypeConstraint()
832
+ if (constraint !== undefined && constraint !== true) {
833
+ map.set(type, constraint)
834
+ }
835
+ }
836
+ }
837
+ return map
838
+ }
839
+
840
+ /**
841
+ * Parse out the checkType and return the resulting type name and the parsed parameters in a structure.
842
+ * @param ctStr
843
+ * @return {{string}name,{number}[p1],{number}[p2]}
844
+ */
845
+ function parseCheckType (ctStr: string = ''): { name: string, p1?: number, p2?: number } {
846
+ let opi = ctStr.indexOf('(')
847
+ if (opi === -1) opi = ctStr.length
848
+ const name = ctStr.substring(0, opi)
849
+ let cpi = ctStr.indexOf(')', opi)
850
+ if (cpi === -1) cpi = ctStr.length
851
+ const p = ctStr.substring(opi + 1, cpi).split(',')
852
+ let p1: any, p2: any
853
+ try {
854
+ p1 = p[0] && parseInt(p[0])
855
+ p2 = p[1] && parseInt(p[1])
856
+ } catch (e) {}
857
+ return { name, p1, p2 }
858
+ }
859
+
860
+ function checkTypeToString (ct: ElementCheckType, p1?: number | string, p2?: number | string) {
861
+ switch (ct) {
862
+ case ElementCheckType.random:
863
+ return `random(${p1})`
864
+ case ElementCheckType.step:
865
+ return `step(${p1})`
866
+ case ElementCheckType.first:
867
+ return `first(${p1})`
868
+ case ElementCheckType.last:
869
+ return `last(${p1})`
870
+ case ElementCheckType.firstThenLast:
871
+ return `firstThenLast(${p1},${p2})`
872
+ case ElementCheckType.firstThenStep:
873
+ return `firstThenStep(${p1},${p2})`
874
+ case ElementCheckType.firstThenRandom:
875
+ return `firstThenRandom(${p1},${p2})`
876
+ case ElementCheckType.none:
877
+ return 'none'
878
+ default:
879
+ case ElementCheckType.all:
880
+ return 'all'
881
+ }
882
+ }
883
+ function checkTypeFromString (ctstr: string): ElementCheckType {
884
+ switch (ctstr.trim().toLowerCase()) {
885
+ case 'random': return ElementCheckType.random
886
+ case 'step': return ElementCheckType.step
887
+ case 'first': return ElementCheckType.first
888
+ case 'last': return ElementCheckType.last
889
+ case 'firstthenlast': return ElementCheckType.firstThenLast
890
+ case 'firstthenstep': return ElementCheckType.firstThenStep
891
+ case 'firstthenrandom': return ElementCheckType.firstThenRandom
892
+ case 'none': return ElementCheckType.none
893
+ default:
894
+ case 'all': return ElementCheckType.all
895
+ }
896
+ }
897
+
898
+ // parse constraints from what may be more than one type (e.g. string|number)
899
+ export function parseConstraintsToMap (typeString: string, blockSet: string = ''): Map<string, TypeConstraint> {
900
+ const map = new Map<string, TypeConstraint>()
901
+ const types = typeString.split('|')
902
+ const blocks = blockSet.split(',')
903
+ for (let type of types) {
904
+ type = (type || '').trim()
905
+ const constraint = (parseConstraints(type, blockSet) != null) || new TypeConstraint()
906
+ if (constraint !== undefined && constraint !== true) {
907
+ map.set(type, constraint)
908
+ }
909
+ }
910
+ return map
911
+ }
912
+
913
+ /**
914
+ * Given a block of text, parse as constraints and return the set if this is a constraint declaration
915
+ * otherwise, return ConstraintStatus.NotConstraint to signify this is a description block and not a constraint declaration
916
+ * @param type - the type parsed from the param or return declaration
917
+ * @param block - the block of text to evaluate
918
+ * @param delim - the split delimiter (defaults to ',')
919
+ */
920
+ export function parseConstraints (type: string, block: string, delim = ','): TypeConstraint | undefined {
921
+ let constraint
922
+ if (!block || !type) return
923
+ const valueType = valueTypeFromString(type)
924
+ let cblock = block.trim()
925
+ // get any constraint parameters
926
+ let fpi = cblock.indexOf('(')
927
+ while (fpi !== -1) {
928
+ let cpi = cblock.indexOf(')', fpi)
929
+ if (cpi === -1) cpi = cblock.length
930
+ const swap = cblock.substring(fpi, cpi).replace(/,/g, ';;')
931
+ cblock = cblock.substring(0, fpi) + swap + cblock.substring(cpi)
932
+ fpi = cblock.indexOf('(', cpi)
933
+ }
934
+
935
+ const expressions = cblock.split(delim)
936
+ for (let expr of expressions) {
937
+ let expVal
938
+ let params
939
+ let not = false
940
+ expr = expr.trim()
941
+ if (!expr.startsWith('match') && !expr.startsWith('!match')) {
942
+ const cpi = expr.indexOf('(')
943
+ if (cpi !== -1) {
944
+ params = expr.substring(cpi).replace(/;;/g, ',').trim()
945
+ if (params.charAt(0) === '(') params = params.substring(1)
946
+ if (params.charAt(params.length - 1) === ')') params = params.substring(0, params.length - 1)
947
+ expr = expr.substring(0, cpi).trim()
948
+ if (expr === 'each') {
949
+ expVal = eachListParse(params)
950
+ } else {
951
+ expVal = constraintListParse(params)
952
+ }
953
+ }
954
+ }
955
+ if (expr.charAt(0) === '!') {
956
+ not = true
957
+ expr = expr.substring(1)
958
+ }
959
+ if (expr.includes('=')) {
960
+ const p = expr.split('=')
961
+ if (p.length > 2) {
962
+ p[1] = p.slice(1).join('=')
963
+ }
964
+ expr = p[0].trim()
965
+ if (expr === 'each') {
966
+ expVal = eachListParse(p[1])
967
+ } else {
968
+ expVal = constraintListParse(p[1])
969
+ }
970
+ }
971
+ expr = expr.trim().toLowerCase()
972
+ switch (valueType) {
973
+ case ValueType.number:
974
+ constraint = (constraint as NumberConstraint) || new NumberConstraint()
975
+ switch (expr) {
976
+ case 'noconstraint':
977
+ case 'no constraint':
978
+ return constraint // early exit if we encounter "- No Constraint"
979
+
980
+ /* Integer, Positive, Negative, NotZero, min, max */
981
+ case 'integer':
982
+ constraint.isInteger = true
983
+ break
984
+ case 'positive':
985
+ constraint.isPositive = true
986
+ break
987
+ case 'negative':
988
+ constraint.isNegative = true
989
+ break
990
+ case 'notzero':
991
+ case 'not zero':
992
+ case 'nonzero':
993
+ constraint.notZero = true
994
+ break
995
+ case 'min':
996
+ constraint.min = expVal as number
997
+ break
998
+ case 'max':
999
+ constraint.max = expVal as number
1000
+ break
1001
+ case 'maxx':
1002
+ constraint.maxx = expVal as number
1003
+ break
1004
+
1005
+ case 'note':
1006
+ constraint.note = expVal as string
1007
+ break
1008
+ default:
1009
+ constraint.badName = expr
1010
+ break
1011
+ }
1012
+
1013
+ break
1014
+ case ValueType.string:
1015
+ // minLength, maxLength, (!)startsWith, (!)endsWith, (!)contains, (!)match
1016
+ constraint = (constraint as StringConstraint) || new StringConstraint()
1017
+ switch (expr) {
1018
+ case 'noconstraint':
1019
+ case 'no constraint':
1020
+ return constraint // early exit if we encounter "- No Constraint"
1021
+
1022
+ case 'minlength':
1023
+ constraint.minLength = expVal as number
1024
+ break
1025
+ case 'maxlength':
1026
+ constraint.maxLength = expVal as number
1027
+ break
1028
+ case 'startswith':
1029
+ not ? constraint.notStartsWith = expVal as string : constraint.startsWith = expVal as string
1030
+ break
1031
+ case 'endswith':
1032
+ not ? constraint.notEndsWith = expVal as string : constraint.endsWith = expVal as string
1033
+ break
1034
+ case 'contains':
1035
+ not ? constraint.notContains = expVal as string : constraint.contains = expVal as string
1036
+ break
1037
+ case 'match':
1038
+ not ? constraint.notMatch = expVal as string : constraint.match = expVal as string
1039
+ break
1040
+ case 'note':
1041
+ constraint.note = expVal as string
1042
+ break
1043
+ default:
1044
+ constraint.badName = expr
1045
+ break
1046
+ }
1047
+ break
1048
+ case ValueType.object:
1049
+ // (!)empty, (!)hasProperties, notNested, noPrototype, canSerialize, noUndefinedProps
1050
+ constraint = (constraint as ObjectConstraint) || new ObjectConstraint()
1051
+ switch (expr) {
1052
+ case 'noconstraint':
1053
+ case 'no constraint':
1054
+ return constraint // early exit if we encounter "- No Constraint"
1055
+
1056
+ case 'empty':
1057
+ constraint.empty = !not
1058
+ constraint.notEmpty = not
1059
+ break
1060
+ case 'hasproperties':
1061
+ case 'has properties':
1062
+ if (typeof expVal === 'string') expVal = [expVal]
1063
+ not ? constraint.notHasProperties = expVal as string[] : constraint.hasProperties = expVal as string[]
1064
+ break
1065
+ case 'notnested':
1066
+ case 'not nested':
1067
+ constraint.notNested = true
1068
+ break
1069
+ case 'noprototype':
1070
+ case 'no prototype':
1071
+ constraint.noPrototype = true
1072
+ break
1073
+ case 'canserialize':
1074
+ case 'can serialize':
1075
+ constraint.canSerialize = true
1076
+ break
1077
+ case 'notruthyprops':
1078
+ case 'no truthy props':
1079
+ constraint.noTruthyProps = true
1080
+ break
1081
+ case 'nofalseyprops':
1082
+ case 'no falsey props':
1083
+ constraint.noFalseyProps = true
1084
+ break
1085
+ case 'instanceof':
1086
+ case 'instance of':
1087
+ if (not) constraint.notInstanceOf = expVal as string
1088
+ else constraint.instanceOf = expVal as string
1089
+ break
1090
+ case 'note':
1091
+ constraint.note = expVal as string
1092
+ break
1093
+ default:
1094
+ constraint.badName = expr
1095
+ break
1096
+ }
1097
+ break
1098
+ case ValueType.array:
1099
+ // minLength, maxLength, (!)contains, each:
1100
+ constraint = (constraint as ArrayConstraint) || new ArrayConstraint()
1101
+ switch (expr) {
1102
+ case 'noconstraint':
1103
+ case 'no constraint':
1104
+ return constraint // early exit if we encounter "- No Constraint"
1105
+
1106
+ case 'minlength':
1107
+ case 'min length':
1108
+ constraint.minLength = expVal as number
1109
+ break
1110
+ case 'maxlength':
1111
+ case 'max length':
1112
+ constraint.maxLength = expVal as number
1113
+ break
1114
+ case 'contains':
1115
+ not ? constraint.notContains = expVal as string : constraint.contains = expVal as string
1116
+ break
1117
+ case 'checktype':
1118
+ case 'check type':
1119
+ const psplit = (params || '').split(',')
1120
+ const pct = parseCheckType('' + expVal)
1121
+ constraint.elementCheckType = checkTypeFromString(pct.name)
1122
+ constraint.elementCheckParameter = (psplit[0] || pct.p1) as number
1123
+ constraint.elementCheckParameter2 = (psplit[1] || pct.p2) as number
1124
+ break
1125
+ case 'each':
1126
+ const type = 'any'
1127
+ constraint.elementConstraints = expVal as any
1128
+ break
1129
+ case 'note':
1130
+ constraint.note = expVal as string
1131
+ break
1132
+ default:
1133
+ constraint.badName = expr
1134
+ break
1135
+ }
1136
+ break
1137
+ default: // none, boolean, regex
1138
+ if (expr === 'no constraint') return
1139
+ constraint = new TypeConstraint(stringFromValueType(valueType))
1140
+ break
1141
+ }
1142
+ }
1143
+ return constraint
1144
+ }
1145
+
1146
+ /**
1147
+ * Simple test to see if a value adheres to a set of constraints
1148
+ */
1149
+ export function validate (
1150
+
1151
+ value: any, // The value to test for constraints. Must be one of the basic types supported by contraints
1152
+ constraintString: string // the constraints to test it against. Constraints listed must match the type being tested. Do not include < > brackets.
1153
+
1154
+ ): string // returns '' if no violation, otherwise returns the error string of the ConstraintError encountered
1155
+ {
1156
+ let type: string = typeof value
1157
+ if (type === 'object') {
1158
+ if (Array.isArray(value)) type = 'array'
1159
+ }
1160
+ const tc = parseConstraints(type, constraintString || '')
1161
+ let ok: string = ''
1162
+ try {
1163
+ if (tc != null) tc.test(value)
1164
+ } catch (e: any) {
1165
+ ok = e.message || e.toString()
1166
+ }
1167
+ return ok
1168
+ }