Skip to content

Commit 55a8dcd

Browse files
authored
Fb/multifile (#89)
* wip 1 * wip 2 * wip 3 * wip 4 * wip 5 * wip 6 * wip 7 * wip 8 * dynamic merge conflict behavior * better merge conflict throw reporting * leaner error reporting during process config * naming * tests to infinity and beyond 1 * tests to infinity and beyond 2 * tests to infinity and beyond 4 * tests to infinity and beyond 5 * require fs without unpacking * one more test for the road * sargent butter fingers in the house * lunter smelled something * some more jest syntax fun to check Verror infos
1 parent 3fbc1a7 commit 55a8dcd

File tree

5 files changed

+416
-160
lines changed

5 files changed

+416
-160
lines changed

src/feature-toggles.js

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
const util = require("util");
1616
const pathlib = require("path");
17-
const { readFile } = require("fs");
17+
const fs = require("fs");
1818
const VError = require("verror");
1919
const yaml = require("yaml");
2020
const redis = require("./redis-adapter");
@@ -49,10 +49,17 @@ const CONFIG_SOURCE = Object.freeze({
4949
AUTO: "AUTO",
5050
});
5151

52+
const CONFIG_MERGE_CONFLICT = Object.freeze({
53+
THROW: "THROW",
54+
PRESERVE: "PRESERVE",
55+
OVERRIDE: "OVERRIDE",
56+
});
57+
5258
const CONFIG_KEY = Object.freeze({
5359
TYPE: "TYPE",
5460
ACTIVE: "ACTIVE",
5561
SOURCE: "SOURCE",
62+
SOURCE_FILEPATH: "SOURCE_FILEPATH",
5663
APP_URL: "APP_URL",
5764
APP_URL_ACTIVE: "APP_URL_ACTIVE",
5865
VALIDATIONS: "VALIDATIONS",
@@ -113,7 +120,7 @@ const SCOPE_PREFERENCE_ORDER_MASKS = [
113120
];
114121

115122
const cfEnv = CfEnv.getInstance();
116-
const readFileAsync = util.promisify(readFile);
123+
const readFileAsync = util.promisify(fs.readFile);
117124
let logger = new Logger(COMPONENT_NAME);
118125

119126
/**
@@ -219,7 +226,7 @@ class FeatureToggles {
219226
}
220227
}
221228

222-
_processConfigSource(source, configFromSource, configFilepath) {
229+
_processConfigSource(source, mergeConflictBehavior, configFromSource, sourceFilepath) {
223230
let count = 0;
224231
if (!isObject(configFromSource)) {
225232
return count;
@@ -229,7 +236,31 @@ class FeatureToggles {
229236
const entries = Object.entries(configFromSource);
230237
for (const [featureKey, value] of entries) {
231238
if (this.__config[featureKey]) {
232-
continue;
239+
switch (mergeConflictBehavior) {
240+
case CONFIG_MERGE_CONFLICT.PRESERVE: {
241+
continue;
242+
}
243+
case CONFIG_MERGE_CONFLICT.THROW: // eslint-disable-current-line no-fallthrough
244+
default: {
245+
const sourceExisting = this.__config[featureKey][CONFIG_KEY.SOURCE];
246+
const sourceConflicting = source;
247+
const sourceFilepathExisting = this.__config[featureKey][CONFIG_KEY.SOURCE_FILEPATH];
248+
const sourceFilepathConflicting = sourceFilepath;
249+
throw new VError(
250+
{
251+
name: VERROR_CLUSTER_NAME,
252+
info: {
253+
featureKey,
254+
sourceExisting,
255+
sourceConflicting,
256+
...(sourceFilepathExisting && { sourceFilepathExisting }),
257+
...(sourceFilepathConflicting && { sourceFilepathConflicting }),
258+
},
259+
},
260+
"feature is configured twice"
261+
);
262+
}
263+
}
233264
}
234265
count++;
235266

@@ -240,6 +271,7 @@ class FeatureToggles {
240271
info: {
241272
featureKey,
242273
source,
274+
...(sourceFilepath && { sourceFilepath }),
243275
},
244276
},
245277
"feature configuration is not an object"
@@ -254,6 +286,10 @@ class FeatureToggles {
254286

255287
this.__config[featureKey][CONFIG_KEY.SOURCE] = source;
256288

289+
if (sourceFilepath) {
290+
this.__config[featureKey][CONFIG_KEY.SOURCE_FILEPATH] = sourceFilepath;
291+
}
292+
257293
if (type) {
258294
this.__config[featureKey][CONFIG_KEY.TYPE] = type;
259295
}
@@ -272,7 +308,7 @@ class FeatureToggles {
272308

273309
if (validations) {
274310
this.__config[featureKey][CONFIG_KEY.VALIDATIONS] = validations;
275-
this._processValidations(featureKey, validations, configFilepath);
311+
this._processValidations(featureKey, validations, sourceFilepath);
276312
}
277313
}
278314

@@ -282,10 +318,19 @@ class FeatureToggles {
282318
/**
283319
* Populate this.__config.
284320
*/
285-
_processConfig([configRuntime, configFromFile, configAuto], configFilepath) {
286-
const configRuntimeCount = this._processConfigSource(CONFIG_SOURCE.RUNTIME, configRuntime, configFilepath);
287-
const configFromFileCount = this._processConfigSource(CONFIG_SOURCE.FILE, configFromFile, configFilepath);
288-
const configAutoCount = this._processConfigSource(CONFIG_SOURCE.AUTO, configAuto, configFilepath);
321+
_processConfig({ configRuntime, configFromFilesEntries, configAuto } = {}) {
322+
const configRuntimeCount = this._processConfigSource(
323+
CONFIG_SOURCE.RUNTIME,
324+
CONFIG_MERGE_CONFLICT.THROW,
325+
configRuntime
326+
);
327+
const configFromFileCount = configFromFilesEntries.reduce(
328+
(count, [configFilepath, configFromFile]) =>
329+
count +
330+
this._processConfigSource(CONFIG_SOURCE.FILE, CONFIG_MERGE_CONFLICT.THROW, configFromFile, configFilepath),
331+
0
332+
);
333+
const configAutoCount = this._processConfigSource(CONFIG_SOURCE.AUTO, CONFIG_MERGE_CONFLICT.PRESERVE, configAuto);
289334

290335
this.__isConfigProcessed = true;
291336
return {
@@ -735,55 +780,74 @@ class FeatureToggles {
735780
name: VERROR_CLUSTER_NAME,
736781
info: { configFilepath },
737782
},
738-
"configFilepath with unsupported extension, allowed extensions are .yaml and .json"
783+
"config filepath with unsupported extension, allowed extensions are .yaml and .json"
739784
);
740785
}
741786

787+
static async _consolidatedConfigFilepaths(configFilepath, configFilepaths) {
788+
let result = [];
789+
if (configFilepath) {
790+
result.push(configFilepath);
791+
}
792+
if (configFilepaths) {
793+
result = result.concat(Object.values(configFilepaths));
794+
}
795+
if (result.length === 0 && (await tryPathReadable(DEFAULT_CONFIG_FILEPATH))) {
796+
result.push(DEFAULT_CONFIG_FILEPATH);
797+
}
798+
return result;
799+
}
800+
742801
/**
743802
* Implementation for {@link initializeFeatures}.
744803
*
745804
* @param {InitializeOptions} [options]
746805
*/
747-
async _initializeFeatures({ config: configRuntime, configFile: configFilepath, configAuto } = {}) {
806+
async _initializeFeatures({
807+
config: configRuntime,
808+
configFile: configFilepath,
809+
configFiles: configFilepaths,
810+
configAuto,
811+
} = {}) {
748812
if (this.__isInitialized) {
749813
return;
750814
}
751815

752-
let configFromFile;
753-
try {
754-
if (!configFilepath && (await tryPathReadable(DEFAULT_CONFIG_FILEPATH))) {
755-
configFilepath = DEFAULT_CONFIG_FILEPATH;
756-
}
757-
if (configFilepath) {
758-
configFromFile = await FeatureToggles.readConfigFromFile(configFilepath);
759-
}
760-
} catch (err) {
761-
throw new VError(
762-
{
763-
name: VERROR_CLUSTER_NAME,
764-
cause: err,
765-
info: {
766-
configFilepath,
767-
},
768-
},
769-
"initialization aborted, could not read config file"
770-
);
771-
}
816+
const consolidatedConfigFilepaths = await FeatureToggles._consolidatedConfigFilepaths(
817+
configFilepath,
818+
configFilepaths
819+
);
820+
const configFromFilesEntries = await Promise.all(
821+
consolidatedConfigFilepaths.map(async (configFilepath) => {
822+
try {
823+
return [configFilepath, await FeatureToggles.readConfigFromFile(configFilepath)];
824+
} catch (err) {
825+
throw new VError(
826+
{
827+
name: VERROR_CLUSTER_NAME,
828+
cause: err,
829+
info: {
830+
configFilepath,
831+
},
832+
},
833+
"initialization aborted, could not read config file"
834+
);
835+
}
836+
})
837+
);
772838

773839
let toggleCounts;
774840
try {
775-
toggleCounts = this._processConfig([configRuntime, configFromFile, configAuto], configFilepath);
841+
toggleCounts = this._processConfig({
842+
configRuntime,
843+
configFromFilesEntries,
844+
configAuto,
845+
});
776846
} catch (err) {
777847
throw new VError(
778848
{
779849
name: VERROR_CLUSTER_NAME,
780850
cause: err,
781-
info: {
782-
...(configAuto && { configAuto: JSON.stringify(configAuto) }),
783-
...(configFromFile && { configFromFile: JSON.stringify(configFromFile) }),
784-
...(configRuntime && { configRuntime: JSON.stringify(configRuntime) }),
785-
configFilepath,
786-
},
787851
},
788852
"initialization aborted, could not process configuration"
789853
);
@@ -1662,6 +1726,7 @@ module.exports = {
16621726
ENV,
16631727
DEFAULT_REDIS_CHANNEL,
16641728
DEFAULT_REDIS_KEY,
1729+
DEFAULT_CONFIG_FILEPATH,
16651730
SCOPE_ROOT_KEY,
16661731
FeatureToggles,
16671732

src/plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ const activate = async () => {
172172
await toggles.initializeFeatures({
173173
config: envFeatureToggles?.config,
174174
configFile: envFeatureToggles?.configFile,
175+
configFiles: envFeatureToggles?.configFiles,
175176
configAuto: ftsAutoConfig,
176177
});
177178

0 commit comments

Comments
 (0)