@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/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
- // Traverses the schema object tree depth first in coordination with config
48
- // object tree and validate config using the schema
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
- this.validateValue(value, field, config);
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
- // if a node/field is of type object and thus is a subtree, add it to
64
- // the stack to be traversed later
65
- if (field.type === 'object') {
66
- stack.push({
67
- subSchema: field.children,
68
- subConfig: value,
69
- parentPath: path,
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 (field.type != 'object' && field.validations) {
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 !== 'object' &&
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 = 'children' in field ? field.children : undefined;
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 (field.type === 'object') {
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: field.children,
399
- children: Object.keys(field.children).reverse(),
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([childName, childTypeName]);
463
+ attributes.push([
464
+ childName,
465
+ field.type === 'array' ? `${childTypeName}[]` : childTypeName,
466
+ ]);
406
467
  } else {
407
- attributes.push([childName, field.type]);
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rosen-bridge/config",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "a package to manage configs of rosen bridge projects",
5
5
  "repository": "git+https://github.com/rosen-bridge/utils.git",
6
6
  "license": "GPL-3.0",
@@ -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('value should be one of the choices');
911
+ ).toThrow();
852
912
 
853
913
  const savedObj = JSON.parse(
854
914
  fs.readFileSync(path.join(configDir, 'local.json'), 'utf-8')