-
-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(schema): improve enum validation
- Loading branch information
Showing
43 changed files
with
1,855 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.