Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2
jobs:
lint:
docker:
- image: circleci/node:8.5
- image: circleci/node:8.9
steps:
- checkout
- run:
Expand All @@ -13,7 +13,7 @@ jobs:
command: npm run lint
test:
docker:
- image: circleci/node:8.5
- image: circleci/node:8.9
steps:
- checkout
- run:
Expand Down
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"env": {
"node": true
},
"extends": "qubyte/ES2015"
"extends": "qubyte/ES2015",
"rules": {
"complexity": ["error", 5]
}
}
11 changes: 10 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

const processConfig = require('./lib/processConfig');
const makeParsers = require('./lib/makeParsers');
const checkTypes = require('./lib/checkTypes');
const checkRequiredProperties = require('./lib/checkRequiredProperties');

module.exports = function configeur(schema, options = {}) {
return processConfig(schema, process.env, makeParsers(options.parsers), options.mutable);
const parsers = makeParsers(options.parsers);

checkTypes(parsers, schema);
checkRequiredProperties(process.env, schema);

const config = processConfig(schema, process.env, parsers);

return options.mutable ? config : Object.freeze(config);
};

module.exports.default = module.exports;
17 changes: 17 additions & 0 deletions lib/checkRequiredProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

module.exports = function checkRequiredProperties(env, schema) {
const missingRequiredProperties = [];

for (const optionName of Object.keys(schema)) {
if (schema[optionName].required && typeof env[optionName] !== 'string') {
missingRequiredProperties.push(optionName);
}
}

if (missingRequiredProperties.length) {
const error = new Error(`Missing required config for: ${missingRequiredProperties.join(', ')}`);
error.missingRequiredProperties = missingRequiredProperties;
throw error;
}
};
12 changes: 12 additions & 0 deletions lib/checkTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

module.exports = function checkTypes(parsers, schema) {
for (const optionName of Object.keys(schema)) {
const optionProperties = schema[optionName];
const type = optionProperties.type || 'string';

if (!parsers.get(type)) {
throw new Error(`Unsupported config value type: ${type}`);
}
}
};
7 changes: 0 additions & 7 deletions lib/defaultParsers.js

This file was deleted.

8 changes: 6 additions & 2 deletions lib/makeParsers.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
'use strict';

const defaultParsers = require('./defaultParsers');

function checkValidParserSpec(spec) {
return Array.isArray(spec) && spec.length === 2 && typeof spec[0] === 'string' && typeof spec[1] === 'function';
}

module.exports = function makeParsers(additional) {
const defaultParsers = [
['string', value => value],
['boolean', value => value === 'true'],
['number', Number]
];

if (additional === undefined) {
return new Map(defaultParsers);
}
Expand Down
34 changes: 9 additions & 25 deletions lib/processConfig.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,23 @@
'use strict';

function parse(name, value, parser) {
function checkAndParseOption(optionName, schema, env, parsers) {
const { type, defaultValue } = schema[optionName];
const parser = parsers.get(type || 'string');
const value = env[optionName] || defaultValue;

if (typeof value !== 'string') {
throw new Error(`Value to cast for ${name} was not a string: ${value}`);
throw new Error(`Value to cast for ${optionName} was not a string: ${value}`);
}

return parser(value);
}

module.exports = function processConfig(schema, env, parsers, mutable) {
module.exports = function processConfig(schema, env, parsers) {
const config = Object.create(null);
const envNames = Object.getOwnPropertyNames(env);
const missingRequiredProperties = [];

for (const optionName of Object.keys(schema)) {
const optionProperties = schema[optionName];
const type = Object.prototype.hasOwnProperty.call(optionProperties, 'type') ? optionProperties.type : 'string';
const parser = parsers.get(type);

if (!parser) {
throw new Error(`Unsupported config value type: ${type}`);
}

if (envNames.includes(optionName)) {
config[optionName] = parse(optionName, env[optionName], parser);
} else if (!optionProperties.required) {
config[optionName] = parse(optionName, optionProperties.defaultValue, parser);
} else {
missingRequiredProperties.push(optionName);
}
}

if (missingRequiredProperties.length) {
throw new Error(`Missing required config for: ${missingRequiredProperties.join(', ')}`);
config[optionName] = checkAndParseOption(optionName, schema, env, parsers);
}

return mutable ? config : Object.freeze(config);
return config;
};
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"files": [
"index.js",
"lib/defaultParsers.js",
"lib/checkRequiredProperties.js",
"lib/checkTypes.js",
"lib/makeParsers.js",
"lib/processConfig.js",
"index.d.ts"
Expand All @@ -32,10 +33,9 @@
},
"homepage": "https://github.com/qubyte/configeur",
"devDependencies": {
"eslint": "^4.8.0",
"eslint": "^4.17.0",
"eslint-config-qubyte": "^1.1.0",
"mocha": "^4.0.0",
"sandboxed-module": "^2.0.3",
"sinon": "^4.0.1"
"mocha": "^5.0.0",
"sandboxed-module": "^2.0.3"
}
}
55 changes: 0 additions & 55 deletions test/index.test.js

This file was deleted.

143 changes: 143 additions & 0 deletions test/integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict';

const assert = require('assert');
const SandboxedModule = require('sandboxed-module');

const env = {};
const configeur = SandboxedModule.require('../index', {
globals: {
process: {
env
}
}
});

describe('configeur', () => {
let schema;

beforeEach(() => {
for (const name of Object.getOwnPropertyNames(env)) {
delete env[name];
}

env.AN_ARG = 'Hello, world!';

schema = {
AN_ARG: {
type: 'string',
required: true
},
ANOTHER_ARG: {
type: 'number',
defaultValue: '123'
}
};
});

it('is a function', () => {
assert.strictEqual(typeof configeur, 'function');
});

it('re-exports itself as default', () => {
assert.equal(configeur.default, configeur);
});

it('returns a frozen prototypeless object when mutable is falsy', () => {
const config = configeur(schema);

assert.strictEqual(Object.getPrototypeOf(config), null);
assert.ok(Object.isFrozen(config));
assert.deepStrictEqual(Object.assign({}, config), {
AN_ARG: 'Hello, world!',
ANOTHER_ARG: 123
});
});

it('returns a non-frozen prototypeless object when mutable is truthy', () => {
const config = configeur(schema, { mutable: true });

assert.strictEqual(Object.getPrototypeOf(config), null);
assert.ok(!Object.isFrozen(config));
assert.deepStrictEqual(Object.assign({}, config), {
AN_ARG: 'Hello, world!',
ANOTHER_ARG: 123
});
});

it('provides parsers for strings (default), numbers, and booleans', () => {
const schema = {
A: { defaultValue: 'blah' },
B: { type: 'string', defaultValue: 'bleh' },
C: { type: 'number', defaultValue: '123' },
D: { type: 'boolean', defaultValue: 'true' }
};

const fine = configeur(schema);

assert.deepStrictEqual(Object.assign({}, fine), { A: 'blah', B: 'bleh', C: 123, D: true });
});

it('allows additional parsers to be provided', () => {
const schema = {
A: { defaultValue: 'blah' },
B: { type: 'string', defaultValue: 'bleh' },
C: { type: 'number', defaultValue: '123' },
D: { type: 'boolean', defaultValue: 'true' },
E: { type: 'custom', defaultValue: 'flop' }
};

const parsers = [
['custom', () => 'flop']
];

const fine = configeur(schema, { parsers });

assert.deepStrictEqual(Object.assign({}, fine), { A: 'blah', B: 'bleh', C: 123, D: true, E: 'flop' });
});

it('checks for invalid parser types', () => {
const schema = {
A: { defaultValue: 'blah' },
B: { type: 'string', defaultValue: 'bleh' },
C: { type: 'number', defaultValue: '123' },
D: { type: 'boolean', defaultValue: 'true' },
E: { type: 'custom', defaultValue: 'flop' },
F: { type: 'non-existent', defaultValue: 'uh oh' }
};

const parsers = [
['custom', () => 'flop']
];

assert.throws(
() => configeur(schema, { parsers }),
err => err.message === 'Unsupported config value type: non-existent'
);
});

it('lists missing required options in a thrown error', () => {
const schema = {
A: { required: true },
AN_ARG: { required: true },
C: { required: true },
D: { defaultValue: 'blah' }
};

assert.throws(
() => configeur(schema),
err => {
const [x, y, ...z] = err.missingRequiredProperties;
return err instanceof Error && z.length === 0 && x === 'A' && y === 'C';
}
);
});

it('throws an error when an environment property is not a string', () => {
env.ANOTHER_ARG = 123;

assert.throws(
() => configeur(schema),
err => err instanceof Error && err.message === 'Value to cast for ANOTHER_ARG was not a string: 123'
);
});
});
Loading