Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Diverse improvements #9

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a53b64b
Add new tags to package.json :bookmark:
AdrieanKhisbe Dec 8, 2019
7f44acd
Reimplement the env resolver :hammer_and_wrench:
AdrieanKhisbe Dec 8, 2019
1ca9fe4
Implement default feature for env resolver :envelope_with_arrow:
AdrieanKhisbe Dec 8, 2019
e8ba211
Add regexp filter for the env resolver :mag:
AdrieanKhisbe Dec 8, 2019
4ff7d7f
Add new regexp resolver :mag_right:
AdrieanKhisbe Dec 8, 2019
687d4fd
Enhance resolveFile to support custom parser :floppy_disk:
AdrieanKhisbe Dec 9, 2019
22bf875
Intoduce a "echo" protocol to build on transform filters :loudspeaker:
AdrieanKhisbe Dec 10, 2019
e259418
Intoduce a configuration mecanism for the filters :gear:
AdrieanKhisbe Dec 10, 2019
2e03004
Intoduce from/to encoding/decoding filters :currency_exchange:
AdrieanKhisbe Dec 10, 2019
d1ef9df
Enhance to filter to support digest algorithmes :clamp:
AdrieanKhisbe Dec 10, 2019
36a4599
Introduce possibility to override, disable, replace filters :wrench:
AdrieanKhisbe Dec 10, 2019
6a310f5
Support multi args for filter and use it to specify digest encoding :…
AdrieanKhisbe Dec 10, 2019
591cff7
Better configuration api for env/echo protocol filters :joystick:
AdrieanKhisbe Dec 10, 2019
9d54e1b
Extends env configuration with env override and defaults :incoming_en…
AdrieanKhisbe Dec 10, 2019
fc9b6a9
Kill impossible branch to achieve full coverage :shield:
AdrieanKhisbe Dec 10, 2019
432c8eb
Change default resolver configuration :gear:
AdrieanKhisbe Dec 14, 2019
7acff53
Introduce getResolver as more customizable default resolver :wrench:
AdrieanKhisbe Dec 14, 2019
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
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ All these are loaded by the `getDefaultResolver`
- [require](#require)
- [exec](#exec)
- [glob](#glob)
- [regexp](#regexp)


### path
Expand All @@ -92,7 +93,13 @@ Creates a handler which will return a buffer containing the content of the base6
### env
`protocall.handlers.env()`

Creates a handler which will resolve the provided value as an environment variable, optionally casting the value using the provided filter. Supported filters are `|d`, `|b`, and `|!b` which will cast to Number and Boolean types respectively.
Creates a handler which will resolve the provided value as an environment variable, optionally casting the value using the provided filter.
Supported filters are:
- `|d` to cast for numbers
- `|b`, and `|!b` to cast Boolean types (`!b` negating the value)
- `|r` to cast to `RegExp`. (`/` delimiters can be omited if you dont specify any flags.)

You can also specify a default value with a similar syntax than bash, using `:-` to separate the variable name from the default value.

Examples:
```json
Expand All @@ -101,7 +108,10 @@ Examples:
"numver": "env:PORT|d",
"true": "env:ENABLED|b",
"false": "env:FALSY|b",
"notFalse": "env:FALSY|!b"
"notFalse": "env:FALSY|!b",
"regexp": "env:REGEXP|r",
"withDefault": "env:SOMETHING:-with default",
"withDefaultAndFilter": "env:SOME_NUMBER:-12|d",
}
```

Expand Down Expand Up @@ -149,6 +159,18 @@ Creates a handler which match files using the patterns the shell uses.
}
```

### regexp
`protocall.handlers.regexp()`

Creates a handler which will convert string to associated regular expression.

```json
{
"anything": "regexp:.*",
"anythingWithFlag": "regexp:/.*/gm",
}
```

## Resolver API
Basicly a resolver enable you to register new protocalls/handlers, and to resolve object(`resolve`) or files(`resolveFile`)

Expand Down Expand Up @@ -203,8 +225,26 @@ Return a promise that is resolved to the processed data.

Return a promise that is resolved to the processed data.

It is now also possible to provide a specific parser for non JSON files.
To do so, instead to provide file path, provide an object with a `path` and `parser` property.
`resolver.resolveFile({path, parser}, [callback]);`

## Advanced resolver usage

### Loading config from yaml

```js
const protocall = require('protocall');
const yaml = require('js-yaml');
const resolver = protocall.getDefaultResolver();
resolver.resolveFile({
path: './relative/path/to.yaml',
parser: yaml.safeLoad
}).then(config => {
// play around with the config
})
```

### Multiple handlers
Multiple handlers can be registered for a given protocol. They will be executed in the order registered and the output
of one handler will be the input of the next handler in the chain.
Expand Down
18 changes: 15 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
const Resolver = require('./src/resolver');
const handlers = require('./src/handlers');

const getDefaultResolver = (dirname, parent) => {
const getDefaultResolver = options => {
const {dirname, parent, envOptions, echoOptions} = options || {};
const folder = dirname || process.cwd();
return new Resolver(parent, {
path: handlers.path(folder),
file: handlers.file(folder),
base64: handlers.base64(),
env: handlers.env(),
env: handlers.env(envOptions),
require: handlers.require(folder),
exec: handlers.exec(folder)
exec: handlers.exec(folder),
regexp: handlers.regexp(),
echo: handlers.echo(echoOptions)
});
};

const getResolver = options => {
if (options.protocols) return new Resolver(options.parent, options.protocols);

const resolver = getDefaultResolver(options);
if (options.extraProtocols) resolver.use(options.extraProtocols);
return resolver;
};

const create = (parent, initialHandlers) => new Resolver(parent, initialHandlers);

module.exports = {
Resolver,
resolver: Resolver,
create,
handlers,
getResolver,
getDefaultResolver
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
],
"keywords": [
"config",
"configuration"
"configuration",
"shortstop",
"protocol"
],
"author": "Erik Toth <[email protected]>",
"contributors": [
Expand All @@ -50,6 +52,7 @@
"ava": "^2.4.0",
"codecov": "^3.6.1",
"eslint": "^6.6.0",
"js-yaml": "^3.13.1",
"np": "^5.1.2",
"nyc": "^14.1.1"
},
Expand Down
144 changes: 111 additions & 33 deletions src/handlers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const _ = require('lodash/fp');
const globby = require('globby');
const callsites = require('callsites');
Expand Down Expand Up @@ -54,41 +55,122 @@ function base64() {
};
}

function toRegexp(value, params) {
const match = value.match(/^\/(.*)\/([miguys]+)?$/);
if (!match) return new RegExp(value, params);

const [, pattern, flags] = match;
return new RegExp(pattern, flags || params);
}

function regexp(defaultFlags) {
return function regexpHandler(value) {
return toRegexp(value, defaultFlags);
};
}

const DIGEST_ALGORITHMS = ['md5', 'md4', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'];

const DEFAULT_FILTERS = {
d(value) {
return parseInt(value, 10);
},
b(value) {
return !['', 'false', '0', undefined].includes(value);
},
'!b'(value) {
return ['', 'false', '0', undefined].includes(value);
},
r(value, params) {
return toRegexp(value, params);
},
from(value, encoding) {
if (!encoding) throw new Error('Missing configuration for the from filter');
if (['b64', 'base64'].includes(encoding)) return Buffer.from(value, 'base64').toString('utf-8');
if (encoding === 'hex') return Buffer.from(value, 'hex').toString('utf-8');
throw new Error(`Unkown format specifed for from filter: '${encoding}'`);
},
to(value, encodingOrAlgo, digestEncoding = 'hex') {
if (!encodingOrAlgo) throw new Error('Missing configuration for the to filter');
if (['b64', 'base64'].includes(encodingOrAlgo)) return Buffer.from(value).toString('base64');
if (encodingOrAlgo === 'hex') return Buffer.from(value).toString('hex');
if (DIGEST_ALGORITHMS.includes(encodingOrAlgo))
return crypto
.createHash(encodingOrAlgo)
.update(value)
.digest(digestEncoding);

throw new Error(`Unkown format specifed for to filter: '${encodingOrAlgo}'`);
}
};

const shouldMerge = ({merge, replace}) => {
if (merge) return true;
if (replace) return false;
if (merge === false && replace === undefined) return false;
return true;
};

/**
* Creates the protocol handler for the `env:` protocol
* @returns {Function}
*/
function env() {
const filters = {
d(value) {
return parseInt(value, 10);
},
b(value) {
return !['', 'false', '0', undefined].includes(value);
},
'!b'(value) {
return ['', 'false', '0', undefined].includes(value);
}
function env(options = {}) {
const filters = options.filters
? shouldMerge(options)
? Object.assign({}, DEFAULT_FILTERS, options.filters)
: options.filters
: DEFAULT_FILTERS;

const env = options.env ? options.env : process.env;

const getValue = (key, defaultOverride) => {
const rawValue = env[key];
if (rawValue !== undefined) return rawValue;
if (defaultOverride) return defaultOverride;
if (options.defaults) return options.defaults[key];
};

return function envHandler(value) {
let result;

Object.keys(filters).some(function(key) {
const fn = filters[key];
const pattern = `|${key}`;
const loc = value.indexOf(pattern);

if (loc > -1 && loc === value.length - pattern.length) {
value = value.slice(0, -pattern.length);
result = fn(process.env[value]);
return true;
}

return false;
});
const match = value.match(/^([\w_]+)(?::-(.+?))?(?:[|](.+))?$/);
if (!match) throw new Error(`Invalid env protocol provided: '${value}'`);
const [, envVariableName, defaultValue, filter] = match;
// TODO: later, could add multiple filters

const resolvedValue = getValue(envVariableName, defaultValue);
if (!filter) return resolvedValue;

const [filterName, ...filterParams] = filter.trim().split(':');
const filterHandler = filters[filterName];
if (!filterHandler)
throw new Error(`Invalid env protocol provided, unknown filter: '${value}'`);
return filterHandler(resolvedValue, ...filterParams);
};
}

return result === undefined ? process.env[value] : result;
/**
* Creates the protocol handler for the `echo:` protocol
* @returns {Function}
*/
function echo(options = {}) {
const filters = options.filters
? shouldMerge(options)
? Object.assign({}, DEFAULT_FILTERS, options.filters)
: options.filters
: DEFAULT_FILTERS;

return function echoHandler(value) {
const [, echoString, filter] = value.match(/^(.*?)(?:[|](.+))?$/);
if (!echoString || echoString.endsWith('|'))
throw new Error(`Invalid echo protocol provided: '${value}'`);
// TODO: later, could add multiple filters
if (!filter) return echoString;

const [filterName, ...filterParams] = filter.trim().split(':');
const filterHandler = filters[filterName];
if (!filterHandler)
throw new Error(`Invalid echo protocol provided, unknown filter: '${value}'`);
return filterHandler(echoString, ...filterParams);
};
}

Expand Down Expand Up @@ -135,11 +217,7 @@ function exec(basedir) {
* @returns {Function}
*/
function glob(options) {
if (_.isString(options)) {
options = {cwd: options};
}

options = options || {};
options = _.isString(options) ? {cwd: options} : options || {};
options.cwd = options.cwd || getCallerFolder();

const resolvePath = _path(options.cwd);
Expand All @@ -148,4 +226,4 @@ function glob(options) {
};
}

module.exports = {path: _path, file, base64, env, require: _require, exec, glob};
module.exports = {path: _path, file, base64, regexp, env, echo, require: _require, exec, glob};
20 changes: 11 additions & 9 deletions src/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ function isModule(file) {
}

class Resolver {
constructor(parent, handlers) {
if (!(parent instanceof Resolver) && !handlers) {
handlers = parent;
constructor(parent, protocols) {
if (!(parent instanceof Resolver) && !protocols) {
protocols = parent;
parent = null;
}
this.parent = parent;
this._handlers = {};
if (handlers) {
this.use(handlers);
if (protocols) {
this.use(protocols);
}
}

Expand Down Expand Up @@ -138,15 +138,17 @@ class Resolver {
}

resolveFile(file, callback) {
if (isModule(file))
const {path, parser} = _.isString(file) ? {path: file, parser: JSON.parse} : file;

if (isModule(path))
// eslint-disable-next-line import/no-dynamic-require
return this.resolve(require(file), file, callback);
return this.resolve(require(path), path, callback);

const result = new Promise((resolve, reject) =>
fs.readFile(file, 'utf8', (err, data) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) return reject(err);
try {
return resolve(JSON.parse(data));
return resolve(parser(data));
} catch (parsingError) {
reject(parsingError);
}
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
foo: bar
truthy: true
falsy: false
numeric: 10
call: "foo:maybe"
i:
came: "bar:in"
like:
- "foo:a"
- wrecking: "bar:ball"
Loading