Skip to content

Commit

Permalink
feat(schema): improve enum validation
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Sep 20, 2019
1 parent 5ff72b5 commit 98c5212
Show file tree
Hide file tree
Showing 43 changed files with 1,855 additions and 11 deletions.
13 changes: 13 additions & 0 deletions .yalc/better-ajv-errors/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# better-ajv-errors

## 0.6.6

### Patch Changes

- 84517c3: Fix a bug where enum error shows duplicate allowed values

## 0.6.5

### Patch Changes

- f2e0424: Fix a bug where nested errors were ignored when top level had enum errors
13 changes: 13 additions & 0 deletions .yalc/better-ajv-errors/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2018 Atlassian Pty Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
107 changes: 107 additions & 0 deletions .yalc/better-ajv-errors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<h1 align="center">
<img width="570" src="media/logo.png" alt="better-ajv-errors">
<br>
</h1>

> JSON Schema validation for Human 👨‍🎤
Main goal of this library is to provide relevant error messages like the following:

<p align="center">
<img src="media/screenshot.svg">
</p>

## Installation

```bash
$ yarn add better-ajv-errors
$ # Or
$ npm i better-ajv-errors
```

Also make sure that you installed [ajv](https://www.npmjs.com/package/ajv) package to validate data against JSON schemas.

## Usage

First, you need to validate your payload with `ajv`. If it's invalid then you can pass `validate.errors` object into `better-ajv-errors`.

```js
import Ajv from 'ajv';
import betterAjvErrors from 'better-ajv-errors';
// const Ajv = require('ajv');
// const betterAjvErrors = require('better-ajv-errors');

// You need to pass `jsonPointers: true`
const ajv = new Ajv({ jsonPointers: true });

// Load schema and data
const schema = ...;
const data = ...;

const validate = ajv.compile(schema);
const valid = validate(data);

if (!valid) {
const output = betterAjvErrors(schema, data, validate.errors);
console.log(output);
}
```

## API

### betterAjvErrors(schema, data, errors, [options])

Returns formatted validation error to **print** in `console`. See [`options.format`](#format) for further details.

#### schema

Type: `Object`

The JSON Schema you used for validation with `ajv`

#### data

Type: `Object`

The JSON payload you validate against using `ajv`

#### errors

Type: `Array`

Array of [ajv validation errors](https://github.com/epoberezkin/ajv#validation-errors)

#### options

Type: `Object`

##### format

Type: `string`
Default: `cli`
Values: `cli` `js`

Use default `cli` output format if you want to **print** beautiful validation errors like following:

<img width="620" src="media/screenshot.svg">

Or, use `js` if you are planning to use this with some API. Your output will look like following:

```javascript
[
{
start: { line: 6, column: 15, offset: 70 },
end: { line: 6, column: 26, offset: 81 },
error:
'/content/0/type should be equal to one of the allowed values: panel, paragraph, ...',
suggestion: 'Did you mean paragraph?',
},
];
```

##### indent

Type: `number` `null`
Default: `null`

If you have an unindented JSON payload and you want the error output indented
18 changes: 18 additions & 0 deletions .yalc/better-ajv-errors/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

var ver = process.versions.node;
var majorVer = parseInt(ver.split('.')[0], 10);

if (majorVer < 4) {
// eslint-disable-next-line no-console
console.error(
'Node version ' +
ver +
' is not supported, please use Node.js 4.0 or higher.'
);
process.exit(1);
} else if (majorVer < 8) {
module.exports = require('./lib/legacy');
} else {
module.exports = require('./lib/modern');
}
147 changes: 147 additions & 0 deletions .yalc/better-ajv-errors/lib/legacy/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"use strict";

require("core-js/modules/es.array.concat");

require("core-js/modules/es.array.filter");

require("core-js/modules/es.array.iterator");

require("core-js/modules/es.array.map");

require("core-js/modules/es.object.assign");

require("core-js/modules/es.object.entries");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.set");

require("core-js/modules/es.string.match");

exports.__esModule = true;
exports.makeTree = makeTree;
exports.filterRedundantErrors = filterRedundantErrors;
exports.createErrorInstances = createErrorInstances;
exports.default = void 0;

var _utils = require("./utils");

var _validationErrors = require("./validation-errors");

var JSON_POINTERS_REGEX = /\/[\w_-]+(\/\d+)?/g; // Make a tree of errors from ajv errors array

function makeTree(ajvErrors) {
if (ajvErrors === void 0) {
ajvErrors = [];
}

var root = {
children: {}
};
ajvErrors.forEach(function (ajvError) {
var dataPath = ajvError.dataPath; // `dataPath === ''` is root

var paths = dataPath === '' ? [''] : dataPath.match(JSON_POINTERS_REGEX);
paths && paths.reduce(function (obj, path, i) {
obj.children[path] = obj.children[path] || {
children: {},
errors: []
};

if (i === paths.length - 1) {
obj.children[path].errors.push(ajvError);
}

return obj.children[path];
}, root);
});
return root;
}

function filterRedundantErrors(root, parent, key) {
/**
* If there is a `required` error then we can just skip everythig else.
* And, also `required` should have more priority than `anyOf`. @see #8
*/
(0, _utils.getErrors)(root).forEach(function (error) {
if ((0, _utils.isRequiredError)(error)) {
root.errors = [error];
root.children = {};
}
});
/**
* If there is an `anyOf` error that means we have more meaningful errors
* inside children. So we will just remove all errors from this level.
*
* If there are no children, then we don't delete the errors since we should
* have at least one error to report.
*/

if ((0, _utils.getErrors)(root).some(_utils.isAnyOfError)) {
if (Object.keys(root.children).length > 0) {
delete root.errors;
}
}
/**
* If all errors are `enum` and siblings have any error then we can safely
* ignore the node.
*
* **CAUTION**
* Need explicit `root.errors` check because `[].every(fn) === true`
* https://en.wikipedia.org/wiki/Vacuous_truth#Vacuous_truths_in_mathematics
*/


if (root.errors && root.errors.length && (0, _utils.getErrors)(root).every(_utils.isEnumError)) {
if ((0, _utils.getSiblings)(parent)(root) // Remove any reference which becomes `undefined` later
.filter(_utils.notUndefined).some(_utils.getErrors)) {
delete parent.children[key];
}
}

Object.entries(root.children).forEach(function (_ref) {
var key = _ref[0],
child = _ref[1];
return filterRedundantErrors(child, root, key);
});
}

function createErrorInstances(root, options) {
var errors = (0, _utils.getErrors)(root);

if (errors.length && errors.every(_utils.isEnumError)) {
var uniqueValues = new Set((0, _utils.concatAll)([])(errors.map(function (e) {
return e.params.allowedValues;
})));
var allowedValues = [].concat(uniqueValues);
var error = errors[0];
return [new _validationErrors.EnumValidationError(Object.assign({}, error, {
params: {
allowedValues
}
}), options)];
} else {
return (0, _utils.concatAll)(errors.reduce(function (ret, error) {
switch (error.keyword) {
case 'additionalProperties':
return ret.concat(new _validationErrors.AdditionalPropValidationError(error, options));

case 'required':
return ret.concat(new _validationErrors.RequiredValidationError(error, options));

default:
return ret.concat(new _validationErrors.DefaultValidationError(error, options));
}
}, []))((0, _utils.getChildren)(root).map(function (child) {
return createErrorInstances(child, options);
}));
}
}

var _default = function _default(ajvErrors, options) {
var tree = makeTree(ajvErrors || []);
filterRedundantErrors(tree);
return createErrorInstances(tree, options);
};

exports.default = _default;
52 changes: 52 additions & 0 deletions .yalc/better-ajv-errors/lib/legacy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

require("core-js/modules/es.array.map");

exports.__esModule = true;
exports.default = void 0;

var _jsonToAst = _interopRequireDefault(require("json-to-ast"));

var _helpers = _interopRequireDefault(require("./helpers"));

var _default = function _default(schema, data, errors, options) {
if (options === void 0) {
options = {};
}

var _options = options,
_options$format = _options.format,
format = _options$format === void 0 ? 'cli' : _options$format,
_options$indent = _options.indent,
indent = _options$indent === void 0 ? null : _options$indent;
var jsonRaw = JSON.stringify(data, null, indent);
var jsonAst = (0, _jsonToAst.default)(jsonRaw, {
loc: true
});

var customErrorToText = function customErrorToText(error) {
return error.print().join('\n');
};

var customErrorToStructure = function customErrorToStructure(error) {
return error.getError();
};

var customErrors = (0, _helpers.default)(errors, {
data,
schema,
jsonAst,
jsonRaw
});

if (format === 'cli') {
return customErrors.map(customErrorToText).join('\n\n');
} else {
return customErrors.map(customErrorToStructure);
}
};

exports.default = _default;
module.exports = exports.default;

0 comments on commit 98c5212

Please sign in to comment.