@splitsoftware/splitio
Version:
144 lines (143 loc) • 6.87 kB
JavaScript
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import { isString, endsWith, find, forOwn, uniq, } from '@splitsoftware/splitio-commons/esm/utils/lang';
import { parseCondition } from '@splitsoftware/splitio-commons/esm/sync/offline/splitsParser/parseCondition';
var logPrefix = 'sync:offline:fetcher: ';
var DEFAULT_FILENAME = '.split';
function configFilesPath(configFilePath) {
if (configFilePath === DEFAULT_FILENAME || !isString(configFilePath)) {
var root = process.env.HOME;
// @TODO env var not documented in help center
if (process.env.SPLIT_CONFIG_ROOT)
root = process.env.SPLIT_CONFIG_ROOT;
if (!root)
throw new Error('Missing root of the feature flags mock file.');
configFilePath = path.join(root, DEFAULT_FILENAME);
}
// Validate the extensions
if (!(endsWith(configFilePath, '.yaml', true) || endsWith(configFilePath, '.yml', true) || endsWith(configFilePath, '.split', true)))
throw new Error("Invalid extension specified for feature flags mock file. Accepted extensions are \".yml\" and \".yaml\". Your specified file is " + configFilePath);
if (!fs.existsSync(configFilePath))
throw new Error("Feature flags mock file not found in " + configFilePath + " - Please review the file location.");
return configFilePath;
}
// This function is not pure nor meant to be. Here we apply modifications to cover
// for behavior that's ensured by the BE.
function arrangeConditions(mocksData) {
// Iterate through each feature flag data
forOwn(mocksData, function (data) {
var conditions = data.conditions;
// On the manager, as feature flag JSONs come with all treatments on the partitions prop,
// we'll add all the treatments to the first condition.
var firstRolloutCondition = find(conditions, function (cond) { return cond.conditionType === 'ROLLOUT'; });
// Malformed mocks may have
var treatments = uniq(data.treatments);
// If they're only specifying a whitelist we add the treatments there.
var allTreatmentsCondition = firstRolloutCondition ? firstRolloutCondition : conditions[0];
var fullyAllocatedTreatment = allTreatmentsCondition.partitions[0].treatment;
treatments.forEach(function (treatment) {
if (treatment !== fullyAllocatedTreatment) {
allTreatmentsCondition.partitions.push({
treatment: treatment,
size: 0
});
}
});
// Don't need these anymore
delete data.treatments;
});
}
export function splitsParserFromFileFactory() {
var previousMock = 'NO_MOCK_LOADED';
// Parse `.split` configuration file and return a map of feature flag objects
function readFeatureFlagConfigFile(log, filePath) {
var FEATURE_FLAG_POSITION = 0;
var TREATMENT_POSITION = 1;
var data;
try {
data = fs.readFileSync(filePath, 'utf-8');
}
catch (e) {
log.error(e && e.message);
return {};
}
if (data === previousMock)
return false;
previousMock = data;
var featureFlagObjects = data.split(/\r?\n/).reduce(function (accum, line, index) {
var tuple = line.trim();
if (tuple === '' || tuple.charAt(0) === '#') {
log.debug(logPrefix + ("Ignoring empty line or comment at #" + index));
}
else {
tuple = tuple.split(/\s+/);
if (tuple.length !== 2) {
log.debug(logPrefix + ("Ignoring line since it does not have exactly two columns #" + index));
}
else {
var featureFlagName = tuple[FEATURE_FLAG_POSITION];
var condition = parseCondition({ treatment: tuple[TREATMENT_POSITION] });
accum[featureFlagName] = { conditions: [condition], configurations: {}, trafficTypeName: 'localhost' };
}
}
return accum;
}, {});
return featureFlagObjects;
}
// Parse `.yml` or `.yaml` configuration files and return a map of feature flag objects
function readYAMLConfigFile(log, filePath) {
var data = '';
var yamldoc = null;
try {
data = fs.readFileSync(filePath, 'utf8');
if (data === previousMock)
return false;
previousMock = data;
yamldoc = yaml.safeLoad(data);
}
catch (e) {
log.error(e);
return {};
}
// Each entry will be mapped to a condition, but we'll also keep the configurations map.
var mocksData = (yamldoc).reduce(function (accum, featureFlagEntry) {
var featureFlagName = Object.keys(featureFlagEntry)[0];
if (!featureFlagName || !isString(featureFlagEntry[featureFlagName].treatment))
log.error(logPrefix + 'Ignoring entry on YAML since the format is incorrect.');
var mockData = featureFlagEntry[featureFlagName];
// "Template" for each feature flag accumulated data
if (!accum[featureFlagName]) {
accum[featureFlagName] = {
configurations: {}, conditions: [], treatments: [], trafficTypeName: 'localhost'
};
}
// Assign the config if there is one on the mock
if (mockData.config)
accum[featureFlagName].configurations[mockData.treatment] = mockData.config;
// Parse the condition from the entry.
var condition = parseCondition(mockData);
accum[featureFlagName].conditions[condition.conditionType === 'ROLLOUT' ? 'push' : 'unshift'](condition);
// Also keep track of the treatments, will be useful for manager functionality.
accum[featureFlagName].treatments.push(mockData.treatment);
return accum;
}, {});
arrangeConditions(mocksData);
return mocksData;
}
// Load the content of a configuration file into an Object
return function featureFlagsParserFromFile(_a) {
var features = _a.features, log = _a.log;
var filePath = configFilesPath(features);
var mockData;
// If we have a filePath, it means the extension is correct, choose the parser.
if (endsWith(filePath, '.split')) {
log.warn(logPrefix + '.split mocks will be deprecated soon in favor of YAML files, which provide more targeting power. Take a look in our documentation.');
mockData = readFeatureFlagConfigFile(log, filePath);
}
else {
mockData = readYAMLConfigFile(log, filePath);
}
return mockData;
};
}