Skip to content

Commit

Permalink
fix: init function now works properly with default params (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
otaciliolacerda authored Jul 19, 2020
1 parent c41f6ff commit b97ae2b
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 67 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ YAML is a superset of JSON and, as such, is a convenient format for specifying h
config["my-app-name"]
```

⚠️ Although Javascript and YAML are case sensitive and accept properties with same name but different case in the same object (e.g. `variable`/`VARiable`, this lib will through an error in such cases. This constraint has the goal to improve the configuration readability and to make it possible the override of such properties by system variables.
⚠️ Although Javascript and YAML are case sensitive and accept properties with same name but different case in the same object (e.g. `variable`/`VARiable`, this lib will throw an error in such cases. This constraint has the goal to improve the configuration readability and to make it possible the override of such properties by system variables.

#### Name Convention

Expand Down
76 changes: 13 additions & 63 deletions lib/autoConfig.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,29 @@
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

const {
isObject,
loadConfiguration,
overrideConfigValuesFromSystemVariables,
} = require('./utils');

// Global config
let config;

/**
* Do a deep merge of two objects. This function will lead to infinite recursion on circular references
* @param target
* @param source
*/
function mergeDeep(target, source) {
const output = { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target)) Object.assign(output, { [key]: source[key] });
else output[key] = mergeDeep(target[key], source[key]);
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}

/**
* Load a configuration file recursively. The nested config overrides the previous.
* This function will lead to infinite recursion on circular references
* @param confObj
*/
function loadConfig(confObj, filePath, configDirectory) {
let resultConfig = {};
if (confObj.include) {
if (Array.isArray(confObj))
throw new Error(`${filePath}: include field must be an array!`);

confObj.include.forEach(confName => {
const subPath = path.join(configDirectory, `app.${confName}.config.yaml`);
const includeConf = yaml.safeLoad(fs.readFileSync(subPath, 'utf8'));
resultConfig = mergeDeep(
resultConfig,
loadConfig(includeConf, subPath, configDirectory)
);
});
}
return mergeDeep(resultConfig, confObj);
}

function init({ profile = process.env.NODE_ENV, configDirectory = './' }) {
if (!profile) throw new Error('NODE_ENV not set.');
function init({
profile = process.env.NODE_ENV,
configDirectory = process.cwd(),
} = {}) {
if (!profile)
throw new Error(
'No profile was given: set NODE_ENV or pass it as a parameter'
);

if (config) {
console.warn(
'autoConfig.init was called again. Config will be re-initialized.'
'Config will be re-initialized: autoConfig.init was called again'
);
}

try {
// Load config
const filePath = path.join(configDirectory, `app.${profile}.config.yaml`);

const confObj = yaml.safeLoad(fs.readFileSync(filePath, 'utf8'));
config = loadConfig(confObj, filePath, configDirectory);

overrideConfigValuesFromSystemVariables(config);
delete config.include;
} catch (e) {
throw new Error(`Auto configuration failed. ${e}`);
}
config = loadConfiguration(configDirectory, profile);
overrideConfigValuesFromSystemVariables(config);
delete config.include;
}

module.exports = { init, getConfig: () => config };
57 changes: 57 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
Expand Down Expand Up @@ -85,10 +89,63 @@ function overrideConfigValuesFromSystemVariables(
});
}

// This function will lead to infinite recursion on circular references
function mergeDeep(target, source) {
const output = { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target)) Object.assign(output, { [key]: source[key] });
else output[key] = mergeDeep(target[key], source[key]);
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}

function loadYamlFile(configDirectory, profile) {
const filePath = path.join(configDirectory, `app.${profile}.config.yaml`);
return yaml.safeLoad(fs.readFileSync(filePath, 'utf8'));
}

// This function will lead to an infinite loop on circular references
function loadConfiguration(configDirectory, profile) {
const queue = [];

const rootConfigObject = loadYamlFile(configDirectory, profile);

let resultConfig = {};
let currentConfig = rootConfigObject;
let currentProfile = profile;
do {
if (currentConfig.include) {
if (!Array.isArray(currentConfig.include)) {
throw new Error(
`Include field must be an array in profile: ${profile}!`
);
}
currentConfig.include.forEach(p => queue.unshift(p));
}

currentProfile = queue.shift();
if (currentProfile) {
currentConfig = loadYamlFile(configDirectory, currentProfile);
resultConfig = mergeDeep(resultConfig, currentConfig);
}
} while (queue.length);

return mergeDeep(resultConfig, rootConfigObject);
}

module.exports = {
isObject,
hasValue,
getPropertyCaseInsensitive,
setPropertyCaseInsensitive,
overrideConfigValuesFromSystemVariables,
mergeDeep,
loadYamlFile,
loadConfiguration,
};
72 changes: 69 additions & 3 deletions test/autoConfig.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,71 @@
describe('Test autoConfig', () => {
it('dummy test to setup jest', () => {
expect(true).toBeTruthy();
const utils = require('../lib/utils');

jest.mock('../lib/utils', () => ({
loadConfiguration: jest.fn(() => ({
include: [],
})),
overrideConfigValuesFromSystemVariables: jest.fn(),
}));

describe('test autoConfig', () => {
let autoConfig;

beforeEach(() => {
jest.isolateModules(() => {
autoConfig = require('../lib/autoConfig');
});
});

it('should throw error if profile is not defined', () => {
delete process.env.NODE_ENV;
expect(() => autoConfig.init()).toThrowError(
/No profile was given: set NODE_ENV or pass it as a parameter/
);
process.env.NODE_ENV = 'test';
});

it('should warn if init is called more than once', () => {
const spy = jest.spyOn(console, 'warn');
spy.mockImplementation(() => {});

autoConfig.init();
autoConfig.init();

expect(console.warn).toHaveBeenCalledWith(
'Config will be re-initialized: autoConfig.init was called again'
);
spy.mockRestore();
});

it('should not contain keyword include in the config object', () => {
autoConfig.init();
expect(autoConfig.getConfig().include).toBeUndefined();
});

it('should configure profile using optional object parameter', () => {
autoConfig.init({ profile: 'mockProfile' });
expect(utils.loadConfiguration).toHaveBeenCalledWith(
expect.anything(),
'mockProfile'
);
});

it('should configure config directory using option object parameter', () => {
autoConfig.init({ profile: 'mockProfile', configDirectory: '/mock/test' });
expect(utils.loadConfiguration).toHaveBeenCalledWith(
'/mock/test',
'mockProfile'
);
});

it('should be able to get the configuration files', () => {
const mockConfig = {
test: 1,
mock: 'mock',
};
utils.loadConfiguration.mockImplementationOnce(() => mockConfig);
expect(autoConfig.getConfig()).toBeUndefined();
autoConfig.init();
expect(autoConfig.getConfig()).toEqual(mockConfig);
});
});

0 comments on commit b97ae2b

Please sign in to comment.