Skip to content

Commit

Permalink
Add tests, linting, and refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanricard committed Feb 20, 2018
1 parent 6c13d71 commit f985831
Show file tree
Hide file tree
Showing 17 changed files with 1,435 additions and 22 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/**
28 changes: 28 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
extends: "airbnb/base"

env:
node: true
mocha: true

ecmaFeatures:
modules: false

rules:
strict: [2, global]
no-param-reassign: 0
no-shadow: 1
comma-dangle: [2, never]
no-unused-vars: 1
space-before-function-paren: 0
new-cap: 0
eol-last: 0

# Node Rules
no-new-require: 2
no-path-concat: 2
no-process-exit: 2
no-sync: 2
no-mixed-requires: 1
callback-return: 0
handle-callback-err: 0
64 changes: 43 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
const fs = require("fs");
const path = require("path");
const assert = require("assert");
const nconf = require("nconf");
const recurpolate = require("recurpolate");

const getJsonExtendsOrder = (rootDir, jsonFilePath, parentName, childPath) => {
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const { Provider } = require('nconf');
const recurpolate = require('recurpolate');
const { defaultsDeep } = require('lodash');

const getJsonExtendsOrder = (configDirPath, jsonFilePath, prevJsonFilePath, prevFileJson = {}) => {
const result = [jsonFilePath];
let fileJson;

try {
fileJson = fs.readFileSync(jsonFilePath, "utf8");
fileJson = fs.readFileSync(jsonFilePath, 'utf8');
} catch (err) {
throw new Error(
`Parent config "${parentName}" not found for child ${childPath}`
);
if (prevFileJson.extends) {
throw new Error(
`Parent config "${prevFileJson.extends}" not found for child ${prevJsonFilePath}`
);
}
throw new Error(`Config file ${jsonFilePath} not found`);
}

try {
Expand All @@ -23,8 +27,10 @@ const getJsonExtendsOrder = (rootDir, jsonFilePath, parentName, childPath) => {
}

if (fileJson.extends) {
const nextJsonFilePath = path.join(rootDir, `${fileJson.extends}.json`);
return result.concat(getJsonExtendsOrder(rootDir, nextJsonFilePath, fileJson.extends, jsonFilePath));
const nextJsonFilePath = path.join(configDirPath, `${fileJson.extends}.json`);
return result.concat(
getJsonExtendsOrder(configDirPath, nextJsonFilePath, jsonFilePath, fileJson)
);
}

return result;
Expand All @@ -38,22 +44,38 @@ function recurp(nconf) {
}

function prepareOptions(options) {
if (options.configDirPath) assert(path.isAbsolute(options.configDirPath, 'Optional configuration `options.configDirPath` must be an absolute path when provided.'))
return options;
if (options.configDirPath) {
assert(
path.isAbsolute(options.configDirPath),
'Optional configuration `options.configDirPath` must be an absolute path when provided.'
);
}

// deep merge default object
return defaultsDeep(
{
nconf: {
argv: {},
env: {}
}
},
options
);
}

function discernConfigDirPath(configFilePath, configDirPath = "") {
function discernConfigDirPath(configFilePath, configDirPath = '') {
// return configDirPath when `options.configDirPath` is configured
if (configDirPath) return configDirPath;
// else assume relative to configFilePath
const pathSegments = configFilePath.split("/");
const pathSegments = configFilePath.split('/');
pathSegments.pop();
return pathSegments.join("/");
return pathSegments.join('/');
}

module.exports = function appfig(configFilePath, afterConfigLoaded = noop, options = {}) {
module.exports = function appfig(configFilePath, options = {}, afterConfigLoaded) {
// prepare options
const _options, { configDirPath, nconf: nconfOptions } = prepareOptions(options);
const _options = prepareOptions(options);
const { nconf: nconfOptions } = _options;
const { argv: argvOptions, env: envOptions } = nconfOptions;

// assign path to configuration directory
Expand All @@ -63,7 +85,7 @@ module.exports = function appfig(configFilePath, afterConfigLoaded = noop, optio
const jsonExtendsOrder = getJsonExtendsOrder(configDirPath, configFilePath);

// instantiate new isolated instance of nconf
const nconf = new nconf.Provider();
const nconf = new Provider();

// load configuration with careful attention to precedence
nconf
Expand Down
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@
"description": "Application configuration made easy.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "mocha ./test/**/*.test.js",
"test:watch": "npm run test -- --watch",
"lint": "eslint ."
},
"author": "Ryan Ricard",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.4",
"nconf": "^0.9.1",
"recurpolate": "^1.2.0"
},
"devDependencies": {
"chai": "^4.1.2",
"eslint": "^4.14.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.8.0",
"mocha": "^4.0.1"
}
}
50 changes: 50 additions & 0 deletions test/configuration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { exec } = require('child_process');
const { expect } = require('chai');
const { developmentConfigPath } = require('./fixtures/paths');

describe('configuration', () => {
describe('nconf options', () => {
it('should faciliate nconf `.env()` options', function(done) {
exec(
`echo "
const appfig = require('./');
const config = appfig('${developmentConfigPath}', {
nconf: {
env: {
whitelist: ['NODE_ENV']
}
}
});
console.log(config.get('NODE_ENV'));
console.log(config.get('OTHER_ENV'));
" | NODE_ENV=dev OTHER_ENV=true node`,
(err, stdout, stderr) => {
expect(stdout).to.match(/^dev\sundefined\s$/);
done();
}
);
});

it('should faciliate nconf `.argv()` options', function(done) {
exec(
`echo "
const appfig = require('./');
const config = appfig('${developmentConfigPath}', {
nconf: {
argv: {
foo: {
default: 'bar'
}
}
}
});
console.log(config.get('foo'));
" | node`,
(err, stdout, stderr) => {
expect(stdout).to.match(/^bar\s$/);
done();
}
);
});
});
});
47 changes: 47 additions & 0 deletions test/errors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const path = require('path');
const { expect } = require('chai');
const appfig = require('../');

const {
developmentConfigPath,
orphanedConfigPath,
invalidConfigPath
} = require('./fixtures/paths');

describe('errors', () => {
describe('file error handling', () => {
it('should throw an exception when dependent file cannot be found', () => {
const errorMessage = `Parent config "non-existent" not found for child ${orphanedConfigPath}`;
expect(appfig.bind(null, orphanedConfigPath)).to.throw(errorMessage);
});

it('should throw an exception when a file is not valid JSON', () => {
const errorMessage = `Config file "${invalidConfigPath}" invalid JSON`;
expect(appfig.bind(null, invalidConfigPath)).to.throw(errorMessage);
});
});

describe('options error handling', () => {
it('should not throw validation exception when `options.configDirPath` is not provided', () => {
expect(appfig.bind(null, developmentConfigPath)).to.not.throw();
});

it('should throw validation exception when `options.configDirPath` is provided and is not a absolute path', () => {
expect(
appfig.bind(null, developmentConfigPath, {
configDirPath: 'not/absolute/path'
})
).to.throw(
'Optional configuration `options.configDirPath` must be an absolute path when provided.'
);
});

it('should not throw validation exception when `options.configDirPath` is an absolute path', () => {
expect(
appfig.bind(null, developmentConfigPath, {
configDirPath: path.join(__dirname, './fixtures/config')
})
).to.not.throw();
});
});
});
4 changes: 4 additions & 0 deletions test/fixtures/config/base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "base",
"base": true
}
9 changes: 9 additions & 0 deletions test/fixtures/config/development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "intermediary",
"name": "development",
"development": true,
"foo": {
"bar": "baa"
},
"recurp": "${foo.bar}-baz"
}
5 changes: 5 additions & 0 deletions test/fixtures/config/intermediary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "base",
"name": "intermediary",
"intermediary": true
}
3 changes: 3 additions & 0 deletions test/fixtures/config/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"some: "invalid json"

3 changes: 3 additions & 0 deletions test/fixtures/config/orphaned.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "non-existent"
}
9 changes: 9 additions & 0 deletions test/fixtures/paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const path = require('path');

module.exports = {
baseConfigPath: path.join(__dirname, 'config/base.json'),
developmentConfigPath: path.join(__dirname, 'config/development.json'),
invalidConfigPath: path.join(__dirname, 'config/invalid.json'),
orphanedConfigPath: path.join(__dirname, 'config/orphaned.json'),
secondaryConfigPath: path.join(__dirname, 'secondary.json')
};
5 changes: 5 additions & 0 deletions test/fixtures/secondary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "./config/base",
"name": "other",
"other": true
}
49 changes: 49 additions & 0 deletions test/loading.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { exec } = require('child_process');
const { expect } = require('chai');
const appfig = require('../');

const { baseConfigPath, developmentConfigPath, secondaryConfigPath } = require('./fixtures/paths');

describe('loading', () => {
it('should load configuration files', () => {
const config = appfig(baseConfigPath);

expect(config.get('name')).to.equal('base');
});

it('should load configuration files from varying locations', () => {
const config = appfig(secondaryConfigPath);

expect(config.get('name')).to.equal('other');
expect(config.get('other')).to.be.true;
expect(config.get('base')).to.be.true;
});

it('should load shell environment variables', function(done) {
exec(
`echo "
const appfig = require('./');
const config = appfig('${developmentConfigPath}');
console.log(config.get('FOO'));
" | FOO=envbar node`,
(err, stdout, stderr) => {
expect(stdout).to.match(/^envbar\s$/);
done();
}
);
});

it('should load command arguments', function(done) {
exec(
`echo "
const appfig = require('./');
const config = appfig('${developmentConfigPath}');
console.log(config.get('FOO'));
" | node - --FOO=argvbar`,
(err, stdout, stderr) => {
expect(stdout).to.match(/^argvbar\s$/);
done();
}
);
});
});
Loading

0 comments on commit f985831

Please sign in to comment.