UNPKG

@splitsoftware/splitio

Version:
144 lines (143 loc) 6.87 kB
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; }; }