@rosen-bridge/config 0.1.0 → 0.2.1
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/CHANGELOG.md +12 -0
- package/dist/config.d.ts +143 -126
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +570 -556
- package/dist/index.js +1 -1
- package/dist/schema/Validators/fieldProperties.d.ts +27 -24
- package/dist/schema/Validators/fieldProperties.d.ts.map +1 -1
- package/dist/schema/Validators/fieldProperties.js +215 -248
- package/dist/schema/types/fields.d.ts +24 -22
- package/dist/schema/types/fields.d.ts.map +1 -1
- package/dist/schema/types/fields.js +1 -1
- package/dist/schema/types/validations.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/value/validators.d.ts.map +1 -1
- package/dist/value/validators.js +149 -176
- package/lib/config.ts +128 -50
- package/lib/schema/Validators/fieldProperties.ts +18 -0
- package/lib/schema/types/fields.ts +8 -1
- package/lib/value/validators.ts +5 -0
- package/package.json +1 -1
- package/tests/config.spec.ts +61 -1
- package/tests/configTestData.ts +251 -12
- package/tsconfig.build.tsbuildinfo +1 -1
package/lib/config.ts
CHANGED
|
@@ -23,59 +23,93 @@ export class ConfigValidator {
|
|
|
23
23
|
* @param {Record<string, any>} config
|
|
24
24
|
*/
|
|
25
25
|
public validateConfig(config: Record<string, any>) {
|
|
26
|
-
const errorPreamble = (path: Array<string>) =>
|
|
27
|
-
`config validation failed for "${path.join('.')}" field`;
|
|
28
|
-
|
|
29
|
-
const stack: Array<{
|
|
30
|
-
subSchema: ConfigSchema;
|
|
31
|
-
subConfig: Record<string, any> | undefined;
|
|
32
|
-
parentPath: Array<string>;
|
|
33
|
-
}> = [
|
|
34
|
-
{
|
|
35
|
-
subSchema: this.schema,
|
|
36
|
-
subConfig: config,
|
|
37
|
-
parentPath: [],
|
|
38
|
-
},
|
|
39
|
-
];
|
|
40
|
-
|
|
41
26
|
this.validateValue(
|
|
42
27
|
config,
|
|
43
28
|
{ type: 'object', children: this.schema },
|
|
44
29
|
config
|
|
45
30
|
);
|
|
46
31
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
while (stack.length > 0) {
|
|
50
|
-
const { subSchema, subConfig, parentPath } = stack.pop()!;
|
|
51
|
-
// Process children of current field
|
|
52
|
-
for (const name of Object.keys(subSchema)) {
|
|
53
|
-
const path = parentPath.concat([name]);
|
|
54
|
-
try {
|
|
55
|
-
const field = subSchema[name];
|
|
56
|
-
let value = undefined;
|
|
57
|
-
if (subConfig != undefined && Object.hasOwn(subConfig, name)) {
|
|
58
|
-
value = subConfig[name];
|
|
59
|
-
}
|
|
32
|
+
this.validateSubConfig(config, config, this.schema, []);
|
|
33
|
+
}
|
|
60
34
|
|
|
61
|
-
|
|
35
|
+
/**
|
|
36
|
+
* traverses and validates a subconfig using the subschema
|
|
37
|
+
*
|
|
38
|
+
* @private
|
|
39
|
+
* @param {Record<string, any>} config
|
|
40
|
+
* @param {Record<string, any>} subConfig
|
|
41
|
+
* @param {ConfigSchema} subSchema
|
|
42
|
+
* @param {string[]} path
|
|
43
|
+
* @memberof ConfigValidator
|
|
44
|
+
*/
|
|
45
|
+
private validateSubConfig(
|
|
46
|
+
config: Record<string, any>,
|
|
47
|
+
subConfig: Record<string, any>,
|
|
48
|
+
subSchema: ConfigSchema,
|
|
49
|
+
path: string[]
|
|
50
|
+
) {
|
|
51
|
+
const errorPreamble = (path: Array<string>) =>
|
|
52
|
+
`config validation failed for "${path.join('.')}" field`;
|
|
53
|
+
for (const name of Object.keys(subSchema)) {
|
|
54
|
+
const childPath = path.concat([name]);
|
|
55
|
+
try {
|
|
56
|
+
const field = subSchema[name];
|
|
57
|
+
let value = undefined;
|
|
58
|
+
if (subConfig != undefined && Object.hasOwn(subConfig, name)) {
|
|
59
|
+
value = subConfig[name];
|
|
60
|
+
}
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
this.validateValue(value, field, config);
|
|
63
|
+
|
|
64
|
+
// if a node/field is of type object and thus is a subtree, traverse it
|
|
65
|
+
if (field.type === 'object') {
|
|
66
|
+
this.validateSubConfig(config, value, field.children, childPath);
|
|
67
|
+
} else if (field.type === 'array') {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
for (const item of value) {
|
|
70
|
+
ConfigValidator.modifyObject(config, item, childPath);
|
|
71
|
+
this.validateSubConfig(
|
|
72
|
+
config,
|
|
73
|
+
{ [name]: item },
|
|
74
|
+
{ [name]: field.items },
|
|
75
|
+
childPath
|
|
76
|
+
);
|
|
77
|
+
ConfigValidator.modifyObject(config, value, childPath);
|
|
78
|
+
}
|
|
71
79
|
}
|
|
72
|
-
} catch (error: any) {
|
|
73
|
-
throw new Error(`${errorPreamble(path)}: ${error.message}`);
|
|
74
80
|
}
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
throw new Error(`${errorPreamble(childPath)}: ${error.message}`);
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/**
|
|
88
|
+
* sets an object's specific subtree to the specified value
|
|
89
|
+
*
|
|
90
|
+
* @static
|
|
91
|
+
* @param {Record<string, any>} obj
|
|
92
|
+
* @param {*} newValue
|
|
93
|
+
* @param {string[]} path
|
|
94
|
+
* @return {*}
|
|
95
|
+
* @memberof ConfigValidator
|
|
96
|
+
*/
|
|
97
|
+
static modifyObject(obj: Record<string, any>, newValue: any, path: string[]) {
|
|
98
|
+
let value: any = obj;
|
|
99
|
+
for (const key of path.slice(0, -1)) {
|
|
100
|
+
if (value != undefined && Object.hasOwn(value, key)) {
|
|
101
|
+
value = value[key];
|
|
102
|
+
} else {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lastKey = path.at(-1);
|
|
108
|
+
if (lastKey != undefined) {
|
|
109
|
+
value[lastKey] = newValue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
/**
|
|
80
114
|
* validates a value in config object
|
|
81
115
|
*
|
|
@@ -98,7 +132,11 @@ export class ConfigValidator {
|
|
|
98
132
|
valueValidators[field.type](value, field);
|
|
99
133
|
}
|
|
100
134
|
|
|
101
|
-
if (
|
|
135
|
+
if (
|
|
136
|
+
field.type !== 'object' &&
|
|
137
|
+
field.type !== 'array' &&
|
|
138
|
+
field.validations
|
|
139
|
+
) {
|
|
102
140
|
for (const validation of field.validations) {
|
|
103
141
|
const name = Object.keys(validation).filter(
|
|
104
142
|
(key) => key !== 'when' && key !== 'error'
|
|
@@ -199,6 +237,11 @@ export class ConfigValidator {
|
|
|
199
237
|
subSchema: field.children,
|
|
200
238
|
parentPath: path,
|
|
201
239
|
});
|
|
240
|
+
} else if (field.type === 'array') {
|
|
241
|
+
stack.push({
|
|
242
|
+
subSchema: { [name]: field.items },
|
|
243
|
+
parentPath: path,
|
|
244
|
+
});
|
|
202
245
|
}
|
|
203
246
|
} catch (error: any) {
|
|
204
247
|
throw new Error(`${errorPreamble(path)}: ${error.message}`);
|
|
@@ -228,7 +271,7 @@ export class ConfigValidator {
|
|
|
228
271
|
if (
|
|
229
272
|
!Object.hasOwn(propertyValidators.all, key) &&
|
|
230
273
|
!(
|
|
231
|
-
field.type
|
|
274
|
+
!['object', 'array'].includes(field.type) &&
|
|
232
275
|
Object.hasOwn(propertyValidators.primitive, key)
|
|
233
276
|
) &&
|
|
234
277
|
!Object.hasOwn(propertyValidators[field.type], key)
|
|
@@ -241,7 +284,7 @@ export class ConfigValidator {
|
|
|
241
284
|
validator(field, this);
|
|
242
285
|
}
|
|
243
286
|
|
|
244
|
-
if (field.type !== 'object') {
|
|
287
|
+
if (field.type !== 'object' && field.type !== 'array') {
|
|
245
288
|
for (const validator of Object.values(propertyValidators.primitive)) {
|
|
246
289
|
validator(field, this);
|
|
247
290
|
}
|
|
@@ -264,7 +307,12 @@ export class ConfigValidator {
|
|
|
264
307
|
for (const part of path) {
|
|
265
308
|
if (subTree != undefined && Object.hasOwn(subTree, part)) {
|
|
266
309
|
field = subTree[part];
|
|
267
|
-
subTree =
|
|
310
|
+
subTree =
|
|
311
|
+
'children' in field
|
|
312
|
+
? field.children
|
|
313
|
+
: 'items' in field && 'children' in field.items
|
|
314
|
+
? field.items.children
|
|
315
|
+
: undefined;
|
|
268
316
|
} else {
|
|
269
317
|
return undefined;
|
|
270
318
|
}
|
|
@@ -326,7 +374,7 @@ export class ConfigValidator {
|
|
|
326
374
|
fieldName: childName,
|
|
327
375
|
children: Object.keys(field.children).reverse(),
|
|
328
376
|
});
|
|
329
|
-
} else if (field.default != undefined) {
|
|
377
|
+
} else if (field.type !== 'array' && field.default != undefined) {
|
|
330
378
|
value[childName] = field.default;
|
|
331
379
|
}
|
|
332
380
|
}
|
|
@@ -384,7 +432,10 @@ export class ConfigValidator {
|
|
|
384
432
|
// if a node/field is of type object and thus is a subtree, add it to
|
|
385
433
|
// the stack to be traversed later. Otherwise it's a leaf and needs no
|
|
386
434
|
// traversal.
|
|
387
|
-
if (
|
|
435
|
+
if (
|
|
436
|
+
field.type === 'object' ||
|
|
437
|
+
(field.type === 'array' && field.items.type === 'object')
|
|
438
|
+
) {
|
|
388
439
|
let childTypeName = `${childName[0].toUpperCase()}${childName.substring(
|
|
389
440
|
1
|
|
390
441
|
)}`;
|
|
@@ -394,17 +445,44 @@ export class ConfigValidator {
|
|
|
394
445
|
childTypeName += typeNameCount.toString();
|
|
395
446
|
}
|
|
396
447
|
|
|
448
|
+
const children =
|
|
449
|
+
field.type === 'array' && field.items.type === 'object'
|
|
450
|
+
? field.items.children
|
|
451
|
+
: field.type === 'object'
|
|
452
|
+
? field.children
|
|
453
|
+
: {};
|
|
454
|
+
|
|
397
455
|
stack.push({
|
|
398
|
-
subSchema:
|
|
399
|
-
children: Object.keys(
|
|
456
|
+
subSchema: children,
|
|
457
|
+
children: Object.keys(children).reverse(),
|
|
400
458
|
parentPath: path,
|
|
401
459
|
typeName: childTypeName,
|
|
402
460
|
attributes: [],
|
|
403
461
|
});
|
|
404
462
|
|
|
405
|
-
attributes.push([
|
|
463
|
+
attributes.push([
|
|
464
|
+
childName,
|
|
465
|
+
field.type === 'array' ? `${childTypeName}[]` : childTypeName,
|
|
466
|
+
]);
|
|
406
467
|
} else {
|
|
407
|
-
|
|
468
|
+
let fieldType: string =
|
|
469
|
+
field.type === 'array' ? field.items.type : field.type;
|
|
470
|
+
let isOptional = true;
|
|
471
|
+
if (field.type !== 'array' && field.validations != undefined) {
|
|
472
|
+
for (const validation of field.validations) {
|
|
473
|
+
if (field.type === 'string' && 'choices' in validation) {
|
|
474
|
+
fieldType = validation.choices.map((c) => `'${c}'`).join(' | ');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if ('required' in validation && !('when' in validation)) {
|
|
478
|
+
isOptional = false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
attributes.push([
|
|
483
|
+
isOptional ? `${childName}?` : childName,
|
|
484
|
+
field.type === 'array' ? `${fieldType}[]` : fieldType,
|
|
485
|
+
]);
|
|
408
486
|
}
|
|
409
487
|
} catch (error: any) {
|
|
410
488
|
throw new Error(`${errorPreamble(path)}: ${error.message}`);
|
|
@@ -425,7 +503,7 @@ export class ConfigValidator {
|
|
|
425
503
|
name: string,
|
|
426
504
|
attributes: Array<[string, string]>
|
|
427
505
|
): string => {
|
|
428
|
-
return `interface ${name} {
|
|
506
|
+
return `export interface ${name} {
|
|
429
507
|
${attributes.map((attr) => `${attr[0]}: ${attr[1]};`).join('\n ')}
|
|
430
508
|
}`;
|
|
431
509
|
};
|
|
@@ -102,9 +102,26 @@ export const propertyValidators = {
|
|
|
102
102
|
},
|
|
103
103
|
object: {
|
|
104
104
|
children: (field: types.ObjectField, config: ConfigValidator) => {
|
|
105
|
+
if (
|
|
106
|
+
!Object.hasOwn(field, 'children') ||
|
|
107
|
+
typeof field.children !== 'object'
|
|
108
|
+
) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`object field type must have a "children" property of type "object"`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
105
113
|
return;
|
|
106
114
|
},
|
|
107
115
|
},
|
|
116
|
+
array: {
|
|
117
|
+
items: (field: types.ArrayField, config: ConfigValidator) => {
|
|
118
|
+
if (!Object.hasOwn(field, 'items') || typeof field.items !== 'object') {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`array field type must have a "items" property of type "object"`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
},
|
|
108
125
|
string: {
|
|
109
126
|
default: (field: types.StringField, config: ConfigValidator) => {
|
|
110
127
|
if (
|
|
@@ -199,6 +216,7 @@ const fieldValidations: Record<string, Record<string, any>> = {
|
|
|
199
216
|
}
|
|
200
217
|
},
|
|
201
218
|
},
|
|
219
|
+
boolean: {},
|
|
202
220
|
number: {
|
|
203
221
|
gt: (validation: VNumeric<number>) => {
|
|
204
222
|
if (!('gt' in validation)) {
|
|
@@ -4,7 +4,7 @@ export type PrimitiveValue = string | boolean | number | bigint;
|
|
|
4
4
|
|
|
5
5
|
export type ConfigSchema = Record<string, ConfigField>;
|
|
6
6
|
|
|
7
|
-
export type ConfigField = ObjectField | PrimitiveField;
|
|
7
|
+
export type ConfigField = ObjectField | ArrayField | PrimitiveField;
|
|
8
8
|
|
|
9
9
|
export type PrimitiveField =
|
|
10
10
|
| StringField
|
|
@@ -19,6 +19,13 @@ export interface ObjectField {
|
|
|
19
19
|
children: ConfigSchema;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface ArrayField {
|
|
23
|
+
type: 'array';
|
|
24
|
+
description?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
items: ConfigField;
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
export interface GenericField<T> {
|
|
23
30
|
default?: T;
|
|
24
31
|
description?: string;
|
package/lib/value/validators.ts
CHANGED
|
@@ -23,6 +23,11 @@ export const valueValidators: Record<string, any> = {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
|
+
array: (value: Array<any>, field: types.ArrayField) => {
|
|
27
|
+
if (!Array.isArray(value)) {
|
|
28
|
+
throw new Error(`value must be of array type`);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
26
31
|
string: (value: string, field: types.StringField) => {
|
|
27
32
|
if (typeof value !== 'string') {
|
|
28
33
|
throw new Error(`value must be of string type`);
|
package/package.json
CHANGED
package/tests/config.spec.ts
CHANGED
|
@@ -76,6 +76,44 @@ describe('ConfigValidator', () => {
|
|
|
76
76
|
)
|
|
77
77
|
).toThrow();
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @target validateSchema should throw exception when array type doesn't
|
|
82
|
+
* have an item property
|
|
83
|
+
* @dependencies
|
|
84
|
+
* @scenario
|
|
85
|
+
* - create a new instance of Config which calls Config.validateSchema
|
|
86
|
+
* - check if any exception is thrown
|
|
87
|
+
* @expected
|
|
88
|
+
* - exception should be thrown
|
|
89
|
+
*/
|
|
90
|
+
it(`should throw exception when array type doesn't have an item property`, async () => {
|
|
91
|
+
expect(
|
|
92
|
+
() =>
|
|
93
|
+
new ConfigValidator(
|
|
94
|
+
<ConfigSchema>testData.arrayTypeSchemaWithoutItems
|
|
95
|
+
)
|
|
96
|
+
).toThrow();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @target validateSchema should throw exception when object type doesn't
|
|
101
|
+
* have a children property
|
|
102
|
+
* @dependencies
|
|
103
|
+
* @scenario
|
|
104
|
+
* - create a new instance of Config which calls Config.validateSchema
|
|
105
|
+
* - check if any exception is thrown
|
|
106
|
+
* @expected
|
|
107
|
+
* - exception should be thrown
|
|
108
|
+
*/
|
|
109
|
+
it(`should throw exception when object type doesn't have a children property`, async () => {
|
|
110
|
+
expect(
|
|
111
|
+
() =>
|
|
112
|
+
new ConfigValidator(
|
|
113
|
+
<ConfigSchema>testData.objectTypeSchemaWithoutChildren
|
|
114
|
+
)
|
|
115
|
+
).toThrow();
|
|
116
|
+
});
|
|
79
117
|
});
|
|
80
118
|
|
|
81
119
|
describe('validateConfig', () => {
|
|
@@ -679,6 +717,28 @@ describe('ConfigValidator', () => {
|
|
|
679
717
|
testData.apiSchemaConfigPairWithStringNumber.config
|
|
680
718
|
);
|
|
681
719
|
});
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* @target validateConfig should throw exception when value doesn't match
|
|
723
|
+
* the schema array type
|
|
724
|
+
* @dependencies
|
|
725
|
+
* @scenario
|
|
726
|
+
* - call validateConfig with the config
|
|
727
|
+
* - check if any exception is thrown
|
|
728
|
+
* @expected
|
|
729
|
+
* - exception should be thrown
|
|
730
|
+
*/
|
|
731
|
+
it(`should throw exception when value doesn't match the schema array type`, async () => {
|
|
732
|
+
const confValidator = new ConfigValidator(
|
|
733
|
+
<ConfigSchema>testData.arraySchemaConfigPairWrongValueType.schema
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
expect(() =>
|
|
737
|
+
confValidator.validateConfig(
|
|
738
|
+
testData.arraySchemaConfigPairWrongValueType.config
|
|
739
|
+
)
|
|
740
|
+
).toThrow();
|
|
741
|
+
});
|
|
682
742
|
});
|
|
683
743
|
|
|
684
744
|
describe('valueAt', () => {
|
|
@@ -848,7 +908,7 @@ describe('ConfigValidator', () => {
|
|
|
848
908
|
|
|
849
909
|
expect(() =>
|
|
850
910
|
confValidator.validateAndWriteConfig(obj, config, 'local', 'json')
|
|
851
|
-
).toThrow(
|
|
911
|
+
).toThrow();
|
|
852
912
|
|
|
853
913
|
const savedObj = JSON.parse(
|
|
854
914
|
fs.readFileSync(path.join(configDir, 'local.json'), 'utf-8')
|