initial release

This commit is contained in:
knidarkness
2019-10-16 23:42:33 +03:00
commit 7f5fa0f9ac
130 changed files with 32903 additions and 0 deletions

11
.babelrc Normal file
View File

@@ -0,0 +1,11 @@
{
"presets": [
["@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
__tests__

19
.eslintrc.yml Normal file
View File

@@ -0,0 +1,19 @@
env:
browser: true
es6: true
node: true
jest: true
extends:
- airbnb-base
globals:
Atomics: readonly
SharedArrayBuffer: readonly
parserOptions:
ecmaVersion: 2018
sourceType: module
rules:
class-methods-use-this: off
global-require: off
import/no-dynamic-require: off
no-plusplus: off

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
node_modules/
coverage/
.vscode/
.eslintcache
test/specs/openapi/rebilly-full.yaml
test/specs/openapi/rebilly-full (1).yaml
yarn.lock
package-lock.json
dist/
test/

9
.npmignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules/
src/
media/
coverage/
test/
dist/__tests__
dist/error/__tests__
RULES.md
README.md

10
.travis.yml Normal file
View File

@@ -0,0 +1,10 @@
language: node_js
node_js:
- "10"
cache:
directories:
- "node_modules"
jobs:
include:
- stage: test
script: npm run test

82
README.md Normal file
View File

@@ -0,0 +1,82 @@
# REVALID. Custom OpenAPI validator
## Approach
Unline other OpenAPI validators Revalid defines the possible type tree of a valid OpenAPI document and then traverses input validating it. This approach is very similar to how compilers work and gives major performance benefits over other approaches. Also, it allows to easily add custom or quite complex rules.
## Features
![Revalid output screenshot](/media/screenshot-output.png)
As for now, Revalid supports such features:
- [x] Multifile validation. No need to bundle your file before using validator.
- [x] Configurable message levels for each rule. You can tailor your experience with Revalid as you wish.
- [x] Lightning fast validation. Check 1 Mb file less than in one second.
- [x] Human readable error messages. Now with stacktrace and codeframes.
- [x] Intuitive suggestions for misspelled type names or references.
- [x] Easy to implement custom rules. Need something? Ask us or do it yourself.
All of them are also provided inside our [VS Code extension](https://redoc.ly).
### Configuration
You can enable/disable separate rules as well as set their priority level using the `revalid.config.json` file which should be in the directory from which you run Revalid.
Example of the configuration file is quite self-explanatory:
```json
{
"enableCodeframe": true,
"rules": {
"no-extra-fields": "off",
"license": {
"url": "on"
},
"license-required": {
"level": "warning"
},
"unique-parameter-names": {
"level": "error",
},
"no-unused-schemas": "off"
}
}
```
More detailed guide about configuring the validator can be found [here](RULES.md).
## Contributing
### Custom validation rules
To create a custom validation rule you should only define a class which extends `AbstractRule`. Inside this class, you can define methods for each type of the OpenAPI Nodes, which returns two methods: `onEnter` and `onExit`. Also, you can use `any()` descriptor to call the rule for all the nodes in your document. These methods will receive four arguments:
- node
- type object
- context
- unresolved node (i.e. before $ref in the root of the node was jumped to)
The rule **must not** mutate any of these object as they are not immutable due to performance reasons.
So, the simplest rule example is following:
```js
import createError from '../error'; // later it will be a separete package: @revalid/rules
class ExampleRule extends AbstractRule {
static get ruleName() {
return 'exampleRule';
}
any() {
return {
onEnter: (node, definition, ctx, unresolvedNode) => {
if (node.name && node.name === 'badName') return createError('"badName" is invalid value for "name" field', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level});
}
}
}
}
export default ExampleRule;
```
Then, put the rule into your local copy of `extendedRules` directory or provide path to the directory with your rule set via `--custom-ruleset <path>` command line argument.

147
RULES.md Normal file
View File

@@ -0,0 +1,147 @@
# Revalid ruleset configuration
## Disabling and configuring rules
All of the following rules are configurable in terms of disabling or changing their severity. In order to update given rule, you should modify (or create) the `revalid.config.json` file in the directory from which you are going to run the Revalid.
If you are creating it from scratch, you might also want to enable/disable codeframe for the full output.
Below is the basic structure of this file:
```json
{
"enableCodeframe": true,
"rules": {
"no-extra-fields": "off",
"license": {
"url": "on"
},
"license-required": {
"level": "warning"
},
"unique-parameter-names": {
"level": "error",
},
"no-unused-schemas": "off"
}
}
```
## Ruleset overview
Below you can find a list of the all currently supported rules. To change your settings for given rule, just add or modify corresponding item in the `rules` section of the `revalid.config.json` in your working directory.
### api-servers
OpenAPI servers must be present and be a non-empty array.
### path-param-exists
Each path parameter in the `parameters` section must be present in the path string.
### license-url
License, if provided within `info` section must provide `url` field.
### no-unused-schemas
It might be a bad sign if some of the schemas from the `components` section are not used anywhere. This checks for such scenarios.
### operation-2xx-response
When designing an API it's usually expected to do something succesfully, although it might fail. So, this rule validates, that there is at least one response in the operation with a 2xx status code.
### operation-description
This rule enforces to provide `description` field in `operation`s as within large definition it becomes really easy to lose track of things.
### operation-operationId
Enforce presence of the `operationId` field in each `operation`.
### operation-operationId-unique
`operationId`s are expected to be unique to really identify operations. This rule checks this principle.
### operation-tags
The `tags` field must be present and be a non-empty array in each `operation`.
### path-declarations-must-exist
Within the `operation` path definition you can define path parametrs. If you do so, each declaration of the parameter name within path must be a non-null string. For example, `/api/user/{userId}/profie` is a valid definition with `userId` parameter, but `/api/user/{}/profile` is not.
### path-keys-no-trailing-slash
Generally, it is considered less confusing if you do not have trailing slashes in your paths. Also, it depends on tooling are `example.com/api/users` and `example.com/api/users/` are treated in the same way, so we suggest you to be consistent on this page.
### provide-contact
Info object must contain `contact` field.
Most of the APIs are not perfect, so there is something useful for your users to know, who can help in case of problems.
### servers-no-trailing-slash
Server URL must not have a trailing slash.
It depends on tooling are `example.com` and `example.com/` are treated in the same way. In the worst case, the latter option when conjuncted with operations paths migth result into `example.com//api/users`.
### unique-parameter-names
Parameters in `operation` objects must be `unique` definition wide.
### oas3-schema
This rule enforces the structural validation of the OpenAPI definitions according to the OpenAPI Specification 3.0.2. It can be fine-tuned to disable or change message level for each specific type of the OpenAPI Objects. For example, if you have custom structure of the `servers` object, what you can do to prevent error messages regarding it is to update your `revalid.config.json` to the following pattern:
```json
{
... your configuration
"rules": {
...other rules,
"oas3-schema": {
"servers": {
"level": "warning"
}
}
}
}
```
or even totally disable:
```json
{
... your configuration
"rules": {
...other rules,
"oas3-schema": {
"servers": "off"
}
}
}
```
Below, you can find the table of available sub-rules you can update:
| Sub-rule name | OpenAPI Object it corresponds to|
|---|---|
| root | [OpenAPI Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oasObject) |
| info | [Open API Info Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject) |
| contact | [Open API Contact Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#contactObject) |
| discriminator | [Open API Discriminator Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject) |
| encoding | [Open API Encoding Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#encodingObject) |
| example | [OpenAPI Example Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject) |
| documentation | [OpenAPI External Documentation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#externalDocumentationObject) |
| header | [OpenAPI Header Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject) |
| license | [OpenAPI License Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#licenseObject) |
| link | [OpenAPI Link Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#linkObject) |
| media-object | [OpenAPI Media Type Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#mediaTypeObject) |
| operation | [OpenAPI Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject) |
| parameter | [OpenAPI Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject) |
| path | [OpenAPI Path Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject) |
| request-body | [OpenAPI Request Body Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject) |
| response | [OpenAPI Response Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject) |
| schema | [OpenAPI Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) |
| secuirty-schema | [OpenAPI Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject)|
| auth-code-flow | [Open API Flow Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowObject)|
| client-creds-flow | [Open API Flow Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowObject)|
| implicit-flow | [Open API Flow Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowObject)|
| password-flow | [Open API Flow Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowObject)|
| server | [OpenAPI Server Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject) |
| server-variable | [OpenAPI Server Variable Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverVariableObject) |
| tag | [OpenAPI Tag Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#tagObject) |
| xml | [OpenAPI XML Obejct](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#xmlObject) |
#### no-extra-fields
By default, custom fields, not defined within OpenAPI specification can be included only using `x-` prefix. This rule enforces such policy.
## Linting rules
### suggest-possible-refs
It is not totally uncommon to have a bad `$ref` in your definition. For example, instead of `#components/schemas/User` one might type `#components/schemas/Use`.
With this rule enabled, Revalid will try to find the closest possible valid `$ref` address in the definition.

20
main.js Normal file
View File

@@ -0,0 +1,20 @@
/* eslint-disable import/no-named-as-default */
/* eslint-disable no-console */
// eslint-disable-next-line import/no-named-as-default-member
import validateFromFile from './src';
const test = (fNmae, name) => {
const start = Date.now();
const options = {
enableCodeframe: true,
enbaleCustomRuleset: true,
};
const results = validateFromFile(fNmae, options);
const end = Date.now();
console.log(results ? results.length : `good with ${name}`);
console.log(`Evaluation took: ${end - start} ms with ${name}`);
};
test('test/specs/openapi/with-file-ref.yaml', 'revalid');

BIN
media/screenshot-output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

54
package.json Normal file
View File

@@ -0,0 +1,54 @@
{
"name": "@redocly/openapi-cli",
"version": "0.1.15",
"description": "",
"main": "./dist/index.js",
"scripts": {
"profile": "npx babel-node --prof main.js",
"babel-node": "babel-node",
"build": "babel src --out-dir dist --source-maps inline && chmod +x ./dist/index.js",
"lint": "eslint ./src",
"prepublishOnly": "npm run build",
"postinstall": "chmod +x ./dist/index.js",
"start": "npx babel-node main.js",
"test": "jest --coverage"
},
"bin": {
"openapi-cli": "dist/index.js"
},
"jest": {
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/dist/"
]
},
"author": "Serhii Dubovyk",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.6.0",
"@babel/core": "^7.6.0",
"@babel/node": "^7.6.1",
"@babel/preset-env": "^7.6.0",
"babel-eslint": "^10.0.3",
"eslint": "^6.4.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.18.2",
"jest": "^24.9.0",
"nyc": "^14.1.1",
"prettier": "^1.18.2",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9"
},
"dependencies": {
"commander": "^3.0.1",
"js-yaml": "^3.13.1",
"jsonpath": "^1.0.2",
"merge-deep": "^3.0.2",
"yaml-ast-parser": "0.0.43"
}
}

198
src/__tests__/index.test.js Normal file
View File

@@ -0,0 +1,198 @@
import { validateFromFile } from "../index";
test("validate simple document", () => {
expect(validateFromFile("./test/specs/openapi/simple.yaml"))
.toMatchInlineSnapshot(`
Array [
Object {
"codeFrame": null,
"file": "./test/specs/openapi/simple.yaml",
"location": Object {
"endCol": 17,
"endIndex": 86,
"endLine": 6,
"startCol": 0,
"startIndex": 0,
"startLine": 1,
},
"message": "The field 'paths' must be present on this level.",
"path": "",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"info": Object {
"license": Object {
"name": "Test license",
},
"taitle": 123,
"version": "0.0.1",
},
"openapi": "3.0.1",
},
},
Object {
"codeFrame": null,
"file": "./test/specs/openapi/simple.yaml",
"location": Object {
"endCol": 9,
"endIndex": 29,
"endLine": 3,
"startCol": 3,
"startIndex": 23,
"startLine": 3,
},
"message": "The field 'taitle' is not allowed here. Use \\"x-\\" prefix to override this behavior.",
"path": "info/taitle",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"license": Object {
"name": "Test license",
},
"taitle": 123,
"version": "0.0.1",
},
},
Object {
"codeFrame": null,
"file": "./test/specs/openapi/simple.yaml",
"location": Object {
"endCol": 5,
"endIndex": 19,
"endLine": 2,
"startCol": 1,
"startIndex": 15,
"startLine": 2,
},
"message": "The field 'title' must be present on this level.",
"path": "info",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"license": Object {
"name": "Test license",
},
"taitle": 123,
"version": "0.0.1",
},
},
]
`);
});
test("Validate simple valid OpenAPI document", () => {
expect(validateFromFile("./test/specs/openapi/test-2.yaml"))
.toMatchInlineSnapshot(`
Array [
Object {
"codeFrame": null,
"file": "./test/specs/openapi/operations/test/operation-2.yaml",
"location": Object {
"endCol": 17,
"endIndex": 130,
"endLine": 7,
"startCol": 3,
"startIndex": 115,
"startLine": 7,
},
"message": "url must be a valid URL",
"path": "externalDocs/url",
"pathStack": Array [
"./test/specs/openapi/test-2.yaml:12 #/paths//user/{userId}/get",
],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"url": "googleacom",
},
},
Object {
"codeFrame": null,
"file": "./test/specs/openapi/operations/test/../../test-2.yaml",
"location": Object {
"endCol": 14,
"endIndex": 873,
"endLine": 43,
"startCol": 13,
"startIndex": 871,
"startLine": 43,
},
"message": "All values of \\"enum\\" field must be of the same type as the \\"type\\" field",
"path": "components/schemas/Pet/properties/status/enum/2",
"pathStack": Array [
"./test/specs/openapi/test-2.yaml:12 #/paths//user/{userId}/get",
"./test/specs/openapi/operations/test/operation-2.yaml:46 #/responses/200/content/application/json/schema",
"./test/specs/openapi/operations/test/../../test-2.yaml:72 #/components/schemas/user/properties/pet",
],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"description": "pet status in the store",
"enum": Array [
"available",
"pending",
12,
],
"type": "string",
},
},
Object {
"codeFrame": null,
"file": "./test/specs/openapi/operations/test/operation.yaml",
"location": Object {
"endCol": 15,
"endIndex": 128,
"endLine": 7,
"startCol": 3,
"startIndex": 115,
"startLine": 7,
},
"message": "url must be a valid URL",
"path": "externalDocs/url",
"pathStack": Array [
"./test/specs/openapi/test-2.yaml:10 #/paths//user/{userId}/post",
],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"url": "asdasdas",
},
},
Object {
"codeFrame": null,
"file": "./test/specs/openapi/test-2.yaml",
"location": Object {
"endCol": 14,
"endIndex": 873,
"endLine": 43,
"startCol": 13,
"startIndex": 871,
"startLine": 43,
},
"message": "All values of \\"enum\\" field must be of the same type as the \\"type\\" field",
"path": "components/schemas/Pet/properties/status/enum/2",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"description": "pet status in the store",
"enum": Array [
"available",
"pending",
12,
],
"type": "string",
},
},
]
`);
});
test("Validate from invalid file", () => {
expect(() => {
validateFromFile("./test/specs/openapi/test-invalid-1.yaml");
}).toThrowErrorMatchingInlineSnapshot('"Can\'t load yaml file"');
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
import fs from 'fs';
import traverse from '../traverse';
import { createErrrorFieldTypeMismatch } from '../error';
const getSource = () => fs.readFileSync('./test/specs/openapi/test-3.yaml', 'utf-8');
test('', () => {
const node = {
field: 12,
b: 12,
'x-allowed': true,
child: {
a: 'text',
},
};
const resolver = {
validators: {
field() {
return (targetNode, ctx) => (typeof node.field === 'string'
? null
: createErrrorFieldTypeMismatch('string', targetNode, ctx));
},
},
properties: {
child: {
validators: {
a() {
return () => null;
},
},
},
},
};
});

193
src/cli.js Normal file
View File

@@ -0,0 +1,193 @@
import program from 'commander';
import path from 'path';
import {
outputLightBlue,
outputBgRed,
outputGrey,
outputBgYellow,
outputRed,
outputBgLightBlue,
outputYellow,
outputUnderline,
} from './utils';
import { validateFromFile } from './validate';
import { messageLevels } from './error/default';
const colorizeMessageHeader = (msg, longestPath) => {
const msgHeader = `${path.relative(process.cwd(), msg.file)}:${msg.location.startLine}:${msg.location.startCol}`;
switch (msg.severity) {
case messageLevels.ERROR:
return outputBgRed(outputBgRed(msgHeader.padEnd(longestPath + 2 - 20)));
case messageLevels.WARNING:
return outputBgYellow(outputRed(msgHeader.padEnd(longestPath + 2 - 20)));
case messageLevels.INFO:
return outputBgLightBlue(outputRed(msgHeader.padEnd(longestPath + 2 - 20)));
default:
return msgHeader;
}
};
const colorizeRuleName = (error, severity) => {
switch (severity) {
case messageLevels.ERROR:
return outputRed(error);
case messageLevels.WARNING:
return outputYellow(error);
case messageLevels.INFO:
return outputBgLightBlue(error);
default:
return error;
}
};
const pathImproveReadability = (msgPath) => msgPath.map((el) => (el[0] === '/' ? outputGrey('[\'') + outputLightBlue(el) + outputGrey('\']') : outputGrey(el)));
const prettifyPathStackRow = (row) => `${outputLightBlue(`${row.file}:${row.startLine}`)} ${outputGrey(`#/${pathImproveReadability(row.path).join(outputGrey('/'))}`)}`;
const renderReferencedFrom = (pathStacks) => {
if (pathStacks.length === 0) return '';
return `This error is referenced from:\n${pathStacks.map((rows, id) => `${id + 1}) ${prettifyPathStackRow(rows.pop())}`).join('\n')}`;
};
const prettyPrint = (i, error) => {
const message = `[${i}] ${colorizeMessageHeader(error)} ${outputGrey(`at #/${pathImproveReadability(error.path).join(outputGrey('/'))}`)}`
+ `\n${error.message}\n`
+ `${error.possibleAlternate ? `\nDid you mean: ${outputLightBlue(error.possibleAlternate)} ?\n` : ''}`
+ `${error.enableCodeframe ? `\n${error.codeFrame}\n\n` : ''}`
+ `${renderReferencedFrom(error.pathStacks)}`
+ '\n\n';
return message;
};
const prettyPrintShort = (i, error, longestPath, longestRuleName) => {
const message = `${(`${error.location.startLine}:${error.location.startCol}`).padEnd(longestPath)} ${colorizeRuleName(error.fromRule.padEnd(longestRuleName + 2), error.severity)} ${error.message}\n`;
return message;
};
const errorBelongsToGroup = (error, group) => error.msg === group.msg
&& error.path.join('/') === group.path.join('/')
&& error.severity === group.severity
&& error.location.startIndex === group.location.startIndex
&& error.location.endIndex === group.location.endIndex
&& error.location.possibleAlternate === group.location.possibleAlternate;
const errorAlreadyInGroup = (error, group) => group.pathStacks.filter((stack) => JSON.stringify(stack) === JSON.stringify(error.pathStack)).length > 0;
const groupFromError = (error) => ({
message: error.message,
location: error.location,
path: error.path,
codeFrame: error.codeFrame,
value: error.value,
file: error.file,
severity: error.severity,
enableCodeframe: error.enableCodeframe,
target: error.target,
possibleAlternate: error.possibleAlternate,
fromRule: error.fromRule,
pathStacks: error.pathStack.length !== 0 ? [error.pathStack] : [],
});
const addErrorToGroup = (error, group) => {
if (error.pathStack.length !== 0 && !errorAlreadyInGroup(error, group)) {
group.pathStacks.push(error.pathStack);
}
return true;
};
const groupErrors = (errors) => {
const groups = [];
for (let i = 0; i < errors.length; i += 1) {
let assigned = false;
for (let j = 0; j < groups.length; j += 1) {
if (errorBelongsToGroup(errors[i], groups[j])) {
assigned = addErrorToGroup(errors[i], groups[j]);
break;
}
}
if (!assigned) groups.push(groupFromError(errors[i]));
}
return groups;
};
const groupByFiles = (result) => {
const fileGroups = {};
result.forEach((row) => {
if (fileGroups[row.file]) {
fileGroups[row.file].push(row);
} else {
fileGroups[row.file] = [row];
}
});
return fileGroups;
};
const cli = () => {
program
.command('validate <filePath>')
.description('Validate given Open API 3 definition file.')
.option('-s, --short', 'Reduce output to required minimun')
.option('-f, --no-frame', 'Print no codeframes with errors.')
.option('--config <path>', 'Specify custom yaml or json config')
.action((filePath, cmdObj) => {
const options = {};
options.enableCodeframe = cmdObj.frame;
if (cmdObj.config) options.configPath = cmdObj.config;
const result = validateFromFile(filePath, options);
const errorsGrouped = groupErrors(result);
const groupedByFile = groupByFiles(errorsGrouped);
const totalErrors = errorsGrouped.filter(
(msg) => msg.severity === messageLevels.ERROR,
).length;
const totalWarnings = errorsGrouped.filter(
(msg) => msg.severity === messageLevels.WARNING,
).length;
if (cmdObj.short && errorsGrouped.length !== 0) {
const posLength = errorsGrouped
.map((msg) => `${msg.location.startLine}:${msg.location.startCol}`)
.sort((e, o) => e.length > o.length)
.pop()
.length;
const longestRuleName = errorsGrouped
.map((msg) => msg.fromRule)
.sort((e, o) => e.length > o.length)
.pop()
.length;
Object.keys(groupedByFile).forEach((fileName) => {
process.stdout.write(`${outputUnderline(`${path.relative(process.cwd(), fileName)}:\n`)}`);
groupedByFile[fileName]
.sort((a, b) => a.severity < b.severity)
.forEach(
(entry, id) => process.stdout.write(
prettyPrintShort(id + 1, entry, posLength, longestRuleName),
),
);
process.stdout.write('\n');
});
} else {
process.stdout.write('\n\n');
errorsGrouped
.sort((a, b) => a.severity < b.severity)
.forEach((entry, id) => process.stdout.write(prettyPrint(id + 1, entry)));
}
process.stdout.write(`Total: errors: ${totalErrors}, warnings: ${totalWarnings}\n`);
process.exit(totalErrors ? -1 : 0);
});
if (process.argv.length === 2) process.argv.push('-h');
program.parse(process.argv);
};
export default cli;

23
src/config.js Normal file
View File

@@ -0,0 +1,23 @@
import fs from 'fs';
import merge from 'merge-deep';
import yaml from 'js-yaml';
function getConfig(options) {
let config = {};
let { configPath } = options;
if (!configPath) configPath = `${process.cwd()}/revalid.config.json`;
const defaultConfigRaw = fs.readFileSync(`${__dirname}/revalid.default.config.json`, 'utf-8');
const defaultConfig = yaml.safeLoad(defaultConfigRaw);
if (fs.existsSync(configPath)) {
const configRaw = fs.readFileSync(configPath, 'utf-8');
config = yaml.safeLoad(configRaw);
}
const resolvedConfig = merge(defaultConfig, config, options);
// console.log(resolvedConfig);
return resolvedConfig;
}
export default getConfig;

View File

@@ -0,0 +1,190 @@
import fs from "fs";
import yaml from "js-yaml";
import createError from "../default";
const getSource = () =>
fs.readFileSync("./test/specs/openapi/test-1.yaml", "utf-8");
const createContext = () => ({
document: yaml.safeLoad(getSource()),
path: ["paths", "/user/{userId}/{name}", "get", "parameters", 0, "required"],
pathStack: [],
source: getSource(),
enableCodeframe: true
});
describe("createError", () => {
test("", () => {
const ctx = createContext();
const node = { required: 123 };
const error = createError("test error msg", node, ctx);
expect(error).toMatchInlineSnapshot(`
Object {
"codeFrame": "16| 
17| - name: userId
18| in: path
19| required: true
20| description: Id of a user
21| schema:",
"file": undefined,
"location": Object {
"endCol": 24,
"endIndex": 343,
"endLine": 19,
"startCol": 11,
"startIndex": 329,
"startLine": 19,
},
"message": "test error msg",
"path": "paths/['/user/{userId}/{name}']/get/parameters/0/required",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"required": 123,
},
}
`);
});
test("create error with no codeframe", () => {
const ctx = createContext();
ctx.enableCodeframe = false;
const node = { required: 123 };
const error = createError("test error msg", node, ctx);
expect(error).toMatchInlineSnapshot(`
Object {
"codeFrame": null,
"file": undefined,
"location": Object {
"endCol": 24,
"endIndex": 343,
"endLine": 19,
"startCol": 11,
"startIndex": 329,
"startLine": 19,
},
"message": "test error msg",
"path": "paths/['/user/{userId}/{name}']/get/parameters/0/required",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"required": 123,
},
}
`);
});
test("pretty print error", () => {
const ctx = createContext();
const node = { required: 123 };
const error = createError("test error msg", node, ctx);
expect(error.prettyPrint()).toMatchInlineSnapshot(`
"undefined:19:11 at #/paths/['/user/{userId}/{name}']/get/parameters/0/required
test error msg
16| 
17| - name: userId
18| in: path
19| required: true
20| description: Id of a user
21| schema:
"
`);
});
test("pretty print error without codeframe", () => {
const ctx = createContext();
ctx.enableCodeframe = false;
const node = { required: 123 };
const error = createError("test error msg", node, ctx);
expect(error.prettyPrint()).toMatchInlineSnapshot(`
"undefined:19:11 at #/paths/['/user/{userId}/{name}']/get/parameters/0/required
test error msg
"
`);
});
test("create error with path stack", () => {
const ctx = createContext();
ctx.pathStack = [
{
path: ["paths"],
file: "test/specs/openapi/test-1.yaml"
}
];
const node = { required: 123 };
const error = createError("test error msg", node, ctx);
expect(error).toMatchInlineSnapshot(`
Object {
"codeFrame": "16| 
17| - name: userId
18| in: path
19| required: true
20| description: Id of a user
21| schema:",
"file": undefined,
"location": Object {
"endCol": 24,
"endIndex": 343,
"endLine": 19,
"startCol": 11,
"startIndex": 329,
"startLine": 19,
},
"message": "test error msg",
"path": "paths/['/user/{userId}/{name}']/get/parameters/0/required",
"pathStack": Array [
"test/specs/openapi/test-1.yaml:11 #/paths",
],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"required": 123,
},
}
`);
});
test("pretty print error with path stack", () => {
const ctx = createContext();
ctx.pathStack = [
{
path: [
"paths",
"/user/{userId}/{name}",
"get",
"parameters",
0,
"required"
],
file: "test/specs/openapi/test-1.yaml"
}
];
const node = { required: 123 };
const error = createError("test error msg", node, ctx);
expect(error.prettyPrint()).toMatchInlineSnapshot(`
"undefined:19:11 at #/paths/['/user/{userId}/{name}']/get/parameters/0/required
from test/specs/openapi/test-1.yaml:19 #/paths//user/{userId}/{name}/get/parameters/0/required
test error msg
16| 
17| - name: userId
18| in: path
19| required: true
20| description: Id of a user
21| schema:
"
`);
});
});

View File

@@ -0,0 +1,175 @@
import yaml from "js-yaml";
import fs from "fs";
import {
createErrorMissingRequiredField,
createErrorFieldNotAllowed,
createErrrorFieldTypeMismatch,
createErrorMutuallyExclusiveFields
} from "..";
const getSource = () =>
fs.readFileSync("./test/specs/openapi/test-1.yaml", "utf-8");
const createContext = () => ({
document: yaml.safeLoad(getSource()),
path: ["paths", "/user/{userId}/{name}", "get", "parameters"],
pathStack: [],
source: getSource(),
enableCodeframe: true
});
describe("createErrorFieldNotAllowed", () => {
test("", () => {
const ctx = createContext();
const node = { required: 123 };
const error = createErrorFieldNotAllowed("wrong", node, ctx);
expect(error).toMatchInlineSnapshot(`
Object {
"codeFrame": "13| 
14| summary: Get a list of all users
15| description: Also gives their status
16| parameters:
17| - name: userId
18| in: path",
"file": undefined,
"location": Object {
"endCol": 17,
"endIndex": 275,
"endLine": 16,
"startCol": 7,
"startIndex": 265,
"startLine": 16,
},
"message": "The field 'wrong' is not allowed here. Use \\"x-\\" prefix to override this behavior.",
"path": "paths/['/user/{userId}/{name}']/get/parameters",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"required": 123,
},
}
`);
});
});
describe("createErrorMissingRequiredField", () => {
test("", () => {
const ctx = createContext();
const node = { required: 123 };
const error = createErrorMissingRequiredField("name", node, ctx);
expect(error).toMatchInlineSnapshot(`
Object {
"codeFrame": "13| 
14| summary: Get a list of all users
15| description: Also gives their status
16| parameters:
17| - name: userId
18| in: path",
"file": undefined,
"location": Object {
"endCol": 17,
"endIndex": 275,
"endLine": 16,
"startCol": 7,
"startIndex": 265,
"startLine": 16,
},
"message": "The field 'name' must be present on this level.",
"path": "paths/['/user/{userId}/{name}']/get/parameters",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"required": 123,
},
}
`);
});
});
describe("createErrrorFieldTypeMismatch", () => {
test("", () => {
const ctx = createContext();
ctx.path = [
"paths",
"/user/{userId}/{name}",
"get",
"parameters",
0,
"required"
];
const node = { required: 123 };
const error = createErrrorFieldTypeMismatch("boolean", node, ctx);
expect(error).toMatchInlineSnapshot(`
Object {
"codeFrame": "16| 
17| - name: userId
18| in: path
19| required: true
20| description: Id of a user",
"file": undefined,
"location": Object {
"endCol": 19,
"endIndex": 337,
"endLine": 19,
"startCol": 11,
"startIndex": 329,
"startLine": 19,
},
"message": "This field must be of boolean type.",
"path": "paths/['/user/{userId}/{name}']/get/parameters/0/required",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"required": 123,
},
}
`);
});
});
describe("createErrorMutuallyExclusiveFields", () => {
const ctx = createContext();
ctx.path = [
"paths",
"/user/{userId}/{name}",
"get",
"parameters",
0,
"required"
];
const node = { required: 123 };
const error = createErrorMutuallyExclusiveFields(
["example", "examples"],
node,
ctx
);
expect(error).toMatchInlineSnapshot(`
Object {
"codeFrame": "16| 
17| - name: userId
18| in: path
19| required: true
20| description: Id of a user",
"file": undefined,
"location": Object {
"endCol": 19,
"endIndex": 337,
"endLine": 19,
"startCol": 11,
"startIndex": 329,
"startLine": 19,
},
"message": "Fields 'example', 'examples' are mutually exclusive.",
"path": "paths/['/user/{userId}/{name}']/get/parameters/0/required",
"pathStack": Array [],
"prettyPrint": [Function],
"severity": "ERROR",
"value": Object {
"required": 123,
},
}
`);
});

77
src/error/default.js Normal file
View File

@@ -0,0 +1,77 @@
import fs from 'fs';
import { getLocationByPath, getCodeFrameForLocation } from '../yaml';
export const messageLevels = {
ERROR: 4,
WARNING: 3,
INFO: 2,
DEBUG: 1,
};
const getLocationForPath = (fName, path, target) => {
const fContent = fs.readFileSync(fName, 'utf-8');
const tempCtx = { source: fContent };
const location = getLocationByPath(Array.from(path), tempCtx, target);
// if (!location) {
// console.log(path);
// }
return location.startLine;
};
const createError = (msg, node, ctx, options) => {
const {
target, severity = messageLevels.ERROR, possibleAlternate, fromRule,
} = options;
let location = getLocationByPath(Array.from(ctx.path), ctx, target);
if (!location) location = getLocationByPath(Array.from(ctx.path), ctx);
return {
message: msg,
path: Array.from(ctx.path),
pathStack: ctx.pathStack.map((el) => {
const startLine = getLocationForPath(el.file, el.path, target);
return {
file: el.file,
startLine,
path: Array.from(el.path),
};
}),
location,
codeFrame: ctx.enableCodeframe && location
? getCodeFrameForLocation(
location.startIndex,
location.endIndex,
ctx.source,
location.startLine,
)
: null,
value: node,
file: ctx.filePath,
severity,
enableCodeframe: ctx.enableCodeframe,
possibleAlternate,
fromRule,
target,
};
};
export const fromError = (error, ctx) => {
let location = getLocationByPath(Array.from(ctx.path), ctx, error.target);
if (!location) location = getLocationByPath(Array.from(ctx.path), ctx);
return {
...error,
...ctx,
path: error.path,
pathStack: ctx.pathStack.map((el) => {
const startLine = getLocationForPath(el.file, el.path, error.target);
return {
file: el.file,
startLine,
path: Array.from(el.path),
};
}),
};
};
export default createError;

22
src/error/index.js Normal file
View File

@@ -0,0 +1,22 @@
import createError from './default';
export const createErrorFieldNotAllowed = (fieldName, node, ctx,
options) => createError(
`The field '${fieldName}' is not allowed here. Use "x-" prefix to override this behavior.`, node, ctx, { target: 'key', ...options },
);
export const createErrorMissingRequiredField = (fieldName, node, ctx,
options) => createError(
`The field '${fieldName}' must be present on this level.`, node, ctx, { target: 'key', ...options },
);
export const createErrrorFieldTypeMismatch = (desiredType, node, ctx, options) => createError(
`This field must be of ${desiredType} type.`, node, ctx, { target: 'key', ...options },
);
export const createErrorMutuallyExclusiveFields = (fieldNames, node, ctx,
options) => createError(
`Fields ${fieldNames.map((el) => `'${el}'`).join(', ')} are mutually exclusive.`, node, ctx, { target: 'key', ...options },
);
export default createError;

View File

@@ -0,0 +1,28 @@
/* eslint-disable class-methods-use-this */
import AbstractRule from './utils/AbstractRule';
import { createErrorMissingRequiredField } from '../error';
class ApiServers extends AbstractRule {
static get ruleName() {
return 'apiServers';
}
get rule() {
return 'api-servers';
}
OpenAPIRoot() {
return {
onEnter: (node, _, ctx) => (
(node.servers && Array.isArray(node.servers) && node.servers.length > 0)
? null
: [
createErrorMissingRequiredField('servers', node, ctx, {
target: 'key', severity: this.config.level, fromRule: this.rule,
}),
]),
};
}
}
module.exports = ApiServers;

View File

@@ -0,0 +1,36 @@
/* eslint-disable class-methods-use-this */
import createError from '../error';
import AbstractRule from './utils/AbstractRule';
class CheckPathParamExists extends AbstractRule {
static get ruleName() {
return 'checkPathParamExists';
}
get rule() {
return 'path-param-exists';
}
OpenAPIParameter() {
return {
onEnter: (node, definition, ctx) => {
const errors = [];
if (node.in && node.in === 'path') {
const visitedNodes = ctx.pathStack.reduce((acc, val) => [...acc, ...(val.path)], []);
const missingNameInPath = [...ctx.path, ...visitedNodes]
.filter((pathNode) => typeof pathNode === 'string' && pathNode.indexOf(`{${node.name}}`) !== -1)
.length === 0
&& (ctx.path.indexOf('components') === -1 || visitedNodes.indexOf('paths') !== -1);
if (missingNameInPath) {
ctx.path.push('name');
errors.push(createError('The "name" field value is not in the current parameter path.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level }));
ctx.path.pop();
}
}
return errors;
},
};
}
}
module.exports = CheckPathParamExists;

View File

@@ -0,0 +1,40 @@
/* eslint-disable class-methods-use-this */
import AbstractRule from './utils/AbstractRule';
class DebugInfo extends AbstractRule {
constructor() {
super();
this.count = 0;
}
static get ruleName() {
return 'debugInfo';
}
get rule() {
return 'debug-info';
}
any() {
return {
onEnter: () => {
this.count += 1;
if (this.count % 4000 === 0) console.log('Processed:', this.count);
},
};
}
OpenAPIRoot() {
return {
onEnter: (node, def, ctx) => {
console.log(ctx.config);
},
onExit: (node, definition, ctx) => {
console.log(this.count);
console.log(ctx.result.length);
},
};
}
}
module.exports = DebugInfo;

View File

@@ -0,0 +1,27 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../error';
import AbstractRule from './utils/AbstractRule';
class LicenseURL extends AbstractRule {
static get ruleName() {
return 'license-url';
}
get rule() {
return 'license-url';
}
OpenAPILicense() {
return {
onEnter: (node, _, ctx) => {
if (!node.url) {
return [createErrorMissingRequiredField('url', node, ctx, { severity: this.config.level, fromRule: this.rule })];
}
return null;
},
};
}
}
module.exports = LicenseURL;

View File

@@ -0,0 +1,26 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../error';
import AbstractRule from './utils/AbstractRule';
class LicenseRequired extends AbstractRule {
static get ruleName() {
return 'license-required';
}
get rule() {
return 'license-required';
}
OpenAPIInfo() {
return {
onEnter: (node, definition, ctx) => {
if (!node.license) {
return [createErrorMissingRequiredField('license', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level })];
}
return null;
},
};
}
}
module.exports = LicenseRequired;

View File

@@ -0,0 +1,72 @@
import createError from '../error';
import AbstractRule from './utils/AbstractRule';
class NoUnusedComponents extends AbstractRule {
constructor(config) {
super(config);
this.components = {};
}
static get ruleName() {
return 'no-unused-schemas';
}
get rule() {
return 'no-unused-schemas';
}
OpenAPIRoot() {
return {
onExit: (node, definition, ctx) => {
const messages = [];
ctx.path.push('components');
ctx.path.push('schemas');
Object.keys(this.components)
.filter((schemaName) => this.components[schemaName] === false)
.forEach((schemaName) => {
ctx.path.push(schemaName);
messages.push(createError(`The schema "${schemaName}" is never used.`, node, ctx, { fromRule: this.rule, target: 'key', severity: this.config.level }));
ctx.path.pop();
});
ctx.path.pop();
ctx.path.pop();
return messages;
},
};
}
OpenAPISchema() {
return {
onEnter: (node, definition, ctx, unresolvedNode) => {
if (unresolvedNode.$ref && unresolvedNode.$ref.indexOf('#/components/schemas') === 0) {
const schemaName = unresolvedNode.$ref.split('/')[3];
if (Object.keys(this.components).indexOf(schemaName) !== -1) {
this.components[schemaName] = true;
} else {
this.components[schemaName] = true;
}
}
},
};
}
OpenAPISchemaMap() {
return {
onEnter: (node, definition, ctx) => {
if (ctx.path[0] === 'components' && ctx.path.length === 2 && ctx.pathStack.length === 0) { // in the components.schemas definition
Object.keys(node).forEach((schemaName) => {
// console.log(schemaName);
if (Object.keys(this.components).indexOf(schemaName) !== -1 || node[schemaName].allOf) {
// .allOf here is used as a very naive check for possible discriminator in parent node
this.components[schemaName] = true;
} else {
this.components[schemaName] = false;
}
});
}
},
};
}
}
module.exports = NoUnusedComponents;

View File

@@ -0,0 +1,41 @@
/* eslint-disable class-methods-use-this */
import createError from '../error';
import AbstractRule from './utils/AbstractRule';
class Operation2xxResponse extends AbstractRule {
static get ruleName() {
return 'operation2xxResponse';
}
get rule() {
return 'operation-2xx-response';
}
constructor(config) {
super(config);
this.responseCodes = [];
}
OpenAPIOperation() {
return {
onExit: (node, definition, ctx) => {
const errors = [];
if (!this.responseCodes.find((code) => code[0] === '2')) {
errors.push(createError('Operation must have at least one 2xx response.', node, ctx, { target: 'value', severity: this.config.level, fromRule: this.rule }));
}
this.responseCodes = [];
return errors;
},
};
}
OpenAPIResponseMap() {
return {
onEnter: (node) => {
this.responseCodes.push(...Object.keys(node));
},
};
}
}
module.exports = Operation2xxResponse;

View File

@@ -0,0 +1,27 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../error';
import AbstractRule from './utils/AbstractRule';
class OperationDescription extends AbstractRule {
static get ruleName() {
return 'operation-description';
}
get rule() {
return 'operation-description';
}
OpenAPIOperation() {
return {
onEnter: (node, _, ctx) => {
if (!node.description) {
return [createErrorMissingRequiredField('description', node, ctx, { severity: this.config.level, fromRule: this.rule })];
}
return null;
},
};
}
}
module.exports = OperationDescription;

View File

@@ -0,0 +1,35 @@
/* eslint-disable class-methods-use-this */
import createError from '../error';
import AbstractRule from './utils/AbstractRule';
class OperationIdUnique extends AbstractRule {
static get ruleName() {
return 'operation-operationId-unique';
}
get rule() {
return 'operation-operationId-unique';
}
constructor() {
super();
this.operationIds = {};
}
OpenAPIOperation() {
return {
onEnter: (node, definition, ctx) => {
if (node.operationId) {
if (this.operationIds[node.operationId]) {
this.operationIds[node.operationId] += 1;
return [createError('The "operationId" fields must be unique.', node, ctx, { target: 'value', severity: this.config.level, fromRule: this.rule })];
}
this.operationIds[node.operationId] = 1;
}
return null;
},
};
}
}
module.exports = OperationIdUnique;

View File

@@ -0,0 +1,27 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../error';
import AbstractRule from './utils/AbstractRule';
class OperationOperationId extends AbstractRule {
static get ruleName() {
return 'operationOperationId';
}
get rule() {
return 'operation-operationId';
}
OpenAPIOperation() {
return {
onEnter: (node, _, ctx) => {
if (!node.operationId) {
return [createErrorMissingRequiredField('operationId', node, ctx, { severity: this.config.level, fromRule: this.rule })];
}
return null;
},
};
}
}
module.exports = OperationOperationId;

View File

@@ -0,0 +1,22 @@
/* eslint-disable class-methods-use-this */
import AbstractRule from './utils/AbstractRule';
import { createErrorMissingRequiredField } from '../error';
class OperationTags extends AbstractRule {
static get ruleName() {
return 'operationTags';
}
get rule() {
return 'operation-tags';
}
OpenAPIOperation() {
return {
onEnter: (node, _, ctx) => (node.tags ? null : createErrorMissingRequiredField('tags', node, ctx, { severity: this.config.level, fromRule: this.rule })),
};
}
}
module.exports = OperationTags;

View File

@@ -0,0 +1,28 @@
/* eslint-disable class-methods-use-this */
import AbstractRule from './utils/AbstractRule';
import createError from '../error';
class PathDeclarationsMustExist extends AbstractRule {
static get ruleName() {
return 'pathDeclarationsMustExist';
}
get rule() {
return 'path-declarations-must-exist';
}
OpenAPIPath() {
return {
onEnter: (node, _, ctx) => (ctx.path[ctx.path.length - 1].indexOf('{}') === -1
? null
: createError(
'Path parameter declarations must be non-empty. {} is invalid.', node, ctx, {
target: 'key', severity: this.config.level, fromRule: this.rule,
},
)),
};
}
}
module.exports = PathDeclarationsMustExist;

View File

@@ -0,0 +1,30 @@
/* eslint-disable class-methods-use-this */
import AbstractRule from './utils/AbstractRule';
import createError from '../error';
class PathKeysNoTrailingSlash extends AbstractRule {
static get ruleName() {
return 'pathKeysNoTrailingSlash';
}
get rule() {
return 'path-keys-no-trailing-slash';
}
OpenAPIPath() {
return {
onEnter: (node, _, ctx) => {
const pathLen = ctx.path.length;
return ctx.path[pathLen - 1][ctx.path[pathLen - 1].length] !== '/'
? null
: [createError(
'Trailing spaces in path are not recommended.', node, ctx, {
target: 'key', severity: this.config.level, fromRule: this.rule,
},
)];
},
};
}
}
module.exports = PathKeysNoTrailingSlash;

View File

@@ -0,0 +1,50 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../error';
import AbstractRule from './utils/AbstractRule';
class ProvideContact extends AbstractRule {
static get ruleName() {
return 'provideContact';
}
get rule() {
return 'provide-contact';
}
constructor() {
super();
this.contactFields = [];
this.requiredFields = ['name', 'email'];
}
OpenAPIInfo() {
return {
onExit: (node, _, ctx) => {
const errors = [];
if (!node.contact) {
return [createErrorMissingRequiredField('contact', node, ctx, { severity: this.config.level, fromRule: this.rule })];
}
this.requiredFields.forEach((fName) => {
if (this.contactFields.indexOf(fName) === -1) {
errors.push(
createErrorMissingRequiredField(
fName, node, ctx, { severity: this.config.level, fromRule: this.rule },
),
);
}
});
return errors;
},
};
}
OpenAPIContact() {
return {
onEnter: (node) => {
this.contactFields.push(...Object.keys(node));
},
};
}
}
module.exports = ProvideContact;

View File

@@ -0,0 +1,27 @@
/* eslint-disable class-methods-use-this */
import AbstractRule from './utils/AbstractRule';
import createError from '../error';
class ServersNoTrailingSlash extends AbstractRule {
static get ruleName() {
return 'servesrNoTrailingSlash';
}
get rule() {
return 'servers-no-trailing-slash';
}
OpenAPIServer() {
return {
onEnter: (node, _, ctx) => (node.url && node.url === '/'
? [createError(
'Trailing spaces in path are not recommended.', node, ctx, {
target: 'key', severity: this.config.level, fromRule: this.rule,
},
)]
: null),
};
}
}
module.exports = ServersNoTrailingSlash;

View File

@@ -0,0 +1,61 @@
import AbstractRule from '../utils/AbstractRule';
import { createErrorFieldNotAllowed, createErrrorFieldTypeMismatch } from '../../error';
import { getClosestString } from '../../utils';
class NoExtraFields extends AbstractRule {
static get ruleName() {
return 'no-extra-fields';
}
any() {
return {
onEnter: (node, definition, ctx) => {
const errors = [];
const allowedChildren = [];
if (definition.properties) {
switch (typeof definition.properties) {
case 'object':
allowedChildren.push(...Object.keys(definition.properties));
break;
case 'function':
allowedChildren.push(...Object.keys(definition.properties(node)));
break;
default:
// do-nothing
}
}
if (allowedChildren.length > 0 && typeof node !== 'object') {
errors.push(
createErrrorFieldTypeMismatch(definition.name, node, ctx, {
fromRule: this.rule, severity: this.config.level,
}),
);
return errors;
}
Object.keys(node).forEach((field) => {
ctx.path.push(field);
if (!allowedChildren.includes(field) && field.indexOf('x-') !== 0 && field !== '$ref') {
const possibleAlternate = getClosestString(field, allowedChildren);
errors.push(
createErrorFieldNotAllowed(
field, node, ctx, {
fromRule: this.rule, severity: this.config.level, possibleAlternate,
},
),
);
}
ctx.path.pop();
});
return errors;
},
};
}
}
module.exports = NoExtraFields;

View File

@@ -0,0 +1,61 @@
import fs from 'fs';
import AbstractRule from '../utils/AbstractRule';
import createError from '../../error';
class NoRefSiblings extends AbstractRule {
static get ruleName() {
return 'no-$ref-siblings';
}
any() {
return {
onEnter: (node, definition, ctx, unresolvedNode) => {
const errors = [];
if (!unresolvedNode || typeof unresolvedNode !== 'object') return errors;
const nodeKeys = Object.keys(unresolvedNode);
if (nodeKeys.indexOf('$ref') === -1) return errors;
if (nodeKeys.length > 1) {
const tempPath = {
path: ctx.path,
filePath: ctx.filePath,
source: ctx.source,
};
const prevPathItem = ctx.pathStack[ctx.pathStack.length - 1];
ctx.path = prevPathItem.path;
ctx.filePath = prevPathItem.file;
ctx.source = fs.readFileSync(prevPathItem.file, 'utf-8');
for (let i = 0; i < nodeKeys.length; i++) {
if (nodeKeys[i] !== '$ref') {
ctx.path.push(nodeKeys[i]);
const e = createError(
'No siblings are allowed inside object with $ref property.',
unresolvedNode,
ctx,
{
severity: this.config.level, fromRule: this.rule, taget: 'key',
},
);
errors.push(e);
ctx.path.pop();
}
}
ctx.source = tempPath.source;
ctx.path = tempPath.path;
ctx.filePath = tempPath.filePath;
}
return errors;
},
};
}
}
module.exports = NoRefSiblings;

View File

@@ -0,0 +1,61 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateAuthorizationCodeOpenAPIFlow extends AbstractRule {
static get ruleName() {
return 'auth-code-flow';
}
validators() {
return {
authorizationUrl: (node, ctx) => {
if (!node.authorizationUrl) return createErrorMissingRequiredField('authorizationUrl', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.authorizationUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
tokenUrl: (node, ctx) => {
if (!node.tokenUrl) return createErrorMissingRequiredField('tokenUrl', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.tokenUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
refreshUrl: (node, ctx) => {
if (node.refreshUrl && typeof node.refreshUrl !== 'string') return createError('The refreshUrl must be a string in the Open API Flow Object', node, ctx, { fromRule: this.rule, severity: this.config.severity });
return null;
},
scopes: (node, ctx) => {
const wrongFormatMap = Object.keys(node.scopes)
.filter((scope) => typeof scope !== 'string' || typeof node.scopes[scope] !== 'string')
.length > 0;
if (wrongFormatMap) return createError('The scopes field must be a Map[string, string] in the Open API Flow Object', node, ctx, { fromRule: this.rule, severity: this.config.severity });
return null;
},
};
}
AuthorizationCodeOpenAPIFlow() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateAuthorizationCodeOpenAPIFlow;

View File

@@ -0,0 +1,56 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateClientCredentialsOpenAPIFlow extends AbstractRule {
static get ruleName() {
return 'client-creds-flow';
}
validators() {
return {
tokenUrl: (node, ctx) => {
if (!node.tokenUrl) return createErrorMissingRequiredField('tokenUrl', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.tokenUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
refreshUrl: (node, ctx) => {
if (node.refreshUrl && typeof node.refreshUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
scopes: (node, ctx) => {
const wrongFormatMap = Object.keys(node.scopes)
.filter((scope) => typeof scope !== 'string' || typeof node.scopes[scope] !== 'string')
.length > 0;
if (wrongFormatMap) return createError('The scopes field must be a Map[string, string] in the Open API Flow Object', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
};
}
ClientCredentialsOpenAPIFlow() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateClientCredentialsOpenAPIFlow;

View File

@@ -0,0 +1,57 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateImplicitOpenAPIFlow extends AbstractRule {
static get ruleName() {
return 'implicit-flow';
}
validators() {
return {
authorizationUrl: (node, ctx) => {
if (!node.authorizationUrl) return createErrorMissingRequiredField('authorizationUrl', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.authorizationUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
refreshUrl: (node, ctx) => {
if (node.refreshUrl && typeof node.refreshUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx);
return null;
},
scopes: (node, ctx) => {
if (!node.scopes) return createErrorMissingRequiredField('scopes', node, ctx, { fromRule: this.rule, severity: this.config.level });
const wrongFormatMap = Object.keys(node.scopes)
.filter((scope) => typeof scope !== 'string' || typeof node.scopes[scope] !== 'string')
.length > 0;
if (wrongFormatMap) return createError('The scopes field must be a Map[string, string] in the Open API Flow Object', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
};
}
ImplicitOpenAPIFlow() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateImplicitOpenAPIFlow;

View File

@@ -0,0 +1,44 @@
/* eslint-disable class-methods-use-this */
import { createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIContact extends AbstractRule {
static get ruleName() {
return 'contact';
}
validators() {
return {
name: (node, ctx) => ((node && node.name) && typeof node.name !== 'string' ? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
url: (node, ctx) => ((node && node.url) && typeof node.url !== 'string' ? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
email: (node, ctx) => ((node && node.url) && typeof node.url !== 'string' ? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
};
}
OpenAPIContact() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIContact;

View File

@@ -0,0 +1,50 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIDiscriminator extends AbstractRule {
static get ruleName() {
return 'discriminator';
}
validators() {
return {
propertyName: (node, ctx) => {
if (!(node && node.propertyName)) return createErrorMissingRequiredField('propertyName', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.propertyName !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
mapping: (node, ctx) => {
if (node && node.mapping && typeof node.mapping !== 'object') return createErrrorFieldTypeMismatch('Map[string, string]', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (node && node.mapping && Object.keys(node.mapping).filter((key) => typeof node.mapping[key] !== 'string').length !== 0) return createErrrorFieldTypeMismatch('Map[string, string]', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
};
}
OpenAPIDiscriminator() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIDiscriminator;

View File

@@ -0,0 +1,64 @@
/* eslint-disable class-methods-use-this */
import { createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIEncoding extends AbstractRule {
static get ruleName() {
return 'encoding';
}
validators() {
return {
contentType: (node, ctx) => {
if (node && node.contentType && typeof node.contentType !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
style: (node, ctx) => {
if (node && node.style && typeof node.style !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
explode: (node, ctx) => {
if (node && node.explode && typeof node.explode !== 'boolean') {
return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
allowReserved: (node, ctx) => {
if (node && node.allowReserved && typeof node.allowReserved !== 'boolean') {
return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
};
}
OpenAPIEncoding() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIEncoding;

View File

@@ -0,0 +1,67 @@
/* eslint-disable class-methods-use-this */
import { createErrrorFieldTypeMismatch, createErrorMutuallyExclusiveFields } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIExample extends AbstractRule {
static get ruleName() {
return 'example';
}
validators() {
return {
value: (node, ctx) => {
if (node.value && node.externalValue) {
return createErrorMutuallyExclusiveFields(['value', 'externalValue'], node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
externalValue: (node, ctx) => {
if (node.externalValue && typeof node.externalValue !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
if (node.value && node.externalValue) {
return createErrorMutuallyExclusiveFields(['value', 'externalValue'], node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
description: (node, ctx) => {
if (node.description && typeof node.description !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
summary: (node, ctx) => {
if (node.summary && typeof node.summary !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
};
}
OpenAPIExample() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIExample;

View File

@@ -0,0 +1,47 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrrorFieldTypeMismatch, createErrorMissingRequiredField } from '../../error';
import { isUrl } from '../../utils';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIExternalDocumentation extends AbstractRule {
static get ruleName() {
return 'external-docs';
}
validators() {
return {
description: (node, ctx) => (node && node.description && typeof node.description !== 'string' ? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
url: (node, ctx) => {
if (node && !node.url) return createErrorMissingRequiredField('url', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (!isUrl(node.url)) return createError('url must be a valid URL', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
};
}
OpenAPIExternalDocumentation() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this.config, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIExternalDocumentation;

View File

@@ -0,0 +1,75 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrrorFieldTypeMismatch, createErrorMutuallyExclusiveFields } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIHeader extends AbstractRule {
static get ruleName() {
return 'header';
}
validators() {
return {
description: (node, ctx) => {
if (node && node.description && typeof node.description !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
required: (node, ctx) => {
if (node && node.required && typeof node.required !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (node && node.in && node.in === 'path' && !(node.required || node.required !== true)) {
return createError('If the parameter location is "path", this property is REQUIRED and its value MUST be true.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
deprecated: (node, ctx) => {
if (node && node.deprecated && typeof node.deprecated !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
allowEmptyValue: (node, ctx) => {
if (node && node.allowEmptyValue && typeof node.allowEmptyValue !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
explode: (node, ctx) => {
if (node && node.explode && typeof node.explode !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
allowReserved: (node, ctx) => {
if (node && node.allowReserved && typeof node.allowReserved !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
example: (node, ctx) => {
if (node.example && node.examples) return createErrorMutuallyExclusiveFields(['example', 'examples'], node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
examples: (node, ctx) => {
if (node.example && node.examples) return createErrorMutuallyExclusiveFields(['examples', 'example'], node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
};
}
OpenAPIHeader() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIHeader;

View File

@@ -0,0 +1,44 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIInfo extends AbstractRule {
static get ruleName() {
return 'info';
}
validators() {
return {
title: (node, ctx) => (!node || !node.title ? createErrorMissingRequiredField('title', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
version: (node, ctx) => (!node || !node.version ? createErrorMissingRequiredField('version', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
description: () => null,
termsOfService: () => null,
};
}
OpenAPIInfo() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIInfo;

View File

@@ -0,0 +1,44 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField } from '../../error';
import { isUrl } from '../../utils';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPILicense extends AbstractRule {
static get ruleName() {
return 'license';
}
validators() {
return {
name: (node, ctx) => (!node || !node.name ? createErrorMissingRequiredField('name', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
url: (node, ctx) => (node && node.url && !isUrl(node.url) ? createError('The url field must be a valid URL.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level }) : null),
};
}
OpenAPILicense() {
return {
onEnter: (node, definition, ctx) => {
if (!node) return [];
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this.config, vals[i])) {
if (Object.keys(node).indexOf(vals[i]) !== -1) ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
if (Object.keys(node).indexOf(vals[i]) !== -1) ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPILicense;

View File

@@ -0,0 +1,66 @@
/* eslint-disable class-methods-use-this */
import { createErrorMutuallyExclusiveFields, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPILink extends AbstractRule {
static get ruleName() {
return 'link';
}
validators() {
return {
operationRef: (node, ctx) => {
if (!node || !node.operationRef) return null;
if (node.operationRef && node.operationId) return createErrorMutuallyExclusiveFields(['operationRef', 'operationId'], node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.operationRef !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
operationId: (node, ctx) => {
if (!node || !node.operationId) return null;
if (node.operationRef && node.operationId) return createErrorMutuallyExclusiveFields(['operationId', 'operationRef'], node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.operationId !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
parameters: (node, ctx) => {
if (!node || !node.parameters) return null;
if (Object.keys(node.parameters).filter((key) => typeof key !== 'string').length > 0) {
return createErrrorFieldTypeMismatch('Map[string, any]', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
description: (node, ctx) => {
if (!node || !node.description) return null;
if (typeof node.description !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
};
}
OpenAPILink() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPILink;

View File

@@ -0,0 +1,42 @@
/* eslint-disable class-methods-use-this */
import { createErrorMutuallyExclusiveFields } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIMediaObject extends AbstractRule {
static get ruleName() {
return 'media-object';
}
validators() {
return {
example: (node, ctx) => (node.example && node.examples ? createErrorMutuallyExclusiveFields(['example', 'examples'], node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
examples: (node, ctx) => (node.example && node.examples ? createErrorMutuallyExclusiveFields(['example', 'examples'], node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
};
}
OpenAPIMediaObject() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIMediaObject;

View File

@@ -0,0 +1,76 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrrorFieldTypeMismatch, createErrorMissingRequiredField } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIOperation extends AbstractRule {
static get ruleName() {
return 'operation';
}
validators() {
return {
tags: (node, ctx) => {
if (!node || !node.tags) return null;
const errors = [];
if (node && node.tags && !Array.isArray(node.tags)) {
return createErrrorFieldTypeMismatch('array.', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
for (let i = 0; i < node.tags.length; i++) {
if (typeof node.tags[i] !== 'string') {
ctx.path.push(i);
errors.push(createError('Items of the tags array must be strings in the Open API Operation object.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level }));
ctx.path.pop();
}
}
return errors;
},
summary: (node, ctx) => {
if (node && node.summary && typeof node.summary !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
description: (node, ctx) => {
if (node && node.description && typeof node.description !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
operationId: (node, ctx) => {
if (node && node.operationId && typeof node.operationId !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
responses: (node, ctx) => (!node.responses ? createErrorMissingRequiredField('responses', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
deprecated: (node, ctx) => {
if (node && node.deprecated && typeof node.deprecated !== 'boolean') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
};
}
OpenAPIOperation() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIOperation;

View File

@@ -0,0 +1,106 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField, createErrrorFieldTypeMismatch, createErrorMutuallyExclusiveFields } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIParameter extends AbstractRule {
static get ruleName() {
return 'parameter';
}
validators() {
return {
name: (node, ctx) => {
if (!node) return null;
if (!node.name) return createErrorMissingRequiredField('name', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.name !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
in: (node, ctx) => {
if (!node) return null;
if (!node.in) return createErrorMissingRequiredField('in', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.in !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (!['query', 'header', 'path', 'cookie'].includes(node.in)) return createError("The 'in' field value can be only one of: 'query', 'header', 'path', 'cookie'", node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
description: (node, ctx) => {
if (node && node.description && typeof node.description !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
required: (node, ctx) => {
if (node && node.required && typeof node.required !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (node && node.in && node.in === 'path' && node.required !== true) {
return createError('If the parameter location is "path", this property is REQUIRED and its value MUST be true.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
deprecated: (node, ctx) => {
if (node && node.deprecated && typeof node.deprecated !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
allowEmptyValue: (node, ctx) => {
if (node && node.allowEmptyValue && typeof node.allowEmptyValue !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
style: (node, ctx) => {
if (node && node.style && typeof node.style !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
explode: (node, ctx) => {
if (node && node.explode && typeof node.explode !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
allowReserved: (node, ctx) => {
if (node && node.allowReserved && typeof node.allowReserved !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
example: (node, ctx) => {
if (node.example && node.examples) return createErrorMutuallyExclusiveFields(['example', 'examples'], node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
examples: (node, ctx) => {
if (node.example && node.examples) return createErrorMutuallyExclusiveFields(['examples', 'example'], node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
schema: (node, ctx) => {
if (node.schema && node.content) {
return createErrorMutuallyExclusiveFields(['schema', 'content'], node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
content: (node, ctx) => {
if (node.schema && node.content) {
return createErrorMutuallyExclusiveFields(['content', 'schema'], node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
};
}
OpenAPIParameter() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this.config, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIParameter;

View File

@@ -0,0 +1,56 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIPath extends AbstractRule {
static get ruleName() {
return 'path';
}
validators() {
return {
summary: (node, ctx) => (node && node.summary && typeof node.summary !== 'string'
? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
description: (node, ctx) => (node && node.description && typeof node.description !== 'string'
? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
servers: (node, ctx) => (node && node.servers && !Array.isArray(node.servers)
? createErrrorFieldTypeMismatch('array', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
parameters: (node, ctx) => {
if (!node || !node.parameters) return null;
if (!Array.isArray(node.parameters)) {
return createErrrorFieldTypeMismatch('array', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
if ((new Set(node.parameters)).size !== node.parameters.length) {
return createError('parameters must be unique in the Path Item object', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
};
}
OpenAPIPath() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIPath;

View File

@@ -0,0 +1,58 @@
/* eslint-disable class-methods-use-this */
import { createErrrorFieldTypeMismatch, createErrorMissingRequiredField } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIRequestBody extends AbstractRule {
static get ruleName() {
return 'request-body';
}
validators() {
return {
description: (node, ctx) => {
if (node && node.description && typeof node.description !== 'string') {
return createErrrorFieldTypeMismatch('string.', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
content: (node, ctx) => {
if (node && !node.content) {
return createErrorMissingRequiredField('content', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
required: (node, ctx) => {
if (node && node.required && typeof node.required !== 'boolean') {
return createErrrorFieldTypeMismatch('boolean.', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
};
}
OpenAPIRequestBody() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIRequestBody;

View File

@@ -0,0 +1,41 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIResponse extends AbstractRule {
static get ruleName() {
return 'response';
}
validators() {
return {
description: (node, ctx) => (!node.description ? createErrorMissingRequiredField('description', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
};
}
OpenAPIResponse() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIResponse;

View File

@@ -0,0 +1,53 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIRoot extends AbstractRule {
static get ruleName() {
return 'root';
}
validators() {
return {
openapi: (node, ctx) => {
if (node && !node.openapi) return createErrorMissingRequiredField('openapi', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
info: (node, ctx) => {
if (node && !node.info) return createErrorMissingRequiredField('info', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
paths: (node, ctx) => {
if (node && !node.paths) return createErrorMissingRequiredField('paths', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
security: () => null,
};
}
OpenAPIRoot() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIRoot;

View File

@@ -0,0 +1,190 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrrorFieldTypeMismatch } from '../../error';
import { matchesJsonSchemaType, getClosestString } from '../../utils';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPISchema extends AbstractRule {
static get ruleName() {
return 'schema';
}
validators() {
return {
title: (node, ctx) => {
if (node && node.title) {
if (!(typeof node.title === 'string')) return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
multipleOf: (node, ctx) => {
if (node && node.multipleOf) {
if (typeof node.multipleOf !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.multipleOf < 0) return createError('Value of multipleOf must be greater or equal to zero', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
maximum: (node, ctx) => {
if (node && node.maximum && typeof node.maximum !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
exclusiveMaximum: (node, ctx) => {
if (node && node.exclusiveMaximum && typeof node.exclusiveMaximum !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
minimum: (node, ctx) => {
if (node && node.minimum && typeof node.minimum !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
exclusiveMinimum: (node, ctx) => {
if (node && node.exclusiveMinimum && typeof node.exclusiveMinimum !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
maxLength: (node, ctx) => {
if (node && node.maxLength) {
if (typeof node.maxLength !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.maxLength < 0) return createError('Value of maxLength must be greater or equal to zero.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
minLength: (node, ctx) => {
if (node && node.minLength) {
if (typeof node.minLength !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.minLength < 0) return createError('Value of minLength must be greater or equal to zero.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
pattern: (node, ctx) => {
if (node && node.pattern) {
// TODO: add regexp validation.
if (typeof node.pattern !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
maxItems: (node, ctx) => {
if (node && node.maxItems) {
if (typeof node.maxItems !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.maxItems < 0) return createError('Value of maxItems must be greater or equal to zero. You can`t have negative amount of something.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
minItems: (node, ctx) => {
if (node && node.minItems) {
if (typeof node.minItems !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.minItems < 0) return createError('Value of minItems must be greater or equal to zero. You can`t have negative amount of something.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
uniqueItems: (node, ctx) => {
if (node && node.uniqueItems) {
if (typeof node.uniqueItems !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
maxProperties: (node, ctx) => {
if (node && node.maxProperties) {
if (typeof node.maxProperties !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.maxProperties < 0) return createError('Value of maxProperties must be greater or equal to zero. You can`t have negative amount of something.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
minProperties: (node, ctx) => {
if (node && node.minProperties) {
if (typeof node.minProperties !== 'number') return createErrrorFieldTypeMismatch('number', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.minProperties < 0) return createError('Value of minProperties must be greater or equal to zero. You can`t have negative amount of something.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
required: (node, ctx) => {
if (node && node.required) {
if (!Array.isArray(node.required)) return createErrrorFieldTypeMismatch('array', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
if (node.required.filter((item) => typeof item !== 'string').length !== 0) return createError('All values of "required" field must be strings', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
enum: (node, ctx) => {
const errors = [];
if (node && node.enum) {
if (!Array.isArray(node.enum)) return [createErrrorFieldTypeMismatch('array', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level })];
if (node.type && typeof node.type === 'string') {
const typeMimsatch = node.enum.filter(
(item) => !matchesJsonSchemaType(item, node.type),
);
typeMimsatch.forEach((val) => {
ctx.path.push(node.enum.indexOf(val));
errors.push(createError('All values of "enum" field must be of the same type as the "type" field.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level }));
ctx.path.pop();
});
}
}
return errors;
},
type: (node, ctx) => {
const errors = [];
if (node.type && !['string', 'object', 'array', 'integer', 'number', 'boolean'].includes(node.type)) {
const possibleAlternate = getClosestString(node.type, ['string', 'object', 'array', 'integer', 'number', 'boolean']);
errors.push(createError('Object type can be one of following only: "string", "object", "array", "integer", "number", "boolean".', node, ctx, {
fromRule: this.rule, target: 'value', severity: this.config.level, possibleAlternate,
}));
}
return errors;
},
items: (node, ctx) => {
if (node && node.items && Array.isArray(node.items)) return createError('Value of items must not be an array. It must be a Schema object', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
additionalProperties: () => null,
description: (node, ctx) => {
if (node && node.description && typeof node.description !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
format: (node, ctx) => {
if (node && node.format && typeof node.format !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
nullable: (node, ctx) => {
if (node && node.nullable && typeof node.nullable !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
readOnly: (node, ctx) => {
if (node && node.readOnly && typeof node.readOnly !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
writeOnly: (node, ctx) => {
if (node && node.writeOnly && typeof node.writeOnly !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
deprecated: (node, ctx) => {
if (node && node.deprecated && typeof node.deprecated !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
};
}
OpenAPISchema() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
if (Object.keys(node).indexOf(vals[i]) !== -1) ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
if (Object.keys(node).indexOf(vals[i]) !== -1) ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPISchema;

View File

@@ -0,0 +1,85 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPISecuritySchema extends AbstractRule {
static get ruleName() {
return 'secuirty-schema';
}
validators() {
return {
type: (node, ctx) => {
if (!node.type) return createErrorMissingRequiredField('type', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.type !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (!['apiKey', 'http', 'oauth2', 'openIdConnect'].includes(node.type)) return createError('The type value can only be one of the following "apiKey", "http", "oauth2", "openIdConnect" is required for the OpenAPI Security Scheme object.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
description: (node, ctx) => {
if (node.description && typeof node.description !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
name: (node, ctx) => {
if (node.type !== 'apiKey') return null;
if (typeof node.name !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
in: (node, ctx) => {
if (node.type !== 'apiKey') return null;
if (!node.in) return createErrorMissingRequiredField('in', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.in !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (!['query', 'header', 'cookie'].includes(node.in)) return createError('The in value can only be one of the following "query", "header" or "cookie" for the OpenAPI Security Scheme object', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
scheme: (node, ctx) => {
if (node.type !== 'http') return null;
if (!node.scheme) return createErrorMissingRequiredField('scheme', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.scheme !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
bearerFormat: (node, ctx) => {
if (node.bearerFormat && node.type !== 'http') return createError('The bearerFormat field is applicable only for http', node, ctx, { fromRule: this.rule, target: 'key', severity: this.config.level });
if (!node.bearerFormat && node.type === 'http') return createErrorMissingRequiredField('bearerFormat', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (node.bearerFormat && typeof node.bearerFormat !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
flows: (node, ctx) => {
if (node.type !== 'oauth2') return null;
if (!node.flows) return createErrorMissingRequiredField('flows', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
openIdConnectUrl: (node, ctx) => {
if (node.type !== 'openIdConnect') return null;
if (!node.openIdConnectUrl) return createErrorMissingRequiredField('openIdConnectUrl', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.openIdConnectUrl !== 'string') return createErrrorFieldTypeMismatch('openIdConnectUrl', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
};
}
OpenAPISecuritySchema() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPISecuritySchema;

View File

@@ -0,0 +1,47 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIServer extends AbstractRule {
static get ruleName() {
return 'server';
}
validators() {
return {
url: (node, ctx) => {
if (!node || !node.url || typeof node.url !== 'string') return createErrorMissingRequiredField('url', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.url !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
description: (node, ctx) => (node && node.description && typeof node.description !== 'string'
? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
};
}
OpenAPIServer() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIServer;

View File

@@ -0,0 +1,54 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIServerVariable extends AbstractRule {
static get ruleName() {
return 'server-variable';
}
validators() {
return {
default: (node, ctx) => {
if (!node || !node.default) return createErrorMissingRequiredField('default', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.default !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
description: (node, ctx) => (node && node.description && typeof node.description !== 'string'
? createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level }) : null),
enum: (node, ctx) => {
if (node && node.enum) {
if (!Array.isArray(node.enum)) return createErrrorFieldTypeMismatch('array', node, ctx);
if (node.type && node.enum.filter((item) => typeof item !== 'string').length !== 0) return createError('All values of "enum" field must be strings', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
}
return null;
},
};
}
OpenAPIServerVariable() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIServerVariable;

View File

@@ -0,0 +1,53 @@
/* eslint-disable class-methods-use-this */
import { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPITag extends AbstractRule {
static get ruleName() {
return 'tag';
}
validators() {
return {
name: (node, ctx) => {
if (!node.name) return createErrorMissingRequiredField('name', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (node && node.name && typeof node.name !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
description: (node, ctx) => {
if (node && node.description && typeof node.description !== 'string') {
return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
}
return null;
},
};
}
OpenAPITag() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPITag;

View File

@@ -0,0 +1,61 @@
/* eslint-disable class-methods-use-this */
import { createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidateOpenAPIXML extends AbstractRule {
static get ruleName() {
return 'xml';
}
validators() {
return {
name: (node, ctx) => {
if (node && node.name && typeof node.name !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
namespace: (node, ctx) => {
// TODO: add validation that format is uri
if (node && node.namespace && typeof node.namespace !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
prefix: (node, ctx) => {
if (node && node.prefix && typeof node.prefix !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
attribute: (node, ctx) => {
if (node && node.attribute && typeof node.attribute !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
wrapped: (node, ctx) => {
if (node && node.wrapped && typeof node.wrapped !== 'boolean') return createErrrorFieldTypeMismatch('boolean', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
};
}
OpenAPIXML() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidateOpenAPIXML;

View File

@@ -0,0 +1,56 @@
/* eslint-disable class-methods-use-this */
import createError, { createErrorMissingRequiredField, createErrrorFieldTypeMismatch } from '../../error';
import isRuleEnabled from '../utils';
import AbstractRule from '../utils/AbstractRule';
class ValidatePasswordOpenAPIFlow extends AbstractRule {
static get ruleName() {
return 'password-flow';
}
validators() {
return {
tokenUrl: (node, ctx) => {
if (!node.tokenUrl) return createErrorMissingRequiredField('tokenUrl', node, ctx, { fromRule: this.rule, severity: this.config.level });
if (typeof node.tokenUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
refreshUrl: (node, ctx) => {
if (node.refreshUrl && typeof node.refreshUrl !== 'string') return createErrrorFieldTypeMismatch('string', node, ctx, { fromRule: this.rule, severity: this.config.level });
return null;
},
scopes: (node, ctx) => {
const wrongFormatMap = Object.keys(node.scopes)
.filter((scope) => typeof scope !== 'string' || typeof node.scopes[scope] !== 'string')
.length > 0;
if (wrongFormatMap) return createError('The scopes field must be a Map[string, string] in the Open API Flow Object', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
return null;
},
};
}
PasswordOpenAPIFlow() {
return {
onEnter: (node, definition, ctx) => {
const result = [];
const validators = this.validators();
const vals = Object.keys(validators);
for (let i = 0; i < vals.length; i += 1) {
if (isRuleEnabled(this, vals[i])) {
ctx.path.push(vals[i]);
const res = validators[vals[i]](node, ctx, this.config);
if (res) {
if (Array.isArray(res)) result.push(...res);
else result.push(res);
}
ctx.path.pop();
}
}
return result;
},
};
}
}
module.exports = ValidatePasswordOpenAPIFlow;

View File

@@ -0,0 +1,27 @@
import AbstractRule from './utils/AbstractRule';
import { getClosestString } from '../utils';
class SuggestPossibleRefs extends AbstractRule {
static get ruleName() {
return 'suggestPossibleRefs';
}
get rule() {
return 'suggest-possible-refs';
}
OpenAPIRoot() {
return {
onExit: (node, definition, ctx) => {
for (let i = 0; i < ctx.result.length; i++) {
if (ctx.result[i].fromRule === 'resolve-ref') {
const possibleAlternate = getClosestString(ctx.result[i].value.$ref, ctx.visited.map((el) => `#/${el.split('::').pop()}`));
ctx.result[i].possibleAlternate = possibleAlternate;
}
}
},
};
}
}
module.exports = SuggestPossibleRefs;

View File

@@ -0,0 +1,62 @@
import createError from '../error';
import AbstractRule from './utils/AbstractRule';
class UniqueParameterNames extends AbstractRule {
static get ruleName() {
return 'uniqueParameterNames';
}
get rule() {
return 'unique-parameter-names';
}
constructor(config) {
super(config);
this.parametersStack = [];
}
exitNode(node) {
if (node.parameters) {
if (Array.isArray(node.parameters)) {
node.parameters.forEach(() => this.parametersStack.pop());
} else if (typeof node.parameters === 'object') {
Object.keys(node.parameters).forEach(() => this.parametersStack.pop());
}
}
}
OpenAPIComponents() {
return {
onExit: this.exitNode.bind(this),
};
}
OpenAPIOperation() {
return {
onExit: this.exitNode.bind(this),
};
}
OpenAPIPath() {
return {
onExit: this.exitNode.bind(this),
};
}
OpenAPIParameter() {
return {
onEnter: (node, definition, ctx) => {
let error;
if (this.parametersStack.includes(node.name) && !(ctx.pathStack.length === 0 && ctx.path.includes('components'))) {
ctx.path.push('name');
error = createError('Duplicate parameters are not allowed. This name already used on higher or same level.', node, ctx, { fromRule: this.rule, target: 'value', severity: this.config.level });
ctx.path.pop();
}
this.parametersStack.push(node.name);
return error ? [error] : [];
},
};
}
}
module.exports = UniqueParameterNames;

View File

@@ -0,0 +1,31 @@
import { messageLevels } from '../../error/default';
import ruleTypes from './rulesTypes';
class AbstractRule {
constructor(config) {
this.config = { ...config };
switch (this.config.level) {
case 'error':
this.config.level = messageLevels.ERROR;
break;
case 'warning':
this.config.level = messageLevels.WARNING;
break;
case 'info':
this.config.level = messageLevels.INFO;
break;
case 'debug':
this.config.level = messageLevels.DEBUG;
break;
default:
this.config.level = messageLevels.ERROR;
break;
}
}
get rule() {
return `${ruleTypes.schema}/${this.constructor.ruleName}`;
}
}
export default AbstractRule;

View File

@@ -0,0 +1,4 @@
export default (config, rulePath) => {
if (config && config[rulePath] && (config[rulePath] === 'off' || config[rulePath] === false)) return false;
return true;
};

View File

@@ -0,0 +1,3 @@
export default {
schema: 'oas3-schema',
};

10
src/index.js Normal file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
import { validateFromFile } from './validate';
import cli from './cli';
export { validate, validateFromFile } from './validate';
export default validateFromFile;
if (require.main === module) {
cli();
}

70
src/loader/index.js Normal file
View File

@@ -0,0 +1,70 @@
import path from 'path';
import fs from 'fs';
function getObjByPathOrParent(json, JSONPath) {
const get = (p, o) => p.reduce((xs, x) => ((xs && xs[x]) ? xs[x] : null), o);
return get(JSONPath.split('.'), json);
}
function loadRuleset(config) {
const ruleSet = [];
const configCopy = {
...config,
rulesPath: config.rulesPath ? config.rulesPath : `${__dirname}/../extendedRules`,
};
let rulesDirectory = path.resolve(configCopy.rulesPath);
if (!fs.existsSync(rulesDirectory)) {
rulesDirectory = `${__dirname}/../extendedRules`;
}
const ruleSetDirContents = fs.readdirSync(rulesDirectory)
.map((fName) => `${rulesDirectory}/${fName}`);
const files = ruleSetDirContents.filter((fName) => fs.lstatSync(fName).isFile());
const dirs = ruleSetDirContents
.filter((fName) => !fs.lstatSync(fName).isFile() && fName.indexOf('utils') === -1);
files.forEach((file) => {
const Rule = require(file);
const ruleInstanceInit = new Rule();
let ruleConfig = getObjByPathOrParent(configCopy.rules, ruleInstanceInit.rule.replace('/', '.'));
if (configCopy && configCopy.rules) {
if (ruleConfig !== 'off') {
const s = ruleInstanceInit.rule.split('/')[0];
// console.log(ruleInstanceInit.rule, ruleConfig);
if (!ruleConfig) {
ruleConfig = getObjByPathOrParent(configCopy.rules, s);
if (ruleConfig && typeof ruleConfig === 'object') {
const allowed = ['level'];
ruleConfig = Object.keys(ruleConfig)
.filter((key) => allowed.includes(key))
.reduce((obj, key) => {
obj[key] = ruleConfig[key];
return obj;
}, {});
}
// console.log(ruleConfig);
}
const ruleInstance = new Rule(ruleConfig);
ruleSet.push(ruleInstance);
}
} else {
const ruleInstance = new Rule();
ruleSet.push(ruleInstance);
}
});
dirs.forEach((dir) => {
const nestedRules = loadRuleset({
...configCopy,
rulesPath: dir,
});
ruleSet.push(...nestedRules);
});
return ruleSet;
}
export default loadRuleset;

80
src/resolver.js Normal file
View File

@@ -0,0 +1,80 @@
import fs from 'fs';
import yaml from 'js-yaml';
import createError from './error';
/**
*
* Here we go over each of the steps in the link and try to retreive the value
* for it. If failed (e.g. because of undefined value) -- return null, to indicate that such
* reference does not exist.
*
* @param {string} link A path in the yaml document which is to be resolved
* @param {*} ctx JSON Object with the document field which represents the YAML structure
*/
const resolve = (link, ctx) => {
const linkSplitted = link.split('#/');
const [filePath, docPath] = linkSplitted;
let fullFileName;
let target;
let fData;
if (filePath) {
const path = ctx.filePath.substring(0, Math.max(ctx.filePath.lastIndexOf('/'), ctx.filePath.lastIndexOf('\\')));
fullFileName = path ? `${path}/${filePath}` : filePath;
if (fs.existsSync(fullFileName)) {
fData = fs.readFileSync(fullFileName, 'utf-8');
target = yaml.safeLoad(fData);
} else {
return null;
}
} else {
target = ctx.document;
}
if (docPath) {
const steps = docPath.split('/').filter((el) => el !== '');
Object.keys(steps).forEach((step) => {
target = target && steps[step] && target[steps[step]] ? target[steps[step]] : null;
});
}
return {
node: target,
updatedSource: filePath ? fData : null,
docPath: docPath ? docPath.split('/') : [],
filePath: fullFileName || null,
};
};
const resolveNode = (node, ctx) => {
if (!node || typeof node !== 'object') return { node, nextPath: null };
let nextPath;
let resolved = {
node,
};
Object.keys(node).forEach((p) => {
if (p === '$ref') {
resolved = resolve(node[p], ctx);
if (resolved && resolved.node) {
nextPath = resolved.docPath;
} else {
ctx.path.push('$ref');
ctx.result.push(createError('Reference does not exist.', node, ctx, { fromRule: 'resolve-ref' }));
ctx.path.pop();
resolved = {};
resolved.node = node;
nextPath = null;
resolved.updatedSource = null;
resolved.filePath = null;
}
}
});
return {
node: resolved.node,
nextPath,
updatedSource: resolved.updatedSource,
filePath: resolved.filePath,
};
};
export default resolveNode;

View File

@@ -0,0 +1,30 @@
{
"enableCodeframe": true,
"enbaleCustomRuleset": true,
"rules": {
"api-servers": "off",
"path-param-exists": "off",
"license-url": "off",
"no-extra-fields": "off",
"no-unused-schemas": "off",
"operation-2xx-response": "off",
"operation-description": "off",
"operation-operationId": "off",
"operation-operationId-unique": "off",
"operation-tags": "off",
"path-declarations-must-exist": "off",
"provide-contact": "off",
"servers-no-trailing-slash": "off",
"unique-parameter-names": "off",
"oas3-schema": {
"level": "warning",
"schema": {
"level": "error"
},
"license": {
"level": "error"
}
}
}
}

176
src/traverse.js Normal file
View File

@@ -0,0 +1,176 @@
/* eslint-disable no-case-declarations */
/* eslint-disable no-use-before-define */
import resolveNode from './resolver';
import { fromError } from './error/default';
function traverseChildren(resolvedNode, definition, ctx, visited) {
let nodeChildren;
const errors = [];
switch (typeof definition.properties) {
case 'function':
nodeChildren = definition.properties(resolvedNode);
const childrenNames = Object.keys(nodeChildren);
const resolvedNodeKeys = Object.keys(resolvedNode);
for (let i = 0; i < childrenNames.length; i += 1) {
const child = childrenNames[i];
let childResult = [];
if (resolvedNodeKeys.includes(child)) {
ctx.path.push(child);
if (resolvedNode[child]) {
childResult = traverseNode(resolvedNode[child], nodeChildren[child], ctx, visited);
}
if (childResult) errors.push(...childResult);
ctx.path.pop();
}
}
break;
case 'object':
const props = Object.keys(definition.properties);
for (let i = 0; i < props.length; i += 1) {
const p = props[i];
let propResult = [];
ctx.path.push(p);
if (typeof definition.properties[p] === 'function') {
if (resolvedNode[p]) {
propResult = traverseNode(resolvedNode[p], definition.properties[p](), ctx, visited);
}
} else if (resolvedNode[p]) {
propResult = traverseNode(resolvedNode[p], definition.properties[p], ctx, visited);
}
if (propResult) errors.push(...propResult);
ctx.path.pop();
}
break;
default:
// do nothing
}
return errors;
}
function onNodeEnter(node, ctx) {
let nextPath;
let prevPath;
let resolvedNode;
let updatedSource;
let prevSource;
let filePath;
let prevFilePath;
({
// eslint-disable-next-line prefer-const
node: resolvedNode, nextPath, updatedSource, filePath,
} = resolveNode(node, ctx));
if (nextPath) {
ctx.pathStack.push({ path: ctx.path, file: ctx.filePath });
prevPath = ctx.path;
ctx.path = nextPath;
}
if (updatedSource) {
ctx.AST = null;
prevFilePath = ctx.filePath;
ctx.filePath = filePath;
prevSource = ctx.source;
ctx.source = updatedSource;
}
return {
resolvedNode,
prevPath,
prevFilePath,
prevSource,
};
}
function onNodeExit(nodeContext, ctx) {
if (nodeContext.prevPath) {
const fromStack = ctx.pathStack.pop();
ctx.path = fromStack.path;
}
if (nodeContext.prevFilePath) {
ctx.AST = null;
ctx.source = nodeContext.prevSource;
ctx.filePath = nodeContext.prevFilePath;
}
}
const nestedIncludes = (c, s) => {
const res = s.find((el) => el === s) !== undefined;
return res;
};
function traverseNode(node, definition, ctx, visited = []) {
if (!node || !definition) return [];
const nodeContext = onNodeEnter(node, ctx);
const isRecursive = nestedIncludes(ctx.path, visited);
const errors = [];
const currentPath = `${ctx.filePath}::${ctx.path.join('/')}`;
const localVisited = Array.from(visited);
localVisited.push(currentPath);
if (Array.isArray(nodeContext.resolvedNode)) {
nodeContext.resolvedNode.forEach((nodeChild, i) => {
ctx.path.push(i);
const arrayResult = traverseNode(nodeChild, definition, ctx, localVisited);
if (arrayResult) errors.push(...arrayResult);
ctx.path.pop();
});
if (nodeContext.nextPath) ctx.path = nodeContext.prevPath;
} else {
runRuleOnRuleset(nodeContext, 'onEnter', ctx, definition, node, errors);
if (!isRecursive && (!definition.isIdempotent || !ctx.visited.includes(currentPath))) {
if (!ctx.visited.includes(currentPath)) ctx.visited.push(currentPath);
const errorsChildren = traverseChildren(
nodeContext.resolvedNode, definition, ctx, localVisited,
);
errors.push(...errorsChildren);
} else {
// Will use cached result if we have already traversed this nodes children
const cachedResult = ctx.cache[currentPath]
? ctx.cache[currentPath].map((r) => fromError(r, ctx)) : [];
ctx.result.push(...cachedResult);
onNodeExit(nodeContext, ctx);
return errors;
}
runRuleOnRuleset(nodeContext, 'onExit', ctx, definition, node, errors);
ctx.cache[currentPath] = errors;
}
onNodeExit(nodeContext, ctx);
return errors;
}
function runRuleOnRuleset(nodeContext, ruleName, ctx, definition, node, errors) {
for (let i = 0; i < ctx.customRules.length; i += 1) {
const errorsOnEnterForType = ctx.customRules[i][definition.name]
&& ctx.customRules[i][definition.name]()[ruleName]
? ctx.customRules[i][definition.name]()[ruleName](
nodeContext.resolvedNode, definition, ctx, node,
) : [];
const errorsOnEnterGeneric = ctx.customRules[i].any && ctx.customRules[i].any()[ruleName]
? ctx.customRules[i].any()[ruleName](nodeContext.resolvedNode, definition, ctx, node) : [];
if (Array.isArray(errorsOnEnterForType)) {
ctx.result.push(...errorsOnEnterForType);
errors.push(...errorsOnEnterForType);
}
if (Array.isArray(errorsOnEnterGeneric)) {
ctx.result.push(...errorsOnEnterGeneric);
errors.push(...errorsOnEnterGeneric);
}
}
}
export default traverseNode;

23
src/types/OpenAPI3Root.js Normal file
View File

@@ -0,0 +1,23 @@
import { OpenAPIInfo } from './OpenAPIInfo';
import { OpenAPIPaths } from './OpenAPIPaths';
import OpenAPIComponents from './OpenAPIComponents';
import OpenAPIServer from './OpenAPIServer';
// import OpenAPISecurityRequirement from './OpenAPISecurityRequirement';
import OpenAPITag from './OpenAPITag';
import OpenAPIExternalDocumentation from './OpenAPIExternalDocumentation';
export default {
name: 'OpenAPIRoot',
isIdempotent: true,
properties: {
openapi: null,
security: null,
info: OpenAPIInfo,
paths: OpenAPIPaths,
servers: OpenAPIServer,
components: OpenAPIComponents,
// security: OpenAPISecurityRequirement,
tags: OpenAPITag,
externalDocs: OpenAPIExternalDocumentation,
},
};

View File

@@ -0,0 +1,25 @@
/* eslint-disable import/no-cycle */
import { OpenAPIPathItem } from './OpenAPIPaths';
export const OpenAPICallback = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIPathItem;
});
return props;
},
};
export const OpenAPICallbackMap = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPICallback;
});
return props;
},
};

View File

@@ -0,0 +1,25 @@
import OpenAPISchemaMap from './OpenAPISchemaMap';
import OpenAPISecuritySchemaMap from './OpenAPISecuritySchema';
import { OpenAPIExampleMap } from './OpenAPIExample';
import { OpenAPIParameterMap } from './OpenAPIParameter';
import { OpenAPIResponseMap } from './OpenAPIResponse';
import { OpenAPIHeaderMap } from './OpenAPIHeader';
import { OpenAPILinkMap } from './OpenAPILink';
import { OpenAPICallbackMap } from './OpenAPICallback';
import { OpenAPIRequestBodyMap } from './OpenAPIRequestBody';
export default {
name: 'OpenAPIComponents',
isIdempotent: true,
properties: {
schemas: OpenAPISchemaMap,
responses: OpenAPIResponseMap,
parameters: OpenAPIParameterMap,
examples: OpenAPIExampleMap,
requestBodies: OpenAPIRequestBodyMap,
headers: OpenAPIHeaderMap,
securitySchemes: OpenAPISecuritySchemaMap,
links: OpenAPILinkMap,
callbacks: OpenAPICallbackMap,
},
};

View File

@@ -0,0 +1,8 @@
export default {
name: 'OpenAPIDiscriminator',
isIdempotent: true,
properties: {
propertyName: null,
mapping: null,
},
};

View File

@@ -0,0 +1,14 @@
// eslint-disable-next-line import/no-cycle
import { OpenAPIHeaderMap } from './OpenAPIHeader';
export default {
name: 'OpenAPIEncoding',
isIdempotent: true,
properties: {
contentType: null,
style: null,
explode: null,
allowReserved: null,
headers: OpenAPIHeaderMap,
},
};

View File

@@ -0,0 +1,21 @@
export const OpenAPIExample = {
name: 'OpenAPIExample',
isIdempotent: true,
properties: {
value: null,
externalValue: null,
description: null,
summary: null,
},
};
export const OpenAPIExampleMap = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIExample;
});
return props;
},
};

View File

@@ -0,0 +1,10 @@
const OpenAPIExternalDocumentation = {
name: 'OpenAPIExternalDocumentation',
isIdempotent: true,
properties: {
description: null,
url: null,
},
};
export default OpenAPIExternalDocumentation;

View File

@@ -0,0 +1,32 @@
import { OpenAPIExampleMap } from './OpenAPIExample';
// eslint-disable-next-line import/no-cycle
import { OpenAPIMediaTypeObject } from './OpenAPIMediaObject';
import OpenAPISchemaObject from './OpenAPISchema';
export const OpenAPIHeader = {
name: 'OpenAPIHeader',
isIdempotent: true,
properties: {
description: null,
required: null,
deprecated: null,
allowEmptyValue: null,
explode: null,
allowReserved: null,
example: null,
schema: OpenAPISchemaObject,
content: OpenAPIMediaTypeObject,
examples: OpenAPIExampleMap,
},
};
export const OpenAPIHeaderMap = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIHeader;
});
return props;
},
};

31
src/types/OpenAPIInfo.js Normal file
View File

@@ -0,0 +1,31 @@
export const OpenAPILicense = {
name: 'OpenAPILicense',
isIdempotent: true,
properties: {
name: null,
url: null,
},
};
export const OpenAPIContact = {
name: 'OpenAPIContact',
isIdempotent: true,
properties: {
name: null,
url: null,
email: null,
},
};
export const OpenAPIInfo = {
name: 'OpenAPIInfo',
isIdempotent: true,
properties: {
title: null,
version: null,
description: null,
termsOfService: null,
license: OpenAPILicense,
contact: OpenAPIContact,
},
};

25
src/types/OpenAPILink.js Normal file
View File

@@ -0,0 +1,25 @@
import OpenAPIServer from './OpenAPIServer';
export const OpenAPILink = {
name: 'OpenAPILink',
isIdempotent: true,
properties: {
operationRef: null,
operationId: null,
parameters: null,
description: null,
requestBody: null,
server: OpenAPIServer,
},
};
export const OpenAPILinkMap = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPILink;
});
return props;
},
};

View File

@@ -0,0 +1,27 @@
/* eslint-disable import/no-cycle */
import OpenAPISchema from './OpenAPISchema';
import { OpenAPIExampleMap } from './OpenAPIExample';
import OpenAPIEncoding from './OpenAPIEncoding';
export const OpenAPIMediaObject = {
name: 'OpenAPIMediaObject',
isIdempotent: true,
properties: {
example: null,
schema: OpenAPISchema,
examples: OpenAPIExampleMap,
encoding: OpenAPIEncoding,
},
};
export const OpenAPIMediaTypeObject = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIMediaObject;
});
return props;
},
};

View File

@@ -0,0 +1,29 @@
/* eslint-disable import/no-cycle */
import { OpenAPIResponseMap } from './OpenAPIResponse';
import { OpenAPIParameter } from './OpenAPIParameter';
import OpenAPIServer from './OpenAPIServer';
import OpenAPIExternalDocumentation from './OpenAPIExternalDocumentation';
import { OpenAPICallbackMap } from './OpenAPICallback';
import { OpenAPIRequestBody } from './OpenAPIRequestBody';
export default {
name: 'OpenAPIOperation',
isIdempotent: false,
properties: {
tags: null,
summary: null,
description: null,
operationId: null,
deprecated: null,
security: null,
externalDocs: OpenAPIExternalDocumentation,
parameters: OpenAPIParameter,
requestBody: OpenAPIRequestBody,
responses: OpenAPIResponseMap,
callbacks: OpenAPICallbackMap,
// TODO:
// security() {},
servers: OpenAPIServer,
},
};

View File

@@ -0,0 +1,35 @@
import OpenAPISchemaObject from './OpenAPISchema';
import { OpenAPIMediaTypeObject } from './OpenAPIMediaObject';
import { OpenAPIExampleMap } from './OpenAPIExample';
export const OpenAPIParameter = {
name: 'OpenAPIParameter',
isIdempotent: false,
properties: {
name: null,
in: null,
description: null,
required: null,
deprecated: null,
allowEmptyValue: null,
style: null,
explode: null,
allowReserved: null,
example: null,
schema: OpenAPISchemaObject,
content: OpenAPIMediaTypeObject,
examples: OpenAPIExampleMap,
},
};
export const OpenAPIParameterMap = {
name: 'OpenAPIParameterMap',
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIParameter;
});
return props;
},
};

35
src/types/OpenAPIPaths.js Normal file
View File

@@ -0,0 +1,35 @@
/* eslint-disable import/no-cycle */
import OpenAPIServer from './OpenAPIServer';
import OpenAPIOperation from './OpenAPIOperation';
import { OpenAPIParameter } from './OpenAPIParameter';
export const OpenAPIPathItem = {
name: 'OpenAPIPath',
isIdempotent: true,
properties: {
summary: null,
description: null,
parameters: OpenAPIParameter,
get: OpenAPIOperation,
put: OpenAPIOperation,
post: OpenAPIOperation,
delete: OpenAPIOperation,
options: OpenAPIOperation,
head: OpenAPIOperation,
patch: OpenAPIOperation,
trace: OpenAPIOperation,
servers: OpenAPIServer,
},
};
export const OpenAPIPaths = {
name: 'OpenAPIPaths',
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIPathItem;
});
return props;
},
};

View File

@@ -0,0 +1,22 @@
import { OpenAPIMediaTypeObject } from './OpenAPIMediaObject';
export const OpenAPIRequestBody = {
name: 'OpenAPIRequestBody',
isIdempotent: true,
properties: {
description: null,
required: null,
content: OpenAPIMediaTypeObject,
},
};
export const OpenAPIRequestBodyMap = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIRequestBody;
});
return props;
},
};

View File

@@ -0,0 +1,27 @@
import { OpenAPIMediaTypeObject } from './OpenAPIMediaObject';
import { OpenAPIHeaderMap } from './OpenAPIHeader';
import { OpenAPILinkMap } from './OpenAPILink';
export const OpenAPIResponse = {
name: 'OpenAPIResponse',
isIdempotent: true,
properties: {
description: null,
content: OpenAPIMediaTypeObject,
headers: OpenAPIHeaderMap,
links: OpenAPILinkMap,
},
};
export const OpenAPIResponseMap = {
name: 'OpenAPIResponseMap',
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIResponse;
});
return props;
},
};

View File

@@ -0,0 +1,62 @@
// @ts-check
/* eslint-disable import/no-cycle */
import OpenAPIExternalDocumentation from './OpenAPIExternalDocumentation';
import OpenAPISchemaMap from './OpenAPISchemaMap';
import OpenAPIDiscriminator from './OpenAPIDiscriminator';
import OpenAPIXML from './OpenAPIXML';
const OpenAPISchemaObject = {
name: 'OpenAPISchema',
isIdempotent: true,
properties: {
allOf() {
return OpenAPISchemaObject;
},
anyOf() {
return OpenAPISchemaObject;
},
oneOf() {
return OpenAPISchemaObject;
},
not() {
return OpenAPISchemaObject;
},
items() {
return OpenAPISchemaObject;
},
properties: OpenAPISchemaMap,
discriminator: OpenAPIDiscriminator,
externalDocs: OpenAPIExternalDocumentation,
xml: OpenAPIXML,
title: null,
description: null,
multipleOf: null,
maximum: null,
exclusiveMaximum: null,
minimum: null,
exclusiveMinimum: null,
maxLength: null,
minLength: null,
pattern: null,
maxItems: null,
minItems: null,
uniqueItems: null,
maxProperties: null,
minProperties: null,
required: null,
enum: null,
type: null,
additionalProperties: null,
format: null,
nullable: null,
readOnly: null,
writeOnly: null,
deprecated: null,
example: null,
default: null,
},
};
export default OpenAPISchemaObject;

View File

@@ -0,0 +1,16 @@
/* eslint-disable import/no-cycle */
import OpenAPISchema from './OpenAPISchema';
const OpenAPISchemaMap = {
name: 'OpenAPISchemaMap',
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPISchema;
});
return props;
},
};
export default OpenAPISchemaMap;

View File

@@ -0,0 +1,3 @@
export default {
isIdempotent: true,
};

View File

@@ -0,0 +1,9 @@
export default {
name: 'AuthorizationCodeOpenAPIFlow',
properties: {
authorizationUrl: null,
tokenUrl: null,
refreshUrl: null,
scopes: null,
},
};

View File

@@ -0,0 +1,8 @@
export default {
name: 'ClientCredentialsOpenAPIFlow',
properties: {
tokenUrl: null,
refreshUrl: null,
scopes: null,
},
};

View File

@@ -0,0 +1,8 @@
export default {
name: 'ImplicitOpenAPIFlow',
properties: {
authorizationUrl: null,
refreshUrl: null,
scopes: null,
},
};

View File

@@ -0,0 +1,21 @@
import ImplicitOpenAPIFlow from './ImplicitOpenAPIFlow';
import PasswordOpenAPIFlow from './PasswordOpenAPIFlow';
import ClientCredentialsOpenAPIFlow from './ClientCredentialsOpenAPIFlow';
import AuthorizationCodeOpenAPIFlow from './AuthorizationCodeOpenAPIFlow';
export default {
properties: {
implicit() {
return ImplicitOpenAPIFlow;
},
password() {
return PasswordOpenAPIFlow;
},
clientCredentials() {
return ClientCredentialsOpenAPIFlow;
},
authorizationCode() {
return AuthorizationCodeOpenAPIFlow;
},
},
};

View File

@@ -0,0 +1,15 @@
import OpenAPIFlows from './OpenAPIFlows';
export default {
name: 'OpenAPISecuritySchema',
properties: {
type: null,
description: null,
name: null,
in: null,
scheme: null,
bearerFormat: null,
openIdConnectUrl: null,
flows: OpenAPIFlows,
},
};

View File

@@ -0,0 +1,8 @@
export default {
name: 'PasswordOpenAPIFlow',
properties: {
tokenUrl: null,
refreshUrl: null,
scopes: null,
},
};

View File

@@ -0,0 +1,11 @@
import OpenAPISecuritySchema from './OpenAPISecuritySchema';
export default {
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPISecuritySchema;
});
return props;
},
};

View File

@@ -0,0 +1,34 @@
const OpenAPIServerVariable = {
name: 'OpenAPIServerVariable',
isIdempotent: true,
properties: {
default: null,
description: null,
enum: null,
},
};
const OpenAPIServerVariableMap = {
isIdempotent: true,
properties(node) {
const props = {};
Object.keys(node).forEach((k) => {
props[k] = OpenAPIServerVariable;
});
return props;
},
};
const OpenAPIServer = {
name: 'OpenAPIServer',
isIdempotent: true,
properties: {
url: null,
description: null,
variables() {
return OpenAPIServerVariableMap;
},
},
};
export default OpenAPIServer;

Some files were not shown because too many files have changed in this diff Show More