Skip to content

Commit

Permalink
Made it possible to define custom converters
Browse files Browse the repository at this point in the history
Also made the code more modular
  • Loading branch information
dlmr committed May 1, 2016
1 parent bfc8f86 commit ad26a10
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 89 deletions.
19 changes: 17 additions & 2 deletions docs/config/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,28 @@ module.exports = {
_description: 'Description for 2',
subgroup: 'Description for subgroup'
},
}
},
validations: {
group1: {
//
},
group2: {
//
}
},
converters: {
group1: {
//
},
group2: {
//
}
}
}
}
```

As can be seen in the example above the Roc settings meta object can have 3 properties; `descriptions`, `groups`, `validations`.
As can be seen in the example above the Roc settings meta object can have 4 properties; `descriptions`, `groups`, `validations`, `converters`.

### `descriptions`
Should mirror the groups and general structure of the normal settings object adding description to the properties. These descriptions will be shown when using the cli and when generation documentation for the settings.
Expand All @@ -85,3 +93,10 @@ Roc assumes that the validators used is either a RegExp or a function that will
For convenience several types of validators exists in `roc` that can be imported from `roc/validators`. For a complete list of them please see [the JSDocs](/docs/JSDocs.md).

See [roc.config.meta.js in roc-web for an example of how the configuration might look](https://github.com/vgno/roc-web/blob/master/src/roc/config/roc.config.meta.js#L62).

### `converters`
Roc will by default provide an automatic converter that will base its converter on the default value of the settings. In most cases will this be sufficient but in some instances a custom converter might be needed. This custom converter will in most cases get a string as input and it's expected to return the correct type.

Should mirror the groups and general structure of the normal settings object adding converters to the properties. These converters will be used by the cli to convert inputs given to the cli directly.

For convenience several types of converters exists in `roc` that can be imported from `roc/converters`. For a complete list of them please see [the JSDocs](/docs/JSDocs.md).
36 changes: 2 additions & 34 deletions src/cli/helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import chalk from 'chalk';
import { isPlainObject, isBoolean, isString, set, difference, isFunction, isRegExp } from 'lodash';
import { isPlainObject, set, difference, isFunction } from 'lodash';
import trimNewlines from 'trim-newlines';
import redent from 'redent';

Expand All @@ -12,7 +12,6 @@ import { fileExists, getRocPackageDependencies, getRocPluginDependencies, getPac
import onProperty from '../helpers/on-property';
import { isValid, throwError } from '../validation';
import { warning, infoLabel, errorLabel, warningLabel, feedbackMessage } from '../helpers/style';
import { toArray, toRegExp } from '../converters';
import { registerAction } from '../hooks/actions';
import getSuggestions from '../helpers/get-suggestions';

Expand Down Expand Up @@ -433,7 +432,7 @@ export function getMappings(documentationObject = []) {
mappings[element.cli.substr(2)] = {
name: element.cli,
path: element.path,
converter: getConverter(element.defaultValue, element.cli),
converter: element.converter,
validator: element.validator
};
});
Expand All @@ -447,37 +446,6 @@ export function getMappings(documentationObject = []) {
return recursiveHelper(documentationObject);
}

// Convert values based on their default value
function getConverter(value, name) {
if (isBoolean(value)) {
return (input) => {
if (isBoolean(input)) {
return input;
}
if (input === 'true' || input === 'false') {
return input === 'true';
}

console.log(feedbackMessage(
warningLabel('Warning', 'Conversion Failed'),
`Invalid value given for ${chalk.bold(name)}. Will use the default ${chalk.bold(value)}.`
));

return value;
};
} else if (isRegExp(value)) {
return (input) => toRegExp(input);
} else if (Array.isArray(value)) {
return toArray;
} else if (Number.isInteger(value)) {
return (input) => parseInt(input, 10);
} else if (!isString(value) && (!value || Object.keys(value).length === 0)) {
return (input) => JSON.parse(input);
}

return (input) => input;
}

/**
* Converts a set of options to {@link rocConfigSettings} object and command specific options.
*
Expand Down
18 changes: 18 additions & 0 deletions src/converters/automatic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { isBoolean, isString, isRegExp } from 'lodash';
import { toArray, toRegExp, toBoolean, toInteger, toObject } from '../converters';

export default function automaticConverter(value, name) {
if (isBoolean(value)) {
return (input) => toBoolean(input, value, name);
} else if (isRegExp(value)) {
return toRegExp;
} else if (Array.isArray(value)) {
return toArray;
} else if (Number.isInteger(value)) {
return toInteger;
} else if (!isString(value) && (!value || Object.keys(value).length === 0)) {
return toObject;
}

return (input) => input;
}
4 changes: 4 additions & 0 deletions src/converters/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export toArray from './to-array';
export toRegExp from './to-regexp';
export toBoolean from './to-boolean';
export toInteger from './to-integer';
export toObject from './to-object';
export automatic from './automatic';
29 changes: 29 additions & 0 deletions src/converters/to-boolean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import chalk from 'chalk';
import { isBoolean } from 'lodash';

import { warningLabel, feedbackMessage } from '../helpers/style';

/**
* Given an input the function will return a boolean.
*
* @param {object} input - The input to be converted.
* @param {boolean} defaultValue - Default value to use if conversion fails.
* @param {string} name - The name of of what is converted.
*
* @returns {bool} - The converted result.
*/
export default function toBoolean(input, defaultValue, name) {
if (isBoolean(input)) {
return input;
}
if (input === 'true' || input === 'false') {
return input === 'true';
}

console.log(feedbackMessage(
warningLabel('Warning', 'Conversion Failed'),
`Invalid value given for ${chalk.bold(name)}. Will use the default ${chalk.bold(defaultValue)}.`
));

return defaultValue;
}
16 changes: 16 additions & 0 deletions src/converters/to-integer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { isInteger } from 'lodash';

/**
* Given an input the function will return an number.
*
* @param {object} input - The input to be converted.
*
* @returns {number} - The converted result.
*/
export default function toInteger(input) {
if (isInteger(input)) {
return input;
}

return parseInt(input, 10);
}
16 changes: 16 additions & 0 deletions src/converters/to-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { isPlainObject } from 'lodash';

/**
* Given an input the function will return an object.
*
* @param {object} input - The input to be converted.
*
* @returns {object} - The converted result.
*/
export default function toObject(input) {
if (isPlainObject(input)) {
return input;
}

return JSON.parse(input);
}
34 changes: 23 additions & 11 deletions src/documentation/build-documentation-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isPlainObject, isFunction } from 'lodash';

import { toCliOption } from './helpers';
import onProperty from '../helpers/on-property';
import automaticConverter from '../converters/automatic';

const defaultValidation = (input, info) => info ? {type: 'Unknown'} : true;

Expand All @@ -20,38 +21,41 @@ export default function buildDocumentationObject(initalObject, meta = {}, inital
return Object.keys(object).map(callback).filter((value) => value !== undefined);
};

const manageGroup = (object, name, group = {}, description = {}, validation = {}, parents, level, parentNames) => {
const manageGroup = (object, name, group = {}, description = {}, validation = {}, converters = {},
parents, level, parentNames) => {
const groupDescription = isPlainObject(group) ? group._description || undefined : group;
return {
name,
parentNames,
level,
description: groupDescription,
objects: recursiveHelper(object, group, description, validation, [], level + 1, parents,
objects: recursiveHelper(object, group, description, validation, converters, [], level + 1, parents,
parentNames.concat(name), true),
children: recursiveHelper(object, group, description, validation, [], level + 1, parents,
children: recursiveHelper(object, group, description, validation, converters, [], level + 1, parents,
parentNames.concat(name))
};
};

const manageLeaf = (object, name, description, validation = defaultValidation, parents) => {
const manageLeaf = (object, name, description, validation = defaultValidation, converter, parents) => {
const { type = 'Unknown', required = false } = isFunction(validation) ?
validation(null, true) :
({type: validation.toString(), req: false });

const cli = toCliOption(parents);
return {
name,
description,
type,
required,
cli,
path: parents.join('.'),
cli: toCliOption(parents),
defaultValue: object,
validator: validation
validator: validation,
converter: converter || automaticConverter(object, cli)
};
};

function recursiveHelper(object, groups = {}, descriptions = {}, validations = {}, filter = [],
function recursiveHelper(object, groups = {}, descriptions = {}, validations = {}, converters = {}, filter = [],
level = 0, initalParents = [], parentNames = [], leaves = false) {
return allObjects(object, (key) => {
// Make sure that we either have no filter or that there is a match
Expand All @@ -60,16 +64,24 @@ export default function buildDocumentationObject(initalObject, meta = {}, inital
const value = object[key];
if (isPlainObject(value) && Object.keys(value).length > 0 && !leaves) {
const group = isPlainObject(groups) ? groups[key] : {};
return manageGroup(value, key, group, descriptions[key], validations[key], parents, level,
parentNames);
return manageGroup(value, key, group, descriptions[key], validations[key], converters[key],
parents, level, parentNames);
} else if ((!isPlainObject(value) || Object.keys(value).length === 0) && leaves) {
return manageLeaf(value, key, descriptions[key], validations[key], parents);
return manageLeaf(value, key, descriptions[key], validations[key], converters[key], parents);
}
}
});
}

return recursiveHelper(initalObject, meta.groups, meta.descriptions, meta.validations, initalFilter, initalLevel);
return recursiveHelper(
initalObject,
meta.groups,
meta.descriptions,
meta.validations,
meta.converters,
initalFilter,
initalLevel
);
}

/**
Expand Down
9 changes: 5 additions & 4 deletions src/documentation/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ export function addPadding(string, length) {
*/
export function toCliOption(configPaths) {
// Runtime should be added directly
if (configPaths[0] === 'runtime') {
configPaths.shift();
}
return '--' + configPaths.join('-');
const paths = configPaths[0] === 'runtime' ?
configPaths.slice(1, configPaths.length) :
configPaths;

return '--' + paths.join('-');
}

/**
Expand Down
38 changes: 0 additions & 38 deletions test/cli/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ General options:
));
});
});

describe('getMappings', () => {
it('should create correct number of mappings', () => {
const mappings = getMappings(complexDocumentObject);
Expand All @@ -272,43 +271,6 @@ General options:
expect(mappings['build-path'].name).toBe('--build-path');
expect(mappings['build-path'].path).toBe('build.path');
});

describe('should create correct coverters', () => {
it('boolean', () => {
const mappings = getMappings(complexDocumentObject);
return consoleMockWrapper((log) => {
expect(mappings['build-useDefaultReduxMiddlewares'].converter(true)).toBe(true);
expect(mappings['build-useDefaultReduxMiddlewares'].converter(false)).toBe(false);

expect(mappings['build-useDefaultReduxMiddlewares'].converter('true')).toBe(true);
expect(mappings['build-useDefaultReduxMiddlewares'].converter('false')).toBe(false);

expect(mappings['build-useDefaultReduxMiddlewares'].converter('asd')).toBe(true);
expect(log.calls[0].arguments[0]).toInclude('Invalid value given');
});
});

it('array', () => {
const mappings = getMappings(complexDocumentObject);
expect(mappings['build-assets'].converter('[1, 2, 3]')).toEqual([1, 2, 3]);
expect(mappings['build-assets'].converter('1,2,3')).toEqual(['1', '2', '3']);
});

it('number', () => {
const mappings = getMappings(complexDocumentObject);
expect(mappings['build-port'].converter('1234')).toEqual(1234);
});

it('object', () => {
const mappings = getMappings(complexDocumentObject);
expect(mappings['build-obj'].converter('{"value": 12}')).toEqual({ value: 12 });
});

it('string', () => {
const mappings = getMappings(complexDocumentObject);
expect(mappings['build-outputName'].converter('some string')).toEqual('some string');
});
});
});
});
});
Loading

0 comments on commit ad26a10

Please sign in to comment.