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

isShape validator and better documentation generation #183

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
49 changes: 49 additions & 0 deletions docs/Validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Roc offers validators that can be used to make sure values have the correct valu
* [isPath](#ispath)
* [isPromise](#ispromise)
* [isRegExp](#isregexp)
* [isShape](#isshape)
* [isString](#isstring)
* [notEmpty](#notempty)
* [oneOf](#oneof)
Expand Down Expand Up @@ -115,6 +116,11 @@ import { isArray, isBoolean } from 'roc/validators';
isArray(isBoolean) => validator
```

__Documentation__
Will use a syntax in the documentation generation when describing wrapped validators. `[TYPE]` means that the type is optional, `<TYPE>` means that it is required. A question mark in front of the type, `?TYPE`, means that it can be empty.

`Array(<String>)` means that `null` and `undefined` are allowed as values but not empty strings as an example.

### `isBoolean`
```javascript
import { isBoolean } from 'roc/validators';
Expand Down Expand Up @@ -167,6 +173,11 @@ import { isObject, isBoolean } from 'roc/validators';
isObject(isBoolean, { unmanaged: false }) => validator
```

__Documentation__
Will use a syntax in the documentation generation when describing wrapped validators. `[TYPE]` means that the type is optional, `<TYPE>` means that it is required. A question mark in front of the type, `?TYPE`, means that it can be empty.

`Object(<String>)` means that null and undefined are allowed as values but not empty strings as an example.

### `isPath`
```javascript
import { isPath } from 'roc/validators';
Expand All @@ -191,6 +202,39 @@ Will validate the input to make sure it’s a regular expression.

`null` and `undefined` are valid.

### `isShape`
```javascript
import { isShape } from 'roc/validators';

isShape(validator, options) => validator
```
Will validate the input to make sure it’s an object matching the defined shape. Possible to provide an optional options object.

`null` and `undefined` are valid.

__`options`__
```
strict Defaults to true, set to false to allow non-validated properties.
```

__Example__
```javascript
import { isShape, isBoolean } from 'roc/validators';

// { a: true } : valid
// { a: true, b: 1 } : not valid
isShape({ a: isBoolean }) => validator

// { a: true } : valid
// { a: true, b: 1 } : valid
isShape({ a: isBoolean }, { strict: false }) => validator
```

__Documentation__
Will use a syntax in the documentation generation when describing wrapped validators. `[TYPE]` means that the type is optional, `<TYPE>` means that it is required. A question mark infront of the type, `?TYPE`, means that it can be empty.

`{ a: <String> }` means that `null` and `undefined` are allowed on a, but not empty strings as an example.

### `isString`
```javascript
import { isString } from 'roc/validators';
Expand Down Expand Up @@ -224,6 +268,11 @@ import { oneOf, isString, isArray } from 'roc/validators';
oneOf(isString, isArray(isString)) => validator
```

__Documentation__
Will use a syntax in the documentation generation when describing wrapped validators. `[TYPE]` means that the type is optional, `<TYPE>` means that it is required. A question mark in front of the type, `?TYPE`, means that it can be empty.

`<?String> / <Boolean>` means that the value can either be a empty string or a boolean as an example.

### `required`
```javascript
import { required } from 'roc/validators';
Expand Down
18 changes: 16 additions & 2 deletions src/hooks/runHookDirectly.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { magenta, underline } from 'chalk';
import { isPlainObject } from 'lodash';

import log from '../log/default';
import isValid from '../validation/helpers/isValid';
Expand Down Expand Up @@ -97,8 +98,7 @@ export default function runHookDirectly({
try {
throwValidationError(
`action in ${actionExtensionName} for ${name}`,
validationResult,
previousValue,
...manageResult(validationResult, previousValue),
'return value of'
);
} catch (err) {
Expand Down Expand Up @@ -144,3 +144,17 @@ export default function runHookDirectly({

return previousValue;
}

function manageResult(validationResult, previousValue) {
if (isPlainObject(validationResult)) {
return [
`${validationResult.message}${validationResult.key ? ` [In property ${validationResult.key}]` : ''}`,
validationResult.value || previousValue,
];
}

return [
validationResult,
previousValue,
];
}
2 changes: 1 addition & 1 deletion src/validation/helpers/createInfoObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function createInfoObject({
unmanagedObject = false,
} = {}) {
const info = isFunction(validator) ? validator(null, true) : { type: validator.toString(), canBeEmpty: null };
const type = wrapper ? wrapper(info.type) : info.type;
const type = wrapper ? wrapper(info.type, info.canBeEmpty, info.required || false) : info.type;
const convert = converter ? converter(info.converter) : info.converter;
return {
type,
Expand Down
7 changes: 7 additions & 0 deletions src/validation/helpers/writeInfoInline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Writes information about if a type is required and if it can be empty inline
*/
export default function writeInfoInline(type, canBeEmpty, required) {
const empty = canBeEmpty ? '?' : '';
return type && `${required ? `<${empty}${type}>` : `[${empty}${type}]`}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to force this to always return a string? Right now it'll return a falsy value or a string

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that type will only ever be the empty string, and because of that we will return the empty string. Could be discussed if it would be better to not have such implicit assumptions.

}
2 changes: 1 addition & 1 deletion src/validation/validateSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function assertValid(value, key, validator, allowRequiredFailure = false) {
function processResult(key, result, value) {
if (isPlainObject(result)) {
return [
`${key}${result.key}`,
`${key}.${result.key}`,
result.message,
result.value,
];
Expand Down
1 change: 1 addition & 0 deletions src/validation/validators/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export isObject from './isObject';
export isPath from './isPath';
export isPromise from './isPromise';
export isRegExp from './isRegExp';
export isShape from './isShape';
export isString from './isString';
export notEmpty from './notEmpty';
export oneOf from './oneOf';
Expand Down
3 changes: 2 additions & 1 deletion src/validation/validators/isArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isArray as isArrayLodash, isPlainObject } from 'lodash';
import createInfoObject from '../helpers/createInfoObject';
import isValid from '../helpers/isValid';
import toArray from '../../converters/toArray';
import writeInfoInline from '../helpers/writeInfoInline';

/**
* Validates an array using a validator.
Expand All @@ -16,7 +17,7 @@ export default function isArray(validator) {
return createInfoObject({
validator,
converter: (converter) => toArray(converter),
wrapper: (wrap) => `Array(${wrap})`,
wrapper: (...args) => `Array(${writeInfoInline(...args)})`,
canBeEmpty: true,
});
}
Expand Down
7 changes: 4 additions & 3 deletions src/validation/validators/isObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isPlainObject } from 'lodash';
import createInfoObject from '../helpers/createInfoObject';
import isValid from '../helpers/isValid';
import toObject from '../../converters/toObject';
import writeInfoInline from '../helpers/writeInfoInline';

/**
* Validates an object using a validator.
Expand All @@ -20,7 +21,7 @@ export default function isObject(...args) {
return createInfoObject({
validator,
converter: () => toObject,
wrapper: (wrap) => `Object(${wrap})`,
wrapper: (...wrapperArgs) => `Object(${writeInfoInline(...wrapperArgs)})`,
canBeEmpty: true,
unmanagedObject: unmanaged,
});
Expand All @@ -43,13 +44,13 @@ export default function isObject(...args) {
if (result !== true) {
if (isPlainObject(result)) {
return {
key: `.${key}${result.key}`,
key: `${key}.${result.key}`,
value: result.value,
message: result.message,
};
}
return {
key: `.${key}`,
key: `${key}`,
value: input[key],
message: result,
};
Expand Down
74 changes: 74 additions & 0 deletions src/validation/validators/isShape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { isPlainObject, difference } from 'lodash';

import createInfoObject from '../helpers/createInfoObject';
import getSuggestions from '../../helpers/getSuggestions';
import isValid from '../helpers/isValid';
import toObject from '../../converters/toObject';
import writeInfoInline from '../helpers/writeInfoInline';

export default function isShape(shape, { strict = true } = {}) {
if (!shape || !isPlainObject(shape) || Object.keys(shape).length === 0) {
throw new Error('The isShape validator requires that a shape object is defined.');
}

return (input, info) => {
const keys = Object.keys(shape);

if (info) {
const types = Object.keys(shape).map((key) => createInfoObject({
validator: shape[key],
wrapper: (...args) => `${key}: ${writeInfoInline(...args)}`,
}).type).join(', ');

// We do not need a speical converter, since there will never be the case that we
// have a input that is not on the ordinary object form, meaning that we will accept
// whatever we get back from it.
// In addtion to this we should not use this validator for something we get on the
// command line since we have better ways to manage shape like object there.
return createInfoObject({
validator: types,
converter: () => toObject,
wrapper: (wrap) => `{ ${wrap} }`,
canBeEmpty: true,
});
}

if (input === undefined || input === null) {
return true;
}

if (!isPlainObject(input)) {
return 'Was not an object and can therefore not have a shape!';
}


for (const key of keys) {
const result = isValid(input[key], shape[key]);

if (result !== true) {
if (isPlainObject(result)) {
return {
key: `${key}.${result.key}`,
value: result.value,
message: result.message,
};
}
return {
key: `${key}`,
value: input[key],
message: result,
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be any way to create a composed validation error of all properties that failed to validate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to both fail early and to not overwhelm the user with error messages.

}
}

if (strict) {
const diff = difference(Object.keys(input), keys);

if (diff.length > 0) {
return `Unknown propertys where found, make sure this is correct.\n${getSuggestions(diff, keys)}`;
}
}

return true;
};
}
6 changes: 5 additions & 1 deletion src/validation/validators/oneOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import convert from '../../converters/convert';
import createInfoObject from '../helpers/createInfoObject';
import getInfoObject from '../helpers/getInfoObject';
import isValid from '../helpers/isValid';
import writeInfoInline from '../helpers/writeInfoInline';

/**
* Validates against a list of validators.
Expand All @@ -19,7 +20,10 @@ export default function oneOf(...validators) {
const types = [];
const converters = [];
for (const validator of validators) {
const result = createInfoObject({ validator });
const result = createInfoObject({
validator,
wrapper: writeInfoInline,
});
types.push(result.type);
if (result.converter) {
converters.push(result.converter);
Expand Down
2 changes: 1 addition & 1 deletion test/validation/validators/isArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('validators', () => {

expect(isArray(validator)(null, true))
.toEqual({
type: 'Array(Type)',
type: 'Array([Type])',
required: false,
canBeEmpty: true,
converter: toArray(),
Expand Down
2 changes: 1 addition & 1 deletion test/validation/validators/isObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('validators', () => {

expect(isObject(validator)(null, true))
.toEqual({
type: 'Object(Type)',
type: 'Object([?Type])',
required: false,
canBeEmpty: true,
converter: toObject,
Expand Down
Loading