Skip to content

Commit 06fe0f4

Browse files
authored
init rework and config overrides (#101)
* consistent config merge ordering auto files runtime * override behavior 1 * override behavior 2 * override behavior 3 * work on tests * fallback values of null make initialize throw now * init throws for partial configs * init throws for double invocation * double invocation should be avoidable via canInitialize * shorter mock little polish * test for override * test for override 2 * test for validation configuration override behavior
1 parent 96ff484 commit 06fe0f4

File tree

6 files changed

+351
-72
lines changed

6 files changed

+351
-72
lines changed

src/feature-toggles.js

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const CONFIG_INFO_KEY = {
7171
[CONFIG_KEY.TYPE]: true,
7272
[CONFIG_KEY.ACTIVE]: true,
7373
[CONFIG_KEY.SOURCE]: true,
74+
[CONFIG_KEY.SOURCE_FILEPATH]: true,
7475
[CONFIG_KEY.APP_URL]: true,
7576
[CONFIG_KEY.APP_URL_ACTIVE]: true,
7677
[CONFIG_KEY.VALIDATIONS]: true,
@@ -237,6 +238,9 @@ class FeatureToggles {
237238
for (const [featureKey, value] of entries) {
238239
if (this.__config[featureKey]) {
239240
switch (mergeConflictBehavior) {
241+
case CONFIG_MERGE_CONFLICT.OVERRIDE: {
242+
break;
243+
}
240244
case CONFIG_MERGE_CONFLICT.PRESERVE: {
241245
continue;
242246
}
@@ -274,26 +278,51 @@ class FeatureToggles {
274278
...(sourceFilepath && { sourceFilepath }),
275279
},
276280
},
277-
"feature configuration is not an object"
281+
"configuration is not an object"
278282
);
279283
}
280284

281285
const { type, active, appUrl, fallbackValue, validations } = value;
282286

283-
this.__featureKeys.push(featureKey);
287+
if ([undefined, null].includes(fallbackValue)) {
288+
throw new VError(
289+
{
290+
name: VERROR_CLUSTER_NAME,
291+
info: {
292+
featureKey,
293+
source,
294+
...(sourceFilepath && { sourceFilepath }),
295+
},
296+
},
297+
"configuration has no or invalid fallback value"
298+
);
299+
}
300+
301+
if (!FEATURE_VALID_TYPES.includes(type)) {
302+
throw new VError(
303+
{
304+
name: VERROR_CLUSTER_NAME,
305+
info: {
306+
featureKey,
307+
source,
308+
...(sourceFilepath && { sourceFilepath }),
309+
},
310+
},
311+
"configuration has no or invalid type"
312+
);
313+
}
314+
284315
this.__fallbackValues[featureKey] = fallbackValue;
285316
this.__config[featureKey] = {};
286317

318+
this.__config[featureKey][CONFIG_KEY.TYPE] = type;
319+
287320
this.__config[featureKey][CONFIG_KEY.SOURCE] = source;
288321

289322
if (sourceFilepath) {
290323
this.__config[featureKey][CONFIG_KEY.SOURCE_FILEPATH] = sourceFilepath;
291324
}
292325

293-
if (type) {
294-
this.__config[featureKey][CONFIG_KEY.TYPE] = type;
295-
}
296-
297326
if (active === false) {
298327
this.__config[featureKey][CONFIG_KEY.ACTIVE] = false;
299328
}
@@ -308,7 +337,6 @@ class FeatureToggles {
308337

309338
if (validations) {
310339
this.__config[featureKey][CONFIG_KEY.VALIDATIONS] = validations;
311-
this._processValidations(featureKey, validations, sourceFilepath);
312340
}
313341
}
314342

@@ -318,25 +346,35 @@ class FeatureToggles {
318346
/**
319347
* Populate this.__config.
320348
*/
321-
_processConfig({ configRuntime, configFromFilesEntries, configAuto } = {}) {
322-
const configRuntimeCount = this._processConfigSource(
323-
CONFIG_SOURCE.RUNTIME,
324-
CONFIG_MERGE_CONFLICT.THROW,
325-
configRuntime
326-
);
349+
_processConfig({ configAuto, configFromFilesEntries, configRuntime } = {}) {
350+
const configAutoCount = this._processConfigSource(CONFIG_SOURCE.AUTO, CONFIG_MERGE_CONFLICT.OVERRIDE, configAuto);
327351
const configFromFileCount = configFromFilesEntries.reduce(
328352
(count, [configFilepath, configFromFile]) =>
329353
count +
330-
this._processConfigSource(CONFIG_SOURCE.FILE, CONFIG_MERGE_CONFLICT.THROW, configFromFile, configFilepath),
354+
this._processConfigSource(CONFIG_SOURCE.FILE, CONFIG_MERGE_CONFLICT.OVERRIDE, configFromFile, configFilepath),
331355
0
332356
);
333-
const configAutoCount = this._processConfigSource(CONFIG_SOURCE.AUTO, CONFIG_MERGE_CONFLICT.PRESERVE, configAuto);
357+
const configRuntimeCount = this._processConfigSource(
358+
CONFIG_SOURCE.RUNTIME,
359+
CONFIG_MERGE_CONFLICT.OVERRIDE,
360+
configRuntime
361+
);
362+
363+
// NOTE: this post-processing is easier to do after the configuration is merged
364+
this.__featureKeys = Object.keys(this.__fallbackValues);
365+
for (const featureKey of this.__featureKeys) {
366+
const validations = this.__config[featureKey][CONFIG_KEY.VALIDATIONS];
367+
if (validations) {
368+
const sourceFilepath = this.__config[featureKey][CONFIG_KEY.SOURCE_FILEPATH];
369+
this._processValidations(featureKey, validations, sourceFilepath);
370+
}
371+
}
334372

335373
this.__isConfigProcessed = true;
336374
return {
375+
[CONFIG_SOURCE.AUTO]: configAutoCount,
337376
[CONFIG_SOURCE.RUNTIME]: configRuntimeCount,
338377
[CONFIG_SOURCE.FILE]: configFromFileCount,
339-
[CONFIG_SOURCE.AUTO]: configAutoCount,
340378
};
341379
}
342380

@@ -804,10 +842,10 @@ class FeatureToggles {
804842
* @param {InitializeOptions} [options]
805843
*/
806844
async _initializeFeatures({
807-
config: configRuntime,
845+
configAuto,
808846
configFile: configFilepath,
809847
configFiles: configFilepaths,
810-
configAuto,
848+
config: configRuntime,
811849
customRedisCredentials,
812850
customRedisClientOptions,
813851
} = {}) {
@@ -841,9 +879,9 @@ class FeatureToggles {
841879
let toggleCounts;
842880
try {
843881
toggleCounts = this._processConfig({
844-
configRuntime,
845-
configFromFilesEntries,
846882
configAuto,
883+
configFromFilesEntries,
884+
configRuntime,
847885
});
848886
} catch (err) {
849887
throw new VError(
@@ -924,17 +962,17 @@ class FeatureToggles {
924962
}
925963

926964
const totalCount =
927-
toggleCounts[CONFIG_SOURCE.RUNTIME] + toggleCounts[CONFIG_SOURCE.FILE] + toggleCounts[CONFIG_SOURCE.AUTO];
965+
toggleCounts[CONFIG_SOURCE.AUTO] + toggleCounts[CONFIG_SOURCE.FILE] + toggleCounts[CONFIG_SOURCE.RUNTIME];
928966
logger.info(
929967
[
930968
"finished initialization",
931969
...(this.__uniqueName ? [`of "${this.__uniqueName}"`] : []),
932970
util.format(
933-
"with %i feature toggles (%i runtime, %i file, %i auto)",
971+
"with %i feature toggles (%i auto, %i file, %i runtime)",
934972
totalCount,
935-
toggleCounts[CONFIG_SOURCE.RUNTIME],
973+
toggleCounts[CONFIG_SOURCE.AUTO],
936974
toggleCounts[CONFIG_SOURCE.FILE],
937-
toggleCounts[CONFIG_SOURCE.AUTO]
975+
toggleCounts[CONFIG_SOURCE.RUNTIME]
938976
),
939977
`using ${redisIntegrationMode}`,
940978
].join(" ")
@@ -964,12 +1002,17 @@ class FeatureToggles {
9641002
* @param {InitializeOptions} [options]
9651003
*/
9661004
async initializeFeatures(options) {
967-
if (!this.__initializePromise) {
968-
this.__initializePromise = this._initializeFeatures(options);
1005+
if (this.__initializePromise) {
1006+
throw new VError({ name: VERROR_CLUSTER_NAME }, "already initialized");
9691007
}
1008+
this.__initializePromise = this._initializeFeatures(options);
9701009
return await this.__initializePromise;
9711010
}
9721011

1012+
get canInitialize() {
1013+
return !this.__initializePromise;
1014+
}
1015+
9731016
// ========================================
9741017
// END OF INITIALIZE SECTION
9751018
// ========================================

test/__snapshots__/feature-toggles.test.js.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ exports[`feature toggles test basic apis initializeFeatureToggles warns for inva
255255
"test/feature_b": {
256256
"SOURCE": "RUNTIME",
257257
"TYPE": "string",
258+
"VALIDATIONS": [
259+
{
260+
"regex": ".+",
261+
},
262+
],
263+
"VALIDATIONS_REGEX": [
264+
/\\.\\+/,
265+
],
258266
},
259267
"test/feature_c": {
260268
"SOURCE": "RUNTIME",

test/feature-toggles.test.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,7 @@ describe("feature toggles test", () => {
6666

6767
describe("enums", () => {
6868
test("config info consistency", () => {
69-
const internalKeys = [
70-
CONFIG_KEY.VALIDATIONS_SCOPES_MAP,
71-
CONFIG_KEY.VALIDATIONS_REGEX,
72-
CONFIG_KEY.SOURCE_FILEPATH,
73-
];
69+
const internalKeys = [CONFIG_KEY.VALIDATIONS_SCOPES_MAP, CONFIG_KEY.VALIDATIONS_REGEX];
7470
const configKeysCheck = [].concat(Object.keys(CONFIG_INFO_KEY), internalKeys).sort();
7571
const configKeys = Object.values(CONFIG_KEY).sort();
7672

@@ -325,8 +321,9 @@ describe("feature toggles test", () => {
325321
type: "boolean",
326322
},
327323
[FEATURE.B]: {
328-
fallbackValue: null, // null not allowed
324+
fallbackValue: "", // empty string not valid
329325
type: "string",
326+
validations: [{ regex: ".+" }],
330327
},
331328
[FEATURE.C]: {
332329
fallbackValue: "1", // type mismatch
@@ -361,7 +358,7 @@ describe("feature toggles test", () => {
361358
expect(outputFromErrorLogger(loggerSpy.warning.mock.calls)).toMatchInlineSnapshot(`
362359
"FeatureTogglesError: found invalid fallback values during initialization
363360
{
364-
validationErrors: '[{"featureKey":"test/feature_b","errorMessage":"value null is not allowed"},{"featureKey":"test/feature_c","errorMessage":"value \\\\"{0}\\\\" has invalid type {1}, must be {2}","errorMessageValues":["1","string","number"]}]'
361+
validationErrors: '[{"featureKey":"test/feature_b","errorMessage":"value \\\\"{0}\\\\" does not match validation regular expression {1}","errorMessageValues":["","/.+/"]},{"featureKey":"test/feature_c","errorMessage":"value \\\\"{0}\\\\" has invalid type {1}, must be {2}","errorMessageValues":["1","string","number"]}]'
365362
}"
366363
`);
367364
expect(loggerSpy.error).not.toHaveBeenCalled();

test/integration-local/__snapshots__/feature-toggles.integration.test.js.snap

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,57 @@ exports[`local integration test init init config works for runtime file auto sim
153153
"test/feature_c": {
154154
"config": {
155155
"SOURCE": "FILE",
156+
"SOURCE_FILEPATH": "toggles.json",
156157
"TYPE": "string",
157158
},
158159
"fallbackValue": "fallbackFileC",
159160
},
160161
"test/feature_d": {
161162
"config": {
162163
"SOURCE": "FILE",
164+
"SOURCE_FILEPATH": "toggles.json",
165+
"TYPE": "string",
166+
},
167+
"fallbackValue": "fallbackFileD",
168+
},
169+
"test/feature_e": {
170+
"config": {
171+
"SOURCE": "AUTO",
172+
"TYPE": "string",
173+
},
174+
"fallbackValue": "fallbackAutoE",
175+
},
176+
}
177+
`;
178+
179+
exports[`local integration test init init config works for runtime file auto simultaneously with overrides 1`] = `
180+
{
181+
"test/feature_a": {
182+
"config": {
183+
"SOURCE": "RUNTIME",
184+
"TYPE": "string",
185+
},
186+
"fallbackValue": "fallbackRuntimeA",
187+
},
188+
"test/feature_b": {
189+
"config": {
190+
"SOURCE": "RUNTIME",
191+
"TYPE": "string",
192+
},
193+
"fallbackValue": "fallbackRuntimeB",
194+
},
195+
"test/feature_c": {
196+
"config": {
197+
"SOURCE": "FILE",
198+
"SOURCE_FILEPATH": "toggles-2.json",
199+
"TYPE": "string",
200+
},
201+
"fallbackValue": "C-from-File2",
202+
},
203+
"test/feature_d": {
204+
"config": {
205+
"SOURCE": "FILE",
206+
"SOURCE_FILEPATH": "toggles-1.json",
163207
"TYPE": "string",
164208
},
165209
"fallbackValue": "fallbackFileD",

0 commit comments

Comments
 (0)