diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index f7804d5d49..f23fad8d3b 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -118,6 +118,8 @@ describe('ConfigReader', () => { expect(config.getOptionalString('X-x2')).toBeUndefined(); expect(config.getOptionalString('x0_x0')).toBeUndefined(); expect(config.getOptionalString('x_x-x_x')).toBeUndefined(); + + expect(new ConfigReader(undefined).getOptionalString('x')).toBeUndefined(); }); it('should throw on invalid keys', () => { @@ -138,6 +140,10 @@ describe('ConfigReader', () => { expect(() => config.getString('a.a.a.a.')).toThrow(/^Invalid config key/); expect(() => config.getString('a._')).toThrow(/^Invalid config key/); expect(() => config.getString('a.-.a')).toThrow(/^Invalid config key/); + + expect(() => new ConfigReader(undefined).getString('.')).toThrow( + /^Invalid config key/, + ); }); it('should read valid values', () => { @@ -225,6 +231,9 @@ describe('ConfigReader with fallback', () => { 'a', 'b', ]); + expect(() => config.getConfig('merged').getStringArray('x')).toThrow( + 'Invalid type in config for key merged.x, got string, wanted string-array', + ); // Config arrays aren't merged either expect(config.getConfigArray('merged.configs').length).toBe(1); @@ -232,7 +241,7 @@ describe('ConfigReader with fallback', () => { expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a'); expect(() => config.getConfigArray('merged.configs')[0].getString('missing'), - ).toThrow("Missing required config value at 'missing'"); + ).toThrow("Missing required config value at 'merged.configs[0].missing'"); expect( config.getConfigArray('merged.configs')[0].getOptionalString('b'), ).toBeUndefined(); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index ff7c8be6e5..e3c349768b 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -39,12 +39,20 @@ function typeOf(value: JsonValue | undefined): string { return type; } -export class ConfigReader implements Config { - private static readonly nullReader = new ConfigReader({}); +// Separate out a couple of common error messages to reduce bundle size. +const errors = { + type(key: string, typeName: string, expected: string) { + return `Invalid type in config for key ${key}, got ${typeName}, wanted ${expected}`; + }, + missing(key: string) { + return `Missing required config value at '${key}'`; + }, +}; +export class ConfigReader implements Config { static fromConfigs(configs: AppConfig[]): ConfigReader { if (configs.length === 0) { - return new ConfigReader({}); + return new ConfigReader(undefined); } // Merge together all configs info a single config with recursive fallback @@ -55,24 +63,25 @@ export class ConfigReader implements Config { } constructor( - private readonly data: JsonObject, + private readonly data: JsonObject | undefined, private readonly fallback?: ConfigReader, + private readonly prefix: string = '', ) {} getConfig(key: string): ConfigReader { const value = this.readValue(key); const fallbackConfig = this.fallback?.getConfig(key); + const prefix = this.fullKey(key); + if (isObject(value)) { - return new ConfigReader(value, fallbackConfig); + return new ConfigReader(value, fallbackConfig, prefix); } if (value !== undefined) { throw new TypeError( - `Invalid type in config for key ${key}, got ${typeOf( - value, - )}, wanted object`, + errors.type(this.fullKey(key), typeOf(value), 'object'), ); } - return fallbackConfig ?? ConfigReader.nullReader; + return fallbackConfig ?? new ConfigReader(undefined, undefined, prefix); } getConfigArray(key: string): ConfigReader[] { @@ -89,13 +98,16 @@ export class ConfigReader implements Config { return true; }); - return (configs ?? []).map(obj => new ConfigReader(obj)); + return (configs ?? []).map( + (obj, index) => + new ConfigReader(obj, undefined, this.fullKey(`${key}[${index}]`)), + ); } getNumber(key: string): number { const value = this.getOptionalNumber(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -110,7 +122,7 @@ export class ConfigReader implements Config { getBoolean(key: string): boolean { const value = this.getOptionalBoolean(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -125,7 +137,7 @@ export class ConfigReader implements Config { getString(key: string): string { const value = this.getOptionalString(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -141,7 +153,7 @@ export class ConfigReader implements Config { getStringArray(key: string): string[] { const value = this.getOptionalStringArray(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -160,6 +172,10 @@ export class ConfigReader implements Config { }); } + private fullKey(key: string): string { + return `${this.prefix}${this.prefix ? '.' : ''}${key}`; + } + private readConfigValue( key: string, validate: ( @@ -179,9 +195,8 @@ export class ConfigReader implements Config { value: theValue = value, expected, } = result; - const typeName = typeOf(theValue); throw new TypeError( - `Invalid type in config for key ${keyName}, got ${typeName}, wanted ${expected}`, + errors.type(this.fullKey(keyName), typeOf(theValue), expected), ); } } @@ -191,12 +206,18 @@ export class ConfigReader implements Config { private readValue(key: string): JsonValue | undefined { const parts = key.split('.'); - - let value: JsonValue | undefined = this.data; for (const part of parts) { if (!CONFIG_KEY_PART_PATTERN.test(part)) { throw new TypeError(`Invalid config key '${key}'`); } + } + + if (this.data === undefined) { + return undefined; + } + + let value: JsonValue | undefined = this.data; + for (const part of parts) { if (isObject(value)) { value = value[part]; } else {