Skip to content

Commit

Permalink
Allow mutilple valid types
Browse files Browse the repository at this point in the history
  • Loading branch information
Jamie Treworgy committed Oct 18, 2018
1 parent 7d5de68 commit 5c25668
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- `[jest-runtime]` If `require` fails without a file extension, print all files that match with one ([#7160](https://github.com/facebook/jest/pull/7160))
- `[jest-haste-map]` Make `ignorePattern` optional ([#7166](https://github.com/facebook/jest/pull/7166))
- `[jest-runtime]` Remove `cacheDirectory` from `ignorePattern` for `HasteMap` if not necessary ([#7166](https://github.com/facebook/jest/pull/7166))
- `[jest-validate]` Add syntax to validate multiple permitted types

### Fixes

Expand Down
45 changes: 45 additions & 0 deletions packages/jest-validate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ Almost anything can be overwritten to suite your needs.

You will find examples of `condition`, `deprecate`, `error`, `unknown`, and `deprecatedConfig` inside source of this repository, named respectively.

## exampleConfig syntax

`exampleConfig` should be an object with key/value pairs that contain an example of a valid value for each key. A configuration value is considered valid when:

- it matches the JavaScript type of the example value, e.g. `string`, `number`, `array`, `boolean`, `function`, or `object`
- it is `null` or `undefined`
- the example value is an array where the first element is the special value `MultipleValidOptions`, and the value matches the type of _any_ of the other values in the array

The last condition is a special syntax that allows validating where more than one type is permissible; see example below. It's acceptable to have multiple values of the same type in the example, so you can also use this syntax to provide more than one example. When a validation failure occurs, the error message will show all other values in the array as examples.

## Examples

Minimal example:
Expand Down Expand Up @@ -128,6 +138,41 @@ This will output:
Documentation: http://custom-docs.com
```

## Example validating multiple types

```js
import {MultipleValidOptions} from 'jest-validate';

validate(config, {
// `bar` will accept either a string or a number
bar: [MultipleValidOptions, 'string is ok', 2],
});
```

#### Error:

```bash
● Validation Error:

Option foo must be of type:
string or number
but instead received:
array

Example:
{
"bar": "string is ok"
}

or

{
"bar": 2
}

Documentation: http://custom-docs.com
```

#### Deprecation

Based on `deprecatedConfig` object with proper deprecation messages. Note custom title:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Repeated types within multiple valid examples are coalesced in error report 1`] = `
"<red><bold><bold>●<bold> Validation Error</>:</>
<red></>
<red> Option <bold>\\"foo\\"</> must be of type:</>
<red> <bold><green>string or number<red></></>
<red> but instead received:</>
<red> <bold><red>boolean<red></></>
<red></>
<red> Example:</>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>\\"foo\\"</></>
<red> }</>
<red></>
<red> or</>
<red></>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>\\"bar\\"</></>
<red> }</>
<red></>
<red> or</>
<red></>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>2</></>
<red> }</>
<red></>"
`;

exports[`displays warning for deprecated config options 1`] = `
"<yellow><bold><bold>●<bold> Deprecation Warning</>:</>
<yellow></>
Expand Down Expand Up @@ -107,6 +134,29 @@ exports[`pretty prints valid config for String 1`] = `
<red></>"
`;

exports[`reports errors nicely when failing with multiple valid options 1`] = `
"<red><bold><bold>●<bold> Validation Error</>:</>
<red></>
<red> Option <bold>\\"foo\\"</> must be of type:</>
<red> <bold><green>string or array<red></></>
<red> but instead received:</>
<red> <bold><red>number<red></></>
<red></>
<red> Example:</>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>\\"text\\"</></>
<red> }</>
<red></>
<red> or</>
<red></>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>[</></>
<red><bold> \\"text\\"</></>
<red><bold> ]</></>
<red> }</>
<red></>"
`;

exports[`works with custom deprecations 1`] = `
"<yellow><bold>My Custom Deprecation Warning</>:</>
<yellow></>
Expand Down
60 changes: 60 additions & 0 deletions packages/jest-validate/src/__tests__/validate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'use strict';

import validate from '../validate';
import {MultipleValidOptions} from '../condition';
import jestValidateExampleConfig from '../example_config';
import jestValidateDefaultConfig from '../default_config';

Expand Down Expand Up @@ -206,3 +207,62 @@ test('works with custom deprecations', () => {
expect(console.warn.mock.calls[0][0]).toMatchSnapshot();
console.warn = warn;
});

test('works with multiple valid types', () => {
const exampleConfig = {
foo: [MultipleValidOptions, 'text', ['text']],
};

expect(
validate(
{foo: 'foo'},
{
exampleConfig,
},
),
).toEqual({
hasDeprecationWarnings: false,
isValid: true,
});
expect(
validate(
{foo: ['foo']},
{
exampleConfig,
},
),
).toEqual({
hasDeprecationWarnings: false,
isValid: true,
});
});

test('reports errors nicely when failing with multiple valid options', () => {
const exampleConfig = {
foo: [MultipleValidOptions, 'text', ['text']],
};

expect(() =>
validate(
{foo: 2},
{
exampleConfig,
},
),
).toThrowErrorMatchingSnapshot();
});

test('Repeated types within multiple valid examples are coalesced in error report', () => {
const exampleConfig = {
foo: [MultipleValidOptions, 'foo', 'bar', 2],
};

expect(() =>
validate(
{foo: false},
{
exampleConfig,
},
),
).toThrowErrorMatchingSnapshot();
});
24 changes: 20 additions & 4 deletions packages/jest-validate/src/condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,29 @@

const toString = Object.prototype.toString;

export default function validationCondition(
option: any,
validOption: any,
): boolean {
export const MultipleValidOptions = Symbol('JEST_MULTIPLE_VALID_OPTIONS');

function validationConditionSingle(option: any, validOption: any): boolean {
return (
option === null ||
option === undefined ||
toString.call(option) === toString.call(validOption)
);
}

export function getConditions(validOption: any) {
if (
Array.isArray(validOption) &&
validOption.length &&
validOption[0] === MultipleValidOptions
) {
return validOption.slice(1);
}
return [validOption];
}

export function validationCondition(option: any, validOption: any): boolean {
return getConditions(validOption).some(e =>
validationConditionSingle(option, e),
);
}
2 changes: 1 addition & 1 deletion packages/jest-validate/src/default_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {ValidationOptions} from './types';
import {deprecationWarning} from './deprecated';
import {unknownOptionWarning} from './warnings';
import {errorMessage} from './errors';
import validationCondition from './condition';
import {validationCondition} from './condition';
import {ERROR, DEPRECATION, WARNING} from './utils';

export default ({
Expand Down
37 changes: 31 additions & 6 deletions packages/jest-validate/src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {ValidationOptions} from './types';
import chalk from 'chalk';
import getType from 'jest-get-type';
import {formatPrettyObject, ValidationError, ERROR} from './utils';
import {getConditions} from './condition';

export const errorMessage = (
option: string,
Expand All @@ -20,22 +21,46 @@ export const errorMessage = (
options: ValidationOptions,
path?: Array<string>,
): void => {
const conditions = getConditions(defaultValue);
const validTypes = conditions
.map(getType)
.filter(uniqueFilter())
.join(' or ');

const message = ` Option ${chalk.bold(
`"${path && path.length > 0 ? path.join('.') + '.' : ''}${option}"`,
)} must be of type:
${chalk.bold.green(getType(defaultValue))}
${chalk.bold.green(validTypes)}
but instead received:
${chalk.bold.red(getType(received))}
Example:
{
${chalk.bold(`"${option}"`)}: ${chalk.bold(
formatPrettyObject(defaultValue),
)}
}`;
${formatExamples(option, conditions)}`;

const comment = options.comment;
const name = (options.title && options.title.error) || ERROR;

throw new ValidationError(name, message, comment);
};

function formatExamples(option: string, examples: Array<any>) {
return examples.map(
e => ` {
${chalk.bold(`"${option}"`)}: ${chalk.bold(formatPrettyObject(e))}
}`,
).join(`
or
`);
}

function uniqueFilter() {
const seen: {[string]: any} = {};
return function(key) {
if (seen[key]) {
return false;
}
return (seen[key] = true);
};
}
2 changes: 2 additions & 0 deletions packages/jest-validate/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
} from './utils';
import validate from './validate';
import validateCLIOptions from './validate_cli_options';
import {MultipleValidOptions} from './condition';

module.exports = {
MultipleValidOptions,
ValidationError,
createDidYouMeanMessage,
format,
Expand Down

0 comments on commit 5c25668

Please sign in to comment.