@openmrs/esm-config 4.0.0-pre.0 → 4.0.0-pre.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.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +9 -8
  4. package/.turbo/turbo-typescript.log +2 -2
  5. package/dist/__mocks__/openmrs-esm-state.mock.d.ts +12 -0
  6. package/dist/openmrs-esm-module-config.js +1 -1
  7. package/dist/openmrs-esm-module-config.js.map +1 -1
  8. package/dist/src/index.d.ts +7 -0
  9. package/dist/src/module-config/module-config.d.ts +57 -0
  10. package/dist/src/module-config/module-config.test.d.ts +1 -0
  11. package/dist/src/module-config/state.d.ts +109 -0
  12. package/dist/src/navigation/interpolate-string.d.ts +34 -0
  13. package/dist/src/navigation/interpolate-string.test.d.ts +1 -0
  14. package/dist/src/navigation/navigate.d.ts +26 -0
  15. package/dist/src/navigation/navigate.test.d.ts +1 -0
  16. package/dist/src/public.d.ts +7 -0
  17. package/dist/src/types.d.ts +49 -0
  18. package/dist/src/validators/type-validators.d.ts +6 -0
  19. package/dist/src/validators/type-validators.test.d.ts +1 -0
  20. package/dist/src/validators/validator.d.ts +24 -0
  21. package/dist/src/validators/validators.d.ts +32 -0
  22. package/dist/src/validators/validators.test.d.ts +1 -0
  23. package/package.json +4 -4
  24. package/src/module-config/module-config.test.ts +63 -5
  25. package/src/module-config/module-config.ts +100 -42
  26. package/src/navigation/interpolate-string.test.ts +3 -3
  27. package/src/navigation/interpolate-string.ts +10 -2
  28. package/src/navigation/navigate.ts +10 -4
  29. package/src/validators/validator.ts +27 -2
  30. package/src/validators/validators.test.ts +18 -1
  31. package/src/validators/validators.ts +13 -1
@@ -0,0 +1,109 @@
1
+ import { Config, ConfigObject, ConfigSchema, ExtensionSlotConfigObject, ProvidedConfig } from "../types";
2
+ /**
3
+ * Internal store
4
+ * A store of the inputs and internal state
5
+ * @internal
6
+ */
7
+ export interface ConfigInternalStore {
8
+ /** Configs added using the `provide` function */
9
+ providedConfigs: Array<ProvidedConfig>;
10
+ /** An object with module names for keys and schemas for values */
11
+ schemas: Record<string, ConfigSchema>;
12
+ /** Whether to use dev defaults or not */
13
+ devDefaultsAreOn: boolean;
14
+ }
15
+ /**
16
+ * @internal
17
+ */
18
+ export declare const configInternalStore: import("unistore").Store<ConfigInternalStore>;
19
+ /**
20
+ * Temporary config
21
+ * LocalStorage-based config used by the implementer tools
22
+ * @internal
23
+ */
24
+ export interface TemporaryConfigStore {
25
+ config: Config;
26
+ }
27
+ /** @internal */
28
+ export declare const temporaryConfigStore: import("unistore").Store<TemporaryConfigStore>;
29
+ /**
30
+ * Config-side extension store
31
+ * Just what esm-config needs to know about extension state. This
32
+ * is to avoid having esm-config depend on esm-extensions, which would
33
+ * create a circular dependency.
34
+ * @internal
35
+ */
36
+ export interface ConfigExtensionStore {
37
+ mountedExtensions: Array<ConfigExtensionStoreElement>;
38
+ }
39
+ /** @internal */
40
+ export interface ConfigExtensionStoreElement {
41
+ slotModuleName: string;
42
+ extensionModuleName: string;
43
+ slotName: string;
44
+ extensionId: string;
45
+ }
46
+ /** @internal */
47
+ export declare const configExtensionStore: import("unistore").Store<ConfigExtensionStore>;
48
+ /**
49
+ * Output configs
50
+ *
51
+ * Each module has its own stores for its config and its extension slots' configs.
52
+ * @internal
53
+ */
54
+ export interface ConfigStore {
55
+ config: ConfigObject | null;
56
+ loaded: boolean;
57
+ }
58
+ /** @internal */
59
+ export declare function getConfigStore(moduleName: string): import("unistore").Store<ConfigStore>;
60
+ /**
61
+ * Configuration for all the specific extension slots
62
+ * @internal
63
+ */
64
+ export interface ExtensionSlotsConfigStore {
65
+ slots: {
66
+ [slotName: string]: {
67
+ config: ExtensionSlotConfigObject;
68
+ loaded: boolean;
69
+ };
70
+ };
71
+ }
72
+ /** @internal */
73
+ export declare function getExtensionSlotsConfigStore(): import("unistore").Store<ExtensionSlotsConfigStore>;
74
+ /** @internal */
75
+ export declare function getExtensionSlotConfig(slotName: string): {
76
+ config: ExtensionSlotConfigObject;
77
+ loaded: boolean;
78
+ };
79
+ /** @internal */
80
+ export declare function getExtensionSlotConfigFromStore(state: ExtensionSlotsConfigStore, slotName: string): {
81
+ config: ExtensionSlotConfigObject;
82
+ loaded: boolean;
83
+ };
84
+ /** @internal */
85
+ export interface ExtensionsConfigStore {
86
+ configs: {
87
+ [slotName: string]: {
88
+ [extensionId: string]: ConfigStore;
89
+ };
90
+ };
91
+ }
92
+ /**
93
+ * One store for all the extensions
94
+ * @internal
95
+ */
96
+ export declare function getExtensionsConfigStore(): import("unistore").Store<ExtensionsConfigStore>;
97
+ /** @internal */
98
+ export declare function getExtensionConfig(slotName: string, extensionId: string): ConfigStore;
99
+ /** @internal */
100
+ export declare function getExtensionConfigFromStore(state: ExtensionsConfigStore, slotName: string, extensionId: string): ConfigStore;
101
+ /**
102
+ * A store of the implementer tools output config
103
+ * @internal
104
+ */
105
+ export interface ImplementerToolsConfigStore {
106
+ config: Config;
107
+ }
108
+ /** @internal */
109
+ export declare const implementerToolsConfigStore: import("unistore").Store<ImplementerToolsConfigStore>;
@@ -0,0 +1,34 @@
1
+ /** @module @category Navigation */
2
+ /**
3
+ * Interpolates a string with openmrsBase and openmrsSpaBase.
4
+ *
5
+ * Useful for accepting `${openmrsBase}` or `${openmrsSpaBase}` template
6
+ * parameters in configurable URLs.
7
+ *
8
+ * @param template A string to interpolate
9
+ * @param additionalParams Additional values to interpolate into the string template
10
+ */
11
+ export declare function interpolateUrl(template: string, additionalParams?: {
12
+ [key: string]: string;
13
+ }): string;
14
+ /**
15
+ * Interpolates values of `params` into the `template` string.
16
+ *
17
+ * Useful for additional template parameters in URLs.
18
+ *
19
+ * Example usage:
20
+ * ```js
21
+ * navigate({
22
+ * to: interpolateString(
23
+ * config.links.patientChart,
24
+ * { patientUuid: patient.uuid }
25
+ * )
26
+ * });
27
+ * ```
28
+ *
29
+ * @param template With optional params wrapped in `${ }`
30
+ * @param params Values to interpolate into the string template
31
+ */
32
+ export declare function interpolateString(template: string, params: {
33
+ [key: string]: string;
34
+ }): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ export declare type TemplateParams = {
2
+ [key: string]: string;
3
+ };
4
+ export interface NavigateOptions {
5
+ to: string;
6
+ templateParams?: TemplateParams;
7
+ }
8
+ /**
9
+ * Calls `location.assign` for non-SPA paths and [navigateToUrl](https://single-spa.js.org/docs/api/#navigatetourl) for SPA paths
10
+ *
11
+ * Example usage:
12
+ * ```js
13
+ * const config = useConfig();
14
+ * const submitHandler = () => {
15
+ * navigate({ to: config.links.submitSuccess });
16
+ * };
17
+ * ```
18
+ *
19
+ * @param to The target path or URL. Supports templating with 'openmrsBase', 'openmrsSpaBase',
20
+ * and any additional template parameters defined in `templateParams`.
21
+ * For example, `${openmrsSpaBase}/home` will resolve to `/openmrs/spa/home`
22
+ * for implementations using the standard OpenMRS and SPA base paths.
23
+ * If `templateParams` contains `{ foo: "bar" }`, then the URL `${openmrsBase}/${foo}`
24
+ * will become `/openmrs/bar`.
25
+ */
26
+ export declare function navigate({ to, templateParams }: NavigateOptions): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export * from "./types";
2
+ export * from "./navigation/interpolate-string";
3
+ export * from "./navigation/navigate";
4
+ export * from "./validators/validator";
5
+ export * from "./validators/validators";
6
+ export { defineConfigSchema, defineExtensionConfigSchema, provide, getConfig, } from "./module-config/module-config";
7
+ export { getConfigStore, ConfigStore } from "./module-config/state";
@@ -0,0 +1,49 @@
1
+ export declare enum Type {
2
+ Array = "Array",
3
+ Boolean = "Boolean",
4
+ ConceptUuid = "ConceptUuid",
5
+ Number = "Number",
6
+ Object = "Object",
7
+ String = "String",
8
+ UUID = "UUID",
9
+ PersonAttributeTypeUuid = "PersonAttributeTypeUuid",
10
+ PatientIdentifierTypeUuid = "PatientIdentifierTypeUuid"
11
+ }
12
+ export interface ConfigSchema {
13
+ [key: string]: ConfigSchema | ConfigValue;
14
+ _type?: Type;
15
+ _validators?: Array<Validator>;
16
+ _elements?: ConfigSchema;
17
+ }
18
+ export interface Config extends Object {
19
+ [moduleName: string]: {
20
+ [key: string]: any;
21
+ };
22
+ }
23
+ export interface ConfigObject extends Object {
24
+ [key: string]: any;
25
+ }
26
+ export declare type ConfigValue = string | number | boolean | void | Array<any> | object;
27
+ export interface ExtensionSlotConfig {
28
+ add?: Array<string>;
29
+ remove?: Array<string>;
30
+ order?: Array<string>;
31
+ configure?: ExtensionSlotConfigureValueObject;
32
+ }
33
+ export interface ExtensionSlotConfigureValueObject {
34
+ [key: string]: object;
35
+ }
36
+ export interface ExtensionSlotConfigObject {
37
+ /** Additional extension IDs to assign to this slot, in addition to those `attach`ed in code. */
38
+ add?: Array<string>;
39
+ /** Extension IDs which were `attach`ed to the slot but which should not be assigned. */
40
+ remove?: Array<string>;
41
+ /** Overrides the default ordering of extensions. */
42
+ order?: Array<string>;
43
+ }
44
+ export declare type ProvidedConfig = {
45
+ source: string;
46
+ config: Config;
47
+ };
48
+ export declare type ValidatorFunction = (value: any) => boolean;
49
+ export declare type Validator = (value: any) => void | string;
@@ -0,0 +1,6 @@
1
+ export declare const isArray: import("..").Validator;
2
+ export declare const isBoolean: import("..").Validator;
3
+ export declare const isNumber: import("..").Validator;
4
+ export declare const isString: import("..").Validator;
5
+ export declare const isObject: import("..").Validator;
6
+ export declare const isUuid: import("..").Validator;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ /** @module @category Config Validation */
2
+ import { Validator, ValidatorFunction } from "../types";
3
+ /**
4
+ * Constructs a custom validator.
5
+ *
6
+ * ### Example
7
+ *
8
+ * ```typescript
9
+ * {
10
+ * foo: {
11
+ * _default: 0,
12
+ * _validators: [
13
+ * validator(val => val >= 0, "Must not be negative.")
14
+ * ]
15
+ * }
16
+ * }
17
+ * ```
18
+ * @param validationFunction Takes the configured value as input. Returns true
19
+ * if it is valid, false otherwise.
20
+ * @param message A string message that explains why the value is invalid. Can
21
+ * also be a function that takes the value as input and returns a string.
22
+ * @returns A validator ready for use in a config schema
23
+ */
24
+ export declare function validator(validationFunction: ValidatorFunction, message: string | ((value: any) => string)): Validator;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Verifies that the value is between the provided minimum and maximum
3
+ *
4
+ * @param min Minimum acceptable value
5
+ * @param max Maximum acceptable value
6
+ */
7
+ export declare const inRange: (min: number, max: number) => import("..").Validator;
8
+ /**
9
+ * Verifies that a string contains only the default URL template
10
+ * parameters, plus any specified in `allowedTemplateParameters`.
11
+ *
12
+ * @param allowedTemplateParameters To be added to `openmrsBase` and `openmrsSpaBase`
13
+ * @category Navigation
14
+ */
15
+ export declare const isUrlWithTemplateParameters: (allowedTemplateParameters: string[]) => import("..").Validator;
16
+ /**
17
+ * Verifies that a string contains only the default URL template parameters.
18
+ *
19
+ * @category Navigation
20
+ */
21
+ export declare const isUrl: import("..").Validator;
22
+ /**
23
+ * Verifies that the value is one of the allowed options.
24
+ * @param allowedValues The list of allowable values
25
+ */
26
+ export declare const oneOf: (allowedValues: Array<any>) => import("..").Validator;
27
+ export declare const validators: {
28
+ inRange: (min: number, max: number) => import("..").Validator;
29
+ isUrl: import("..").Validator;
30
+ isUrlWithTemplateParameters: (allowedTemplateParameters: string[]) => import("..").Validator;
31
+ oneOf: (allowedValues: Array<any>) => import("..").Validator;
32
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-config",
3
- "version": "4.0.0-pre.0",
3
+ "version": "4.0.0-pre.1",
4
4
  "license": "MPL-2.0",
5
5
  "description": "A configuration library for the OpenMRS Single-Spa framework.",
6
6
  "browser": "dist/openmrs-esm-module-config.js",
@@ -45,12 +45,12 @@
45
45
  "systemjs": "6.x"
46
46
  },
47
47
  "devDependencies": {
48
- "@openmrs/esm-globals": "^4.0.0-pre.0",
49
- "@openmrs/esm-state": "^4.0.0-pre.0",
48
+ "@openmrs/esm-globals": "^4.0.0-pre.1",
49
+ "@openmrs/esm-state": "^4.0.0-pre.1",
50
50
  "@types/ramda": "^0.26.44",
51
51
  "@types/systemjs": "^6.1.0",
52
52
  "babel-plugin-ramda": "^2.0.0",
53
53
  "single-spa": "^5.9.2"
54
54
  },
55
- "gitHead": "254a7226212aac82df4434639b2584e24ac240e4"
55
+ "gitHead": "9f58b801f07526083a9edc1cc5a1e58660d71eb0"
56
56
  }
@@ -12,7 +12,6 @@ import {
12
12
  ConfigInternalStore,
13
13
  configInternalStore,
14
14
  getExtensionConfig,
15
- getExtensionsConfigStore,
16
15
  ImplementerToolsConfigStore,
17
16
  implementerToolsConfigStore,
18
17
  temporaryConfigStore,
@@ -101,6 +100,33 @@ describe("defineConfigSchema", () => {
101
100
  expect(console.error).not.toHaveBeenCalled();
102
101
  });
103
102
 
103
+ it("runs top-level validators on filled-in config", () => {
104
+ const schema = {
105
+ foo: {
106
+ _type: Type.String,
107
+ _default: "bar-value",
108
+ },
109
+ fooStart: {
110
+ _type: Type.String,
111
+ _default: "bar",
112
+ },
113
+ _validators: [
114
+ validator(
115
+ (v) => v.foo.startsWith(v.fooStart),
116
+ (v) =>
117
+ `The value of \`foo\` must start with the value of \`fooStart\` (which is ${v.fooStart}).`
118
+ ),
119
+ ],
120
+ };
121
+ Config.defineConfigSchema("foo-module", schema);
122
+ Config.provide({ "foo-module": { foo: "different" } });
123
+ expect(console.error).toHaveBeenCalledWith(
124
+ expect.stringMatching(
125
+ /The value of `foo` must start with the value of `fooStart`.*bar/
126
+ )
127
+ );
128
+ });
129
+
104
130
  it("logs an error if a non-function validator is provided", () => {
105
131
  const schema = {
106
132
  bar: { _default: [], _validators: [false] },
@@ -264,7 +290,7 @@ describe("getConfig", () => {
264
290
  await Config.getConfig("foo-module");
265
291
  expect(console.error).toHaveBeenCalledWith(
266
292
  expect.stringMatching(
267
- /value.*{\"a\":{\"b\":5}\}.*foo-module.bar.*c must equal a\.b \+ diff/
293
+ /Invalid configuration for foo-module.bar.*c must equal a\.b \+ diff/
268
294
  )
269
295
  );
270
296
 
@@ -360,6 +386,19 @@ describe("getConfig", () => {
360
386
  );
361
387
  });
362
388
 
389
+ it("tolerates defaults that don't pass validation", async () => {
390
+ Config.defineConfigSchema("foo-module", {
391
+ foo: {
392
+ _default: null,
393
+ _validators: [
394
+ validator((val) => val.startsWith("thi"), "must start with 'thi'"),
395
+ ],
396
+ },
397
+ });
398
+ await Config.getConfig("foo-module");
399
+ expect(console.error).not.toHaveBeenCalled();
400
+ });
401
+
363
402
  it("validators pass", async () => {
364
403
  Config.defineConfigSchema("foo-module", {
365
404
  foo: {
@@ -505,6 +544,27 @@ describe("getConfig", () => {
505
544
  expect(console.error).not.toHaveBeenCalled();
506
545
  });
507
546
 
547
+ it("supports array of string elements", async () => {
548
+ Config.defineConfigSchema("foo-module", {
549
+ foo: {
550
+ _type: Type.Array,
551
+ _default: ["bar"],
552
+ _elements: {
553
+ _type: Type.String,
554
+ },
555
+ },
556
+ });
557
+ const testConfig = {
558
+ "foo-module": {
559
+ foo: ["bar", "baz", "qux"],
560
+ },
561
+ };
562
+ Config.provide(testConfig);
563
+ const config = await Config.getConfig("foo-module");
564
+ expect(config.foo).toStrictEqual(["bar", "baz", "qux"]);
565
+ expect(console.error).not.toHaveBeenCalled();
566
+ });
567
+
508
568
  it("supports validation of array elements", async () => {
509
569
  Config.defineConfigSchema("foo-module", {
510
570
  foo: {
@@ -638,9 +698,7 @@ describe("getConfig", () => {
638
698
  Config.provide(badConfig);
639
699
  await Config.getConfig("foo-module");
640
700
  expect(console.error).toHaveBeenCalledWith(
641
- expect.stringMatching(
642
- /value.*{\"a\":{\"b\":1},\"c\":3}.*foo-module.bar\[1\].*c must equal a\.b \+ 1/
643
- )
701
+ expect.stringMatching(/foo-module.bar\[1\].*c must equal a\.b \+ 1/i)
644
702
  );
645
703
 
646
704
  await resetAll();
@@ -280,8 +280,9 @@ export function processConfig(
280
280
  keyPathContext: string,
281
281
  devDefaultsAreOn: boolean = false
282
282
  ) {
283
- validateConfig(schema, providedConfig, keyPathContext);
283
+ validateStructure(schema, providedConfig, keyPathContext);
284
284
  const config = setDefaults(schema, providedConfig, devDefaultsAreOn);
285
+ runAllValidatorsInConfigTree(schema, config, keyPathContext);
285
286
  return config;
286
287
  }
287
288
 
@@ -326,12 +327,13 @@ function computeExtensionConfig(
326
327
  // TODO: validate that a schema exists for the module
327
328
  const schema =
328
329
  extensionConfigSchema ?? configState.schemas[extensionModuleName];
329
- validateConfig(schema, combinedConfig, nameOfSchemaSource);
330
+ validateStructure(schema, combinedConfig, nameOfSchemaSource);
330
331
  const config = setDefaults(
331
332
  schema,
332
333
  combinedConfig,
333
334
  configState.devDefaultsAreOn
334
335
  );
336
+ runAllValidatorsInConfigTree(schema, config, nameOfSchemaSource);
335
337
  delete config.extensionSlots;
336
338
  return config;
337
339
  }
@@ -571,8 +573,9 @@ function getConfigForModule(
571
573
  moduleName,
572
574
  getProvidedConfigs(configState, tempConfigState)
573
575
  );
574
- validateConfig(schema, inputConfig, moduleName);
576
+ validateStructure(schema, inputConfig, moduleName);
575
577
  const config = setDefaults(schema, inputConfig, configState.devDefaultsAreOn);
578
+ runAllValidatorsInConfigTree(schema, config, moduleName);
576
579
  delete config.extensionSlots;
577
580
  return config;
578
581
  }
@@ -593,21 +596,25 @@ function mergeConfigs(configs: Array<Config>) {
593
596
  return mergeDeepAll({}, configs) as Config;
594
597
  }
595
598
 
596
- // Recursively check the provided config tree to make sure that all
597
- // of the provided properties exist in the schema. Run validators
598
- // where present in the schema.
599
- const validateConfig = (
599
+ /**
600
+ * Recursively check the provided config tree to make sure that all
601
+ * of the provided properties exist in the schema, and that types are
602
+ * correct. Does not run validators yet, since those will be run on
603
+ * the config with the defaults filled in.
604
+ */
605
+ function validateStructure(
600
606
  schema: ConfigSchema,
601
607
  config: ConfigObject,
602
608
  keyPath = ""
603
- ) => {
609
+ ) {
610
+ // validate each constituent element
604
611
  for (const key of Object.keys(config)) {
605
612
  const value = config[key];
606
613
  const thisKeyPath = keyPath + "." + key;
607
614
  const schemaPart = schema[key] as ConfigSchema;
608
615
 
609
616
  if (!schema.hasOwnProperty(key)) {
610
- if (key !== "extensionSlots") {
617
+ if (!(key === "extensionSlots" && keyPath !== "")) {
611
618
  console.error(
612
619
  `Unknown config key '${thisKeyPath}' provided. Ignoring.`
613
620
  );
@@ -616,27 +623,34 @@ const validateConfig = (
616
623
  continue;
617
624
  }
618
625
 
619
- checkType(thisKeyPath, schemaPart._type, value);
620
- runValidators(thisKeyPath, schemaPart._validators, value);
621
-
622
- if (isOrdinaryObject(value)) {
623
- // structurally validate only if there's elements specified
624
- // or there's a `_default` value, which indicates a freeform object
625
- if (schemaPart._type === Type.Object) {
626
- validateDictionary(schemaPart, value, thisKeyPath);
627
- } else if (!schemaPart.hasOwnProperty("_default")) {
628
- // recurse to validate nested object structure
629
- validateConfig(schemaPart, value, thisKeyPath);
630
- }
631
- } else {
632
- if (schemaPart._type === Type.Array) {
633
- validateArray(schemaPart, value, thisKeyPath);
634
- }
626
+ validateBranchStructure(schemaPart, value, thisKeyPath);
627
+ }
628
+ }
629
+
630
+ function validateBranchStructure(
631
+ schemaPart: ConfigSchema,
632
+ value: any,
633
+ keyPath: string
634
+ ) {
635
+ checkType(keyPath, schemaPart._type, value);
636
+
637
+ if (isOrdinaryObject(value)) {
638
+ // structurally validate only if there's elements specified
639
+ // or there's no `_default` value (which would indicate a freeform object)
640
+ if (schemaPart._type === Type.Object) {
641
+ validateDictionaryStructure(schemaPart, value, keyPath);
642
+ } else if (!schemaPart.hasOwnProperty("_default")) {
643
+ // recurse to validate nested object structure
644
+ validateStructure(schemaPart, value, keyPath);
645
+ }
646
+ } else {
647
+ if (schemaPart._type === Type.Array) {
648
+ validateArrayStructure(schemaPart, value, keyPath);
635
649
  }
636
650
  }
637
- };
651
+ }
638
652
 
639
- function validateDictionary(
653
+ function validateDictionaryStructure(
640
654
  dictionarySchema: ConfigSchema,
641
655
  config: ConfigObject,
642
656
  keyPath: string
@@ -644,12 +658,12 @@ function validateDictionary(
644
658
  if (dictionarySchema._elements) {
645
659
  for (const key of Object.keys(config)) {
646
660
  const value = config[key];
647
- validateConfig(dictionarySchema._elements, value, `${keyPath}.${key}`);
661
+ validateStructure(dictionarySchema._elements, value, `${keyPath}.${key}`);
648
662
  }
649
663
  }
650
664
  }
651
665
 
652
- function validateArray(
666
+ function validateArrayStructure(
653
667
  arraySchema: ConfigSchema,
654
668
  value: ConfigObject,
655
669
  keyPath: string
@@ -657,17 +671,55 @@ function validateArray(
657
671
  // if there is an array element object schema, verify that elements match it
658
672
  if (hasObjectSchema(arraySchema._elements)) {
659
673
  for (let i = 0; i < value.length; i++) {
660
- validateConfig(arraySchema._elements, value[i], `${keyPath}[${i}]`);
674
+ validateBranchStructure(
675
+ arraySchema._elements,
676
+ value[i],
677
+ `${keyPath}[${i}]`
678
+ );
661
679
  }
662
680
  }
663
681
 
664
682
  for (let i = 0; i < value.length; i++) {
665
683
  checkType(`${keyPath}[${i}]`, arraySchema._elements?._type, value[i]);
666
- runValidators(
667
- `${keyPath}[${i}]`,
668
- arraySchema._elements?._validators,
669
- value[i]
670
- );
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Run all the validators in the config tree. This should be run
689
+ * on the config object after it has been filled in with all the defaults, since
690
+ * higher-level validators may refer to default values.
691
+ */
692
+ function runAllValidatorsInConfigTree(
693
+ schema: ConfigSchema,
694
+ config: ConfigObject,
695
+ keyPath = ""
696
+ ) {
697
+ // If `!schema`, there should have been a structural validation error printed already.
698
+ if (schema) {
699
+ if (config !== schema._default) {
700
+ runValidators(keyPath, schema._validators, config);
701
+ }
702
+
703
+ if (isOrdinaryObject(config)) {
704
+ for (const key of Object.keys(config)) {
705
+ const value = config[key];
706
+ const thisKeyPath = keyPath + "." + key;
707
+ const schemaPart = schema[key] as ConfigSchema;
708
+ if (schema._type === Type.Object && schema._elements) {
709
+ runAllValidatorsInConfigTree(schema._elements, value, thisKeyPath);
710
+ } else {
711
+ runAllValidatorsInConfigTree(schemaPart, value, thisKeyPath);
712
+ }
713
+ }
714
+ } else if (Array.isArray(config) && schema._elements) {
715
+ for (let i = 0; i < config.length; i++) {
716
+ runAllValidatorsInConfigTree(
717
+ schema._elements,
718
+ config[i],
719
+ `${keyPath}[${i}]`
720
+ );
721
+ }
722
+ }
671
723
  }
672
724
  }
673
725
 
@@ -699,15 +751,21 @@ function runValidators(
699
751
  const validatorResult = validator(value);
700
752
 
701
753
  if (typeof validatorResult === "string") {
702
- const valueString =
703
- typeof value === "object" ? JSON.stringify(value) : value;
704
- console.error(
705
- `Invalid configuration value ${valueString} for ${keyPath}: ${validatorResult}`
706
- );
754
+ if (typeof value === "object") {
755
+ console.error(
756
+ `Invalid configuration for ${keyPath}: ${validatorResult}`
757
+ );
758
+ } else {
759
+ console.error(
760
+ `Invalid configuration value ${value} for ${keyPath}: ${validatorResult}`
761
+ );
762
+ }
707
763
  }
708
764
  }
709
765
  } catch (e) {
710
- console.error(`Skipping invalid validator at "${keyPath}".`);
766
+ console.error(
767
+ `Skipping invalid validator at "${keyPath}". Encountered error\n\t${e}`
768
+ );
711
769
  }
712
770
  }
713
771
  }
@@ -748,7 +806,7 @@ const setDefaults = (
748
806
  const elements = schemaPart._elements;
749
807
 
750
808
  if (configPart && hasObjectSchema(elements)) {
751
- if (schemaPart._type === Type.Array) {
809
+ if (schemaPart._type === Type.Array && Array.isArray(configPart)) {
752
810
  const configWithDefaults = configPart.map((conf: Config) =>
753
811
  setDefaults(elements, conf, devDefaultsAreOn)
754
812
  );