From 02b74c376affbbf7e42b7202b1a3cadcb8b79725 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jun 2020 11:58:53 +0200 Subject: [PATCH] packages/config: added get and getOptional --- packages/config/package.json | 3 + packages/config/src/reader.test.ts | 172 +++++++++++++++++++++++++++++ packages/config/src/reader.ts | 30 +++++ packages/config/src/types.ts | 3 + 4 files changed, 208 insertions(+) diff --git a/packages/config/package.json b/packages/config/package.json index 8bf4001e62..95a5852ae5 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -29,6 +29,9 @@ "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" }, + "dependencies": { + "lodash": "^4.17.15" + }, "devDependencies": { "@types/jest": "^25.2.2", "@types/node": "^12.0.0" diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 64e4b3ed69..9d06e0b025 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -38,17 +38,26 @@ const DATA = { function expectValidValues(config: ConfigReader) { expect(config.keys()).toEqual(Object.keys(DATA)); + expect(config.get('zero')).toBe(0); expect(config.getNumber('zero')).toBe(0); expect(config.getNumber('one')).toBe(1); + expect(config.getOptional('true')).toBe(true); expect(config.getBoolean('true')).toBe(true); expect(config.getBoolean('false')).toBe(false); expect(config.getString('string')).toBe('string'); + expect(config.get('strings')).toEqual(['string1', 'string2']); expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); expect(config.getConfig('nested').getNumber('one')).toBe(1); + expect(config.get('nested')).toEqual({ + one: 1, + string: 'string', + strings: ['string1', 'string2'], + }); expect(config.getConfig('nested').getString('string')).toBe('string'); expect( config.getOptionalConfig('nested')!.getStringArray('strings'), ).toEqual(['string1', 'string2']); + expect(config.getOptional('missing')).toBe(undefined); expect(config.getOptionalConfig('missing')).toBe(undefined); expect(config.getOptionalConfigArray('missing')).toBe(undefined); expect(config.getNumber('zero')).toBe(0); @@ -360,3 +369,166 @@ describe('ConfigReader with fallback', () => { ).toBeUndefined(); }); }); + +describe('ConfigReader.get()', () => { + const config1 = { + a: { + x: 'x1', + y: ['y11', 'y12', 'y13'], + z: false, + }, + b: { + x: 'x1', + y: ['y11'], + }, + }; + const config2 = { + b: { + x: 'x2', + y: ['y21', 'y22'], + z: 'z2', + }, + c: { + c1: { + c2: 'c2', + }, + }, + }; + const config3 = { + c: { + c1: 'c1', + }, + }; + const configs = [ + { + data: config1, + context: '1', + }, + { + data: config2, + context: '2', + }, + { + data: config3, + context: '3', + }, + ]; + + it('should be able to select sub-configs', () => { + expect(new ConfigReader(config1).get('a')).toEqual(config1.a); + expect(new ConfigReader(config1).get('b')).toEqual(config1.b); + expect(new ConfigReader(config2).get('b')).toEqual(config2.b); + expect(new ConfigReader(config2).get('c')).toEqual(config2.c); + expect(new ConfigReader(config3).get('c')).toEqual(config3.c); + expect(new ConfigReader(config2).get('c.c1')).toEqual(config2.c.c1); + expect(new ConfigReader(config2).getConfig('c').get('c1')).toEqual( + config2.c.c1, + ); + }); + + it('should merge in fallback configs', () => { + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('a')).toEqual( + { + x: 'x1', + y: ['y11', 'y12', 'y13'], + z: false, + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('b')).toEqual( + { + x: 'x1', + y: ['y11'], + z: 'z2', + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('c')).toEqual( + { + c1: { + c2: 'c2', + }, + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('a')).toEqual( + { + x: 'x1', + y: ['y11', 'y12', 'y13'], + z: false, + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('b')).toEqual( + { + x: 'x1', + y: ['y11'], + z: 'z2', + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('c')).toEqual( + { + c1: { + c2: 'c2', + }, + }, + ); + + expect( + ConfigReader.fromConfigs([configs[2], configs[1]]).getOptional('b'), + ).toEqual({ + x: 'x2', + y: ['y21', 'y22'], + z: 'z2', + }); + expect( + ConfigReader.fromConfigs([configs[2], configs[1]]).getOptional('c'), + ).toEqual({ + c1: 'c1', + }); + }); + + it('should not merge non-objects', () => { + const config = ConfigReader.fromConfigs([ + { + data: { + a: ['1', '2'], + c: [], + d: { + x: 'x', + }, + e: ['3'], + f: 'foo', + g: { z: 'z' }, + h: { + a: 'a1', + c: 'c1', + }, + }, + context: '1', + }, + { + data: { + a: ['x', 'y', 'z'], + b: ['1'], + c: ['1'], + d: ['2'], + e: { + y: 'y', + }, + f: { x: 'x' }, + g: 'bar', + h: { + a: 'a2', + b: 'b2', + }, + }, + context: '2', + }, + ]); + expect(config.get('a')).toEqual(['1', '2']); + expect(config.get('b')).toEqual(['1']); + expect(config.get('c')).toEqual([]); + expect(config.get('d')).toEqual({ x: 'x' }); + expect(config.get('e')).toEqual(['3']); + expect(config.get('f')).toEqual('foo'); + expect(config.get('g')).toEqual({ z: 'z' }); + expect(config.get('h')).toEqual({ a: 'a1', b: 'b2', c: 'c1' }); + }); +}); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 4e302a73a4..0669bcb6d8 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -15,6 +15,8 @@ */ import { AppConfig, Config, JsonValue, JsonObject } from './types'; +import cloneDeep from 'lodash/cloneDeep'; +import mergeWith from 'lodash/mergeWith'; // Update the same pattern in config-loader package if this is changed const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; @@ -78,6 +80,34 @@ export class ConfigReader implements Config { return [...new Set([...localKeys, ...fallbackKeys])]; } + get(key: string): JsonValue { + const value = this.getOptional(key); + if (value === undefined) { + throw new Error(errors.missing(this.fullKey(key))); + } + return value; + } + + getOptional(key: string): JsonValue | undefined { + const value = this.readValue(key); + const fallbackValue = this.fallback?.getOptional(key); + + if (value === undefined) { + return fallbackValue; + } else if (fallbackValue === undefined) { + return value; + } + + // Avoid merging arrays and primitive values, since that's how merging works for other + // methods for reading config. + return mergeWith( + {}, + { value: cloneDeep(fallbackValue) }, + { value }, + (into, from) => (!isObject(from) || !isObject(into) ? from : undefined), + ).value; + } + getConfig(key: string): ConfigReader { const value = this.getOptionalConfig(key); if (value === undefined) { diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 180d526b40..aaadcd70dc 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -32,6 +32,9 @@ export type AppConfig = { export type Config = { keys(): string[]; + get(key: string): JsonValue; + getOptional(key: string): JsonValue | undefined; + getConfig(key: string): Config; getOptionalConfig(key: string): Config | undefined;