├── .nvmrc ├── .npmrc ├── .gitignore ├── test ├── mocha.opts └── lib │ ├── all-rules.js │ ├── rules │ ├── root-tags.js │ ├── root-consumes.js │ ├── root-produces.js │ ├── operation-custom.js │ ├── no-path-item-parameters.js │ ├── responses-custom.js │ ├── tags-ref.js │ ├── no-ref-overrides.js │ ├── operation-tags.js │ ├── no-param-required-default.js │ ├── root-info.js │ ├── operation-response-codes.js │ ├── no-path-dupes.js │ ├── no-orphan-refs.js │ ├── info-custom.js │ ├── path-style.js │ ├── operation-payload-put.js │ ├── path-parameters.js │ ├── properties-style.js │ ├── schema-custom.js │ ├── parameters-custom.js │ └── text-content.js │ ├── helpers │ ├── PatternOption.js │ └── TextParser.js │ ├── OpenApiLint.js │ └── constants.js ├── Gemfile ├── CONTRIBUTING.md ├── Rakefile ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── lib ├── RuleFailure.js ├── RuleResult.js ├── constants.js ├── rules │ ├── root-tags.js │ ├── root-consumes.js │ ├── root-produces.js │ ├── info-custom.js │ ├── no-path-item-parameters.js │ ├── root-info.js │ ├── properties-custom.js │ ├── operation-custom.js │ ├── properties-style.js │ ├── schema-custom.js │ ├── no-orphan-refs.js │ ├── no-ref-overrides.js │ ├── no-restricted-words.js │ ├── operation-tags.js │ ├── no-path-dupes.js │ ├── no-param-required-default.js │ ├── tags-ref.js │ ├── parameters-custom.js │ ├── responses-custom.js │ ├── path-style.js │ ├── path-parameters.js │ ├── text-content.js │ ├── operation-response-codes.js │ └── operation-payload-put.js ├── helpers │ ├── PatternOption.js │ ├── TextParser.js │ ├── CustomValidator.js │ └── SchemaObjectParser.js └── OpenApiLint.js ├── .travis.yml ├── docs ├── rules │ ├── root-consumes.md │ ├── root-produces.md │ ├── root-tags.md │ ├── no-path-item-parameters.md │ ├── operation-tags.md │ ├── no-path-dupes.md │ ├── tags-ref.md │ ├── root-info.md │ ├── info-custom.md │ ├── no-param-required-default.md │ ├── no-orphan-refs.md │ ├── operation-custom.md │ ├── responses-custom.md │ ├── no-ref-overrides.md │ ├── operation-response-codes.md │ ├── path-style.md │ ├── no-restricted-words.md │ ├── operation-payload-put.md │ ├── properties-style.md │ ├── parameters-custom.md │ ├── text-content.md │ ├── path-parameters.md │ ├── schema-custom.md │ └── properties-custom.md └── common.md ├── package.json ├── Gemfile.lock ├── LICENSE ├── .eslintrc.json ├── .releasinator.rb ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v4.8.3 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --check-leaks 2 | --recursive 3 | --reporter spec 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'releasinator', '~> 0.6' 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | * Contributions welcome! Feel free to submit a pull request. 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | spec = Gem::Specification.find_by_name 'releasinator' 2 | load "#{spec.gem_dir}/lib/tasks/releasinator.rake" 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Expected or desired behavior: 4 | 5 | ### Actual behavior: 6 | 7 | ### Steps to reproduce: 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Problem 2 | 3 | 4 | 5 | ### Solution 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/RuleFailure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Record = require('immutable').Record; 4 | 5 | class RuleFailure extends Record({ location: '', hint: '' }) { 6 | 7 | } 8 | 9 | module.exports = RuleFailure; 10 | -------------------------------------------------------------------------------- /lib/RuleResult.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Record = require('immutable').Record; 4 | const List = require('immutable').List; 5 | 6 | class RuleResult extends Record({ description: '', failures: new List() }) { 7 | 8 | } 9 | 10 | module.exports = RuleResult; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | notifications: 3 | email: 4 | - external-ci-notifications+openapilint@getbraintree.com 5 | 6 | node_js: 7 | - "4" 8 | - "5" 9 | - "6" 10 | - "7" 11 | - "8" 12 | sudo: false 13 | branches: 14 | only: 15 | - master 16 | 17 | # Run npm test always 18 | script: 19 | - "npm test" 20 | -------------------------------------------------------------------------------- /docs/rules/root-consumes.md: -------------------------------------------------------------------------------- 1 | # enforce present and non-empty `consumes` array (root-consumes) 2 | 3 | Validates that the `consumes` array is present and non-empty. 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "consumes": [ 10 | "application/json" 11 | ] 12 | } 13 | ``` 14 | 15 | ## Examples of **incorrect** usage 16 | 17 | ```json 18 | { 19 | "consumes": [] 20 | } 21 | ``` 22 | 23 | ```json 24 | { 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/rules/root-produces.md: -------------------------------------------------------------------------------- 1 | # enforce present and non-empty `produces` array (root-produces) 2 | 3 | Validates that the `produces` array is present and non-empty. 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "produces": [ 10 | "application/json" 11 | ] 12 | } 13 | ``` 14 | 15 | ## Examples of **incorrect** usage 16 | 17 | ```json 18 | { 19 | "produces": [] 20 | } 21 | ``` 22 | 23 | ```json 24 | { 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/rules/root-tags.md: -------------------------------------------------------------------------------- 1 | # enforce present and non-empty `tags` array (root-tags) 2 | 3 | Validates that the `tags` array is present and non-empty. 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "tags": [ 10 | { 11 | "name": "pet" 12 | }, 13 | { 14 | "name": "animal" 15 | } 16 | ] 17 | } 18 | ``` 19 | 20 | ## Examples of **incorrect** usage 21 | 22 | ```json 23 | { 24 | "tags": [] 25 | } 26 | ``` 27 | 28 | ```json 29 | { 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = { 4 | httpMethods: ['get', 'put', 'post', 'delete', 'options', 'head', 'patch'], 5 | 6 | // braces surrounding something that does not include any braces or whitespace 7 | reValidPathTemplateParam: /^{[^{}\s]+}$/, 8 | 9 | caseStyles: { 10 | spine: /^[a-z0-9-]*$/, 11 | 'cap-spine': /^[A-Z0-9-]*$/, 12 | snake: /^[a-z0-9_]*$/, 13 | any: /^[a-zA-Z0-9-_.]+$/ 14 | } 15 | }; 16 | 17 | module.exports = constants; 18 | -------------------------------------------------------------------------------- /docs/rules/no-path-item-parameters.md: -------------------------------------------------------------------------------- 1 | # enforce not present path item parameters (no-path-item-parameters) 2 | 3 | Validates that the optional path item parameters, at paths./mypath.parameters, is not present. 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "paths": { 10 | "/pets": { 11 | } 12 | } 13 | } 14 | ``` 15 | 16 | ## Examples of **incorrect** usage 17 | ```json 18 | { 19 | "paths": { 20 | "/pets": { 21 | "parameters": [ 22 | ] 23 | } 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /lib/rules/root-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleFailure = require('../RuleFailure'); 4 | const List = require('immutable').List; 5 | 6 | const rule = { 7 | description: 'enforce present and non-empty tags array', 8 | validate(options, schema) { 9 | if (schema.tags) { 10 | if (schema.tags.length > 0) { 11 | // success! 12 | return new List(); 13 | } 14 | 15 | return new List().push(new RuleFailure({ location: 'tags', hint: 'Empty tags' })); 16 | } 17 | 18 | return new List().push(new RuleFailure({ location: 'tags', hint: 'Missing tags' })); 19 | } 20 | }; 21 | 22 | module.exports = rule; 23 | -------------------------------------------------------------------------------- /lib/rules/root-consumes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleFailure = require('../RuleFailure'); 4 | const List = require('immutable').List; 5 | 6 | const rule = { 7 | description: 'enforce present and non-empty consumes array', 8 | validate(options, schema) { 9 | if (schema.consumes) { 10 | if (schema.consumes.length > 0) { 11 | // success! 12 | return new List(); 13 | } 14 | 15 | return new List().push(new RuleFailure({ location: 'consumes', hint: 'Empty consumes' })); 16 | } 17 | 18 | return new List().push(new RuleFailure({ location: 'consumes', hint: 'Missing consumes' })); 19 | } 20 | }; 21 | 22 | module.exports = rule; 23 | -------------------------------------------------------------------------------- /lib/rules/root-produces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleFailure = require('../RuleFailure'); 4 | const List = require('immutable').List; 5 | 6 | const rule = { 7 | description: 'enforce present and non-empty produces array', 8 | validate(options, schema) { 9 | if (schema.produces) { 10 | if (schema.produces.length > 0) { 11 | // success! 12 | return new List(); 13 | } 14 | 15 | return new List().push(new RuleFailure({ location: 'produces', hint: 'Empty produces' })); 16 | } 17 | 18 | return new List().push(new RuleFailure({ location: 'produces', hint: 'Missing produces' })); 19 | } 20 | }; 21 | 22 | module.exports = rule; 23 | -------------------------------------------------------------------------------- /lib/rules/info-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const List = require('immutable').List; 4 | 5 | const CustomValidator = require('../helpers/CustomValidator'); 6 | 7 | const rule = { 8 | description: 'enforce info object complies with custom config constraints', 9 | validate(options, schema) { 10 | const errorList = []; 11 | 12 | const myCustomValidator = new CustomValidator(options, schema, errorList); 13 | 14 | myCustomValidator.validateOptions(); 15 | 16 | if (schema.info) { 17 | myCustomValidator.validateAllCustoms('info', schema.info, 'info', 'info', () => true); 18 | } 19 | 20 | return new List(errorList); 21 | } 22 | }; 23 | 24 | module.exports = rule; 25 | -------------------------------------------------------------------------------- /docs/rules/operation-tags.md: -------------------------------------------------------------------------------- 1 | # enforce present and non-empty operation `tags` array (operation-tags) 2 | 3 | Validates that all operations have a non-empty `tags` array. 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "paths": { 10 | "/pets": { 11 | "get": { 12 | "tags": ["pet"] 13 | } 14 | } 15 | } 16 | } 17 | 18 | ``` 19 | 20 | ## Examples of **incorrect** usage 21 | 22 | ```json 23 | { 24 | "paths": { 25 | "/pets": { 26 | "get": { 27 | "tags": [] 28 | } 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | ```json 35 | { 36 | "paths": { 37 | "/pets": { 38 | "get": { 39 | } 40 | } 41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/rules/no-path-dupes.md: -------------------------------------------------------------------------------- 1 | # enforce paths are logically unique (no-path-dupes) 2 | 3 | Validates that paths that differ only in its path template parameters are not present. 4 | 5 | Example of equivalent paths: 6 | /pets/{pet_id} 7 | /pets/{rascal_id} 8 | 9 | ## Examples of *correct* usage 10 | 11 | ```json 12 | { 13 | "paths": { 14 | "/pets": { 15 | }, 16 | "/pets/{pet_id}": { 17 | }, 18 | "/pets/{pet_id}/feed": { 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | ## Examples of **incorrect** usage 25 | ```json 26 | { 27 | "paths": { 28 | "/pets": { 29 | }, 30 | "/pets/{pet_id}": { 31 | }, 32 | "/pets/{a_different_pet_id}": { 33 | } 34 | } 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/rules/tags-ref.md: -------------------------------------------------------------------------------- 1 | # enforce operation `tags` are present in root level `tags` (tags-ref) 2 | 3 | Validates that any operation `tags` are present in root level `tags`. 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "tags": [ 10 | { 11 | "name": "pet" 12 | } 13 | ], 14 | "paths": { 15 | "/pets": { 16 | "get": { 17 | "tags": ["pet"] 18 | } 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | ## Examples of **incorrect** usage 25 | 26 | ```json 27 | { 28 | "tags": [ 29 | { 30 | "name": "alligator" 31 | } 32 | ], 33 | "paths": { 34 | "/pets": { 35 | "get": { 36 | "tags": ["pet"] 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /lib/rules/no-path-item-parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleFailure = require('../RuleFailure'); 4 | const List = require('immutable').List; 5 | 6 | const rule = { 7 | description: 'enforce not present path item parameters', 8 | validate(options, schema) { 9 | const errorList = []; 10 | 11 | if (schema.paths) { 12 | Object.keys(schema.paths).forEach((pathKey) => { 13 | const path = schema.paths[pathKey]; 14 | 15 | if (path.parameters) { 16 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.parameters`, hint: 'Found parameters' })); 17 | } 18 | }); 19 | } 20 | 21 | return new List(errorList); 22 | } 23 | }; 24 | 25 | module.exports = rule; 26 | -------------------------------------------------------------------------------- /lib/rules/root-info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleFailure = require('../RuleFailure'); 4 | const List = require('immutable').List; 5 | 6 | const rule = { 7 | description: 'enforce present and valid info object', 8 | validate(options, schema) { 9 | const errorList = []; 10 | 11 | if (schema.info) { 12 | if (!schema.info.title) { 13 | errorList.push(new RuleFailure({ location: 'info', hint: 'Missing info.title' })); 14 | } 15 | 16 | if (!schema.info.version) { 17 | errorList.push(new RuleFailure({ location: 'info', hint: 'Missing info.version' })); 18 | } 19 | } else { 20 | errorList.push(new RuleFailure({ location: 'info', hint: 'Missing info' })); 21 | } 22 | 23 | return new List(errorList); 24 | } 25 | }; 26 | 27 | module.exports = rule; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapilint", 3 | "version": "0.10.0", 4 | "description": "Node.js linter for OpenAPI specs", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/braintree/openapilint.git" 8 | }, 9 | "main": "./lib/OpenApiLint.js", 10 | "license": "MIT", 11 | "engines": { 12 | "node": ">=4.2" 13 | }, 14 | "scripts": { 15 | "lint": "eslint *.js ./lib ./test", 16 | "pretest": "npm run lint", 17 | "test": "mocha" 18 | }, 19 | "files": [ 20 | "lib" 21 | ], 22 | "dependencies": { 23 | "immutable": "^3.8.1", 24 | "lodash": "^3.3.1" 25 | }, 26 | "devDependencies": { 27 | "chai": "^4.0.2", 28 | "chai-as-promised": "^7.1.1", 29 | "chai-spies": "^0.7.1", 30 | "eslint": "^4.8.0", 31 | "eslint-config-airbnb-base": "^11.3.2", 32 | "eslint-plugin-import": "^2.7.0", 33 | "mocha": "^3.4.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/rules/properties-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const List = require('immutable').List; 4 | 5 | const CustomValidator = require('../helpers/CustomValidator'); 6 | const SchemaObjectParser = require('../helpers/SchemaObjectParser'); 7 | 8 | const rule = { 9 | description: 'enforce properties comply with custom config constraints', 10 | validate(options, schema) { 11 | const errorList = []; 12 | 13 | const myCustomValidator = new CustomValidator(options, schema, errorList); 14 | const mySchemaObjectParser = new SchemaObjectParser(schema, errorList); 15 | 16 | myCustomValidator.validateOptions(); 17 | mySchemaObjectParser.forEachProperty((propertyKey, propertyObject, pathToProperty) => { 18 | myCustomValidator.validateAllCustoms(propertyKey, propertyObject, pathToProperty, 'property', () => true); 19 | }); 20 | 21 | return new List(errorList); 22 | } 23 | }; 24 | 25 | module.exports = rule; 26 | -------------------------------------------------------------------------------- /docs/rules/root-info.md: -------------------------------------------------------------------------------- 1 | # enforce present and valid `info` object (root-info) 2 | 3 | Validates that the `info` object is present and valid to the specification, which states that [the `info` object is required](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object), and within it, the [`title` and `version` properties are required](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#infoObject). 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "info": { 10 | "title": "The title", 11 | "version": "1.3" 12 | } 13 | } 14 | ``` 15 | 16 | ## Examples of **incorrect** usage 17 | 18 | ```json 19 | { 20 | } 21 | ``` 22 | 23 | ```json 24 | { 25 | "info": {} 26 | } 27 | ``` 28 | 29 | ```json 30 | { 31 | "info": { 32 | "version": "1.3" 33 | } 34 | } 35 | ``` 36 | 37 | ```json 38 | { 39 | "info": { 40 | "title": "The title" 41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.5.0) 5 | public_suffix (~> 2.0, >= 2.0.2) 6 | colorize (0.8.1) 7 | configatron (4.5.0) 8 | faraday (0.11.0) 9 | multipart-post (>= 1.2, < 3) 10 | github-markup (1.4.1) 11 | json (1.8.6) 12 | multipart-post (2.0.0) 13 | octokit (4.6.2) 14 | sawyer (~> 0.8.0, >= 0.5.3) 15 | public_suffix (2.0.5) 16 | redcarpet (3.3.4) 17 | releasinator (0.6.4) 18 | colorize (~> 0.7) 19 | configatron (~> 4.5) 20 | json (~> 1.8) 21 | octokit (~> 4.0) 22 | semantic (~> 1.4) 23 | vandamme (~> 0.0.11) 24 | sawyer (0.8.1) 25 | addressable (>= 2.3.5, < 2.6) 26 | faraday (~> 0.8, < 1.0) 27 | semantic (1.6.0) 28 | vandamme (0.0.11) 29 | github-markup (~> 1.3) 30 | redcarpet (~> 3.3.2) 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | releasinator (~> 0.6) 37 | 38 | BUNDLED WITH 39 | 1.14.4 40 | -------------------------------------------------------------------------------- /docs/rules/info-custom.md: -------------------------------------------------------------------------------- 1 | # enforce info object complies with custom config constraints (properties-custom) 2 | 3 | Validates that properties match their provided constraints. Constraints are provided in a simple format: 4 | 5 | 1. When `whenField` matches `whenPattern`, 6 | 2. Then `thenField` MUST match `thenPattern`. 7 | 8 | This format works for almost any constraint. `xField` is the name of the info object's field. `xPattern` is a properly escaped regex string. For more than one constraint, use an array of constraint options. 9 | 10 | ## Config A 11 | 12 | Validates that info.description is present and non-empty. 13 | 14 | ```json 15 | { 16 | "whenField": "$key", 17 | "whenPattern": ".*", 18 | "thenField": "description", 19 | "thenPattern": "[a-zA-Z]", 20 | } 21 | ``` 22 | 23 | ### Examples of *correct* usage with above config 24 | 25 | ```json 26 | { 27 | "info": { 28 | "description": "The description" 29 | } 30 | } 31 | ``` 32 | 33 | ### Examples of *incorrect* usage with above config 34 | 35 | ```json 36 | { 37 | "info": { 38 | } 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/rules/no-param-required-default.md: -------------------------------------------------------------------------------- 1 | # enforce required parameters do not have a default value (no-param-required-default) 2 | 3 | Validates that required parameters do not have a `default` field, as defined in the [OpenAPI 2.0 spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#fixed-fields-7). 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "paths": { 10 | "/first/{id}": { 11 | "get": { 12 | "parameters": [ 13 | { 14 | "name": "id", 15 | "type": "string", 16 | "in": "path", 17 | "required": true 18 | } 19 | ] 20 | } 21 | } 22 | } 23 | } 24 | ``` 25 | 26 | ## Examples of **incorrect** usage 27 | ```json 28 | { 29 | "paths": { 30 | "/first/{id}": { 31 | "get": { 32 | "parameters": [ 33 | { 34 | "name": "id", 35 | "type": "string", 36 | "in": "path", 37 | "required": true, 38 | "default": "default_value" 39 | } 40 | ] 41 | } 42 | } 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /lib/rules/operation-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const List = require('immutable').List; 5 | 6 | const constants = require('../constants'); 7 | const CustomValidator = require('../helpers/CustomValidator'); 8 | 9 | const rule = { 10 | description: 'enforce operation objects comply with custom config constraints', 11 | validate(options, schema) { 12 | const errorList = []; 13 | 14 | const myCustomValidator = new CustomValidator(options, schema, errorList); 15 | 16 | myCustomValidator.validateOptions(); 17 | 18 | if (schema.paths) { 19 | Object.keys(schema.paths).forEach((pathKey) => { 20 | const path = schema.paths[pathKey]; 21 | 22 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 23 | const operationObject = path[operationKey]; 24 | 25 | myCustomValidator.validateAllCustoms(operationKey, operationObject, `paths.${pathKey}.${operationKey}`, 'operation', () => true); 26 | }); 27 | }); 28 | } 29 | 30 | return new List(errorList); 31 | } 32 | }; 33 | 34 | module.exports = rule; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Braintree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/rules/properties-style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../constants'); 4 | const RuleFailure = require('../RuleFailure'); 5 | const List = require('immutable').List; 6 | 7 | const SchemaObjectParser = require('../helpers/SchemaObjectParser'); 8 | 9 | const rule = { 10 | description: 'enforce all properties\' keys conform to a specified input style', 11 | validate(options, schema) { 12 | if (!options 13 | || !options.case 14 | || Object.keys(constants.caseStyles).indexOf(options.case) === -1) { 15 | throw new Error('Invalid config specified'); 16 | } 17 | 18 | const errorList = []; 19 | const mySchemaObjectParser = new SchemaObjectParser(schema, errorList); 20 | 21 | mySchemaObjectParser.forEachProperty((propertyKey, propertyObject, pathToProperty) => { 22 | if (!propertyKey.match(constants.caseStyles[options.case])) { 23 | errorList.push(new RuleFailure({ location: pathToProperty, hint: `"${propertyKey}" does not comply with case: "${options.case}"` })); 24 | } 25 | }); 26 | 27 | return new List(errorList); 28 | } 29 | }; 30 | 31 | module.exports = rule; 32 | -------------------------------------------------------------------------------- /lib/rules/schema-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const List = require('immutable').List; 4 | 5 | const CustomValidator = require('../helpers/CustomValidator'); 6 | const SchemaObjectParser = require('../helpers/SchemaObjectParser'); 7 | 8 | const rule = { 9 | description: 'enforce schema objects comply with custom config constraints', 10 | validate(options, schema) { 11 | const errorList = []; 12 | 13 | const myCustomValidator = new CustomValidator(options, schema, errorList); 14 | const mySchemaObjectParser = new SchemaObjectParser(schema, errorList); 15 | 16 | myCustomValidator.validateOptions(); 17 | mySchemaObjectParser.forEachSchema((schemaObject, pathToSchema) => { 18 | myCustomValidator.validateAllCustoms(undefined, schemaObject, pathToSchema, 'schema', 19 | // returning true indicates the rule should be run. 20 | option => 21 | schemaObject.openapilintType === undefined 22 | || (!!option.alsoApplyTo 23 | && option.alsoApplyTo.indexOf(schemaObject.openapilintType) > -1) 24 | ); 25 | }); 26 | 27 | return new List(errorList); 28 | } 29 | }; 30 | 31 | module.exports = rule; 32 | -------------------------------------------------------------------------------- /lib/rules/no-orphan-refs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const RuleFailure = require('../RuleFailure'); 6 | const List = require('immutable').List; 7 | 8 | const SchemaObjectParser = require('../helpers/SchemaObjectParser'); 9 | 10 | const rule = { 11 | description: 'enforce all refs are reachable', 12 | validate(options, schema) { 13 | const errorList = []; 14 | const mySchemaObjectParser = new SchemaObjectParser(schema, errorList); 15 | 16 | const visitedRefsList = mySchemaObjectParser.getVisitedRefs(); 17 | 18 | if (schema.definitions) { 19 | Object.keys(schema.definitions).forEach((definition) => { 20 | const definitionPath = `#/definitions/${definition}`; 21 | 22 | if (!_.include(visitedRefsList, definitionPath)) { 23 | errorList.push(new RuleFailure({ 24 | location: `definitions.${definition}`, 25 | hint: 'Definition is not reachable' 26 | })); 27 | } 28 | }); 29 | } 30 | 31 | // TODO check other referenced fields once this project supports them. 32 | 33 | return new List(errorList); 34 | } 35 | }; 36 | 37 | module.exports = rule; 38 | -------------------------------------------------------------------------------- /docs/rules/no-orphan-refs.md: -------------------------------------------------------------------------------- 1 | # enforce all refs are reachable (no-orphan-refs) 2 | 3 | Validates that all `$ref`s are reachable. 4 | 5 | ## Example of *correct* usage 6 | 7 | ```json 8 | { 9 | "definitions": { 10 | "Pet": { 11 | }, 12 | "Pets": { 13 | "type": "array", 14 | "items": { 15 | "$ref": "#/definitions/Pet" 16 | } 17 | } 18 | }, 19 | "paths": { 20 | "/pets": { 21 | "get": { 22 | "responses": { 23 | "200": { 24 | "schema": { 25 | "$ref": "#/definitions/Pets" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | ## Examples of **incorrect** usage 36 | 37 | ```json 38 | { 39 | "definitions": { 40 | "Pet": { 41 | }, 42 | "Pets": { 43 | "type": "array", 44 | "items": { 45 | "type": "object" 46 | } 47 | } 48 | }, 49 | "paths": { 50 | "/pets": { 51 | "get": { 52 | "responses": { 53 | "200": { 54 | "schema": { 55 | "$ref": "#/definitions/Pets" 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /lib/rules/no-ref-overrides.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const RuleFailure = require('../RuleFailure'); 6 | const List = require('immutable').List; 7 | 8 | const SchemaObjectParser = require('../helpers/SchemaObjectParser'); 9 | 10 | const rule = { 11 | description: 'enforce no refs have overrides', 12 | validate(options, schema) { 13 | const errorList = []; 14 | const mySchemaObjectParser = new SchemaObjectParser(schema, errorList); 15 | 16 | const normalizedOptions = options || {}; 17 | const normalizedAllowProperties = normalizedOptions.allowProperties || []; 18 | 19 | mySchemaObjectParser.forEachSchema(undefined, undefined, (schemaObject, pathToSchema) => { 20 | Object.keys(_.omit(schemaObject, normalizedAllowProperties.concat(['$ref', 'openapilintType']))).forEach((schemaKey) => { 21 | errorList.push(new RuleFailure({ 22 | location: `${pathToSchema}.${schemaKey}#override`, 23 | hint: 'Found $ref object override' 24 | })); 25 | }); 26 | }); 27 | 28 | // TODO check other referenced fields once this project supports them. 29 | 30 | return new List(errorList); 31 | } 32 | }; 33 | 34 | module.exports = rule; 35 | -------------------------------------------------------------------------------- /test/lib/all-rules.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const _ = require('lodash'); 5 | 6 | const List = require('immutable').List; 7 | 8 | const rulesPath = 'lib/rules'; 9 | 10 | describe('all-rules should have common attributes', () => { 11 | fs.readdir(rulesPath, (err, items) => { 12 | assert.isNull(err); 13 | assert.isTrue(items.length > 0); 14 | 15 | _.filter(items, (element) => { /\.js$/.test(element); }).forEach((element) => { 16 | const requiredName = path.parse(element).name; 17 | 18 | // eslint-disable-next-line global-require, import/no-dynamic-require 19 | const rule = require(`../../${rulesPath}/${requiredName}`); 20 | 21 | it(`${requiredName} should have common attributes`, () => { 22 | // rules should have a non-empty description 23 | assert.isDefined(rule.description); 24 | assert.isTrue(rule.description.length > 0); 25 | 26 | // validating a rule should return a failure list 27 | const result = rule.validate({}, {}); 28 | 29 | assert.isTrue(result instanceof List, `${requiredName} returned a ${result} instead of a List`); 30 | }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /lib/rules/no-restricted-words.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const List = require('immutable').List; 4 | 5 | const RuleFailure = require('../RuleFailure'); 6 | const TextParser = require('../helpers/TextParser'); 7 | 8 | const rule = { 9 | description: 'enforce certain words or phrases are not included in text fields, including title, summary, or description', 10 | 11 | validate(options, schema) { 12 | const normalizedOptions = options || {}; 13 | const normalizedRestrictedWords = normalizedOptions.words || []; 14 | 15 | if (normalizedRestrictedWords.length === 0) { 16 | return new List([]); 17 | } 18 | 19 | const errorList = []; 20 | const myTextParser = new TextParser(schema, errorList); 21 | const applyTo = ['description', 'summary', 'title']; 22 | 23 | myTextParser.forEachTextField(applyTo, (field, pathToField) => { 24 | normalizedRestrictedWords.forEach((word) => { 25 | if (new RegExp(`\\b${word}\\b`, 'i').test(field)) { 26 | errorList.push(new RuleFailure({ 27 | location: pathToField, 28 | hint: `Found '${field}'` 29 | })); 30 | } 31 | }); 32 | }); 33 | 34 | return new List(errorList); 35 | } 36 | }; 37 | 38 | module.exports = rule; 39 | -------------------------------------------------------------------------------- /lib/rules/operation-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const constants = require('../constants'); 6 | const RuleFailure = require('../RuleFailure'); 7 | const List = require('immutable').List; 8 | 9 | const rule = { 10 | description: 'enforce present and non-empty operation tags array', 11 | validate(options, schema) { 12 | const errorList = []; 13 | 14 | if (schema.paths) { 15 | Object.keys(schema.paths).forEach((pathKey) => { 16 | const path = schema.paths[pathKey]; 17 | 18 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 19 | const operation = path[operationKey]; 20 | 21 | if (operation.tags) { 22 | if (operation.tags.length === 0) { 23 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.${operationKey}.tags`, hint: 'Empty tags' })); 24 | } else { 25 | // success, yay! 26 | } 27 | } else { 28 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.${operationKey}`, hint: 'Missing tags' })); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | return new List(errorList); 35 | } 36 | }; 37 | 38 | module.exports = rule; 39 | -------------------------------------------------------------------------------- /docs/rules/operation-custom.md: -------------------------------------------------------------------------------- 1 | # enforce operation objects comply with custom config constraints (operation-custom) 2 | 3 | Validates that operation objects match their provided constraints. Constraints are provided in a simple format: 4 | 5 | 1. When `whenField` matches `whenPattern`, 6 | 2. Then `thenField` MUST match `thenPattern`. 7 | 8 | This format works for almost any constraint. `xField` is the name of the operation's field, or `$key` to indicate the operation's key. `xPattern` is a properly escaped regex string. For more than one constraint, use an array of constraint options. 9 | 10 | ## Config A 11 | 12 | Validates that any operation object must have a summary. 13 | 14 | ```json 15 | { 16 | "whenField": "$key", 17 | "whenPattern": ".*", 18 | "thenField": "summary", 19 | "thenPattern": "[a-zA-Z]", 20 | } 21 | 22 | ``` 23 | 24 | ### Examples of *correct* usage with above config 25 | 26 | ```json 27 | { 28 | "paths": { 29 | "/pets": { 30 | "get": { 31 | "summary": "The get pets summary", 32 | } 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ### Examples of *incorrect* usage with above config 39 | 40 | ```json 41 | { 42 | "paths": { 43 | "/pets": { 44 | "get": { 45 | } 46 | } 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /test/lib/rules/root-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const rootTagsRule = require('../../../lib/rules/root-tags'); 5 | 6 | describe('root-tags', () => { 7 | const options = true; 8 | 9 | it('should not report errors when tags is present and non-empty', () => { 10 | const schema = { 11 | tags: [ 12 | { name: 'tag1' }, 13 | { name: 'tag2' } 14 | ] 15 | }; 16 | 17 | const failures = rootTagsRule.validate(options, schema); 18 | 19 | assert.equal(failures.size, 0); 20 | }); 21 | 22 | it('should report error when tags is not present', () => { 23 | const schema = {}; 24 | 25 | const failures = rootTagsRule.validate(options, schema); 26 | 27 | assert.equal(failures.size, 1); 28 | assert.equal(failures.get(0).get('location'), 'tags'); 29 | assert.equal(failures.get(0).get('hint'), 'Missing tags'); 30 | }); 31 | 32 | it('should report error when tags is empty', () => { 33 | const schema = { 34 | tags: [] 35 | }; 36 | 37 | const failures = rootTagsRule.validate(options, schema); 38 | 39 | assert.equal(failures.size, 1); 40 | assert.equal(failures.get(0).get('location'), 'tags'); 41 | assert.equal(failures.get(0).get('hint'), 'Empty tags'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /lib/rules/no-path-dupes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const constants = require('../constants'); 6 | const RuleFailure = require('../RuleFailure'); 7 | const List = require('immutable').List; 8 | 9 | const rule = { 10 | description: 'enforce paths are logically unique', 11 | validate(options, schema) { 12 | const errorList = []; 13 | 14 | if (schema.paths) { 15 | const normalizedPaths = []; 16 | 17 | Object.keys(schema.paths).forEach((pathKey) => { 18 | // construct a normalized path 19 | let normalizedPath = ''; 20 | 21 | _.each(pathKey.split('/'), (param, index) => { 22 | if (param.match(constants.reValidPathTemplateParam)) { 23 | normalizedPath += `/{pathTemplate${index}}`; 24 | } else { 25 | normalizedPath += `/${param}`; 26 | } 27 | }); 28 | 29 | // error if normalized path already exists 30 | if (_.includes(normalizedPaths, normalizedPath)) { 31 | errorList.push(new RuleFailure({ location: `paths.${pathKey}`, hint: 'Found duplicate path' })); 32 | } else { 33 | normalizedPaths.push(normalizedPath); 34 | } 35 | }); 36 | } 37 | 38 | return new List(errorList); 39 | } 40 | }; 41 | 42 | module.exports = rule; 43 | -------------------------------------------------------------------------------- /test/lib/rules/root-consumes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const rootConsumesRule = require('../../../lib/rules/root-consumes'); 5 | 6 | describe('root-consumes', () => { 7 | const options = true; 8 | 9 | it('should not report errors when consumes is present and non-empty', () => { 10 | const schema = { 11 | consumes: [ 12 | 'application/json' 13 | ] 14 | }; 15 | 16 | const failures = rootConsumesRule.validate(options, schema); 17 | 18 | assert.equal(failures.size, 0); 19 | }); 20 | 21 | it('should report error when consumes is not present', () => { 22 | const schema = {}; 23 | 24 | const failures = rootConsumesRule.validate(options, schema); 25 | 26 | assert.equal(failures.size, 1); 27 | assert.equal(failures.get(0).get('location'), 'consumes'); 28 | assert.equal(failures.get(0).get('hint'), 'Missing consumes'); 29 | }); 30 | 31 | it('should report error when consumes is empty', () => { 32 | const schema = { 33 | consumes: [] 34 | }; 35 | 36 | const failures = rootConsumesRule.validate(options, schema); 37 | 38 | assert.equal(failures.size, 1); 39 | assert.equal(failures.get(0).get('location'), 'consumes'); 40 | assert.equal(failures.get(0).get('hint'), 'Empty consumes'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/lib/rules/root-produces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const rootProducesRule = require('../../../lib/rules/root-produces'); 5 | 6 | describe('root-produces', () => { 7 | const options = true; 8 | 9 | it('should not report errors when produces is present and non-empty', () => { 10 | const schema = { 11 | produces: [ 12 | 'application/json' 13 | ] 14 | }; 15 | 16 | const failures = rootProducesRule.validate(options, schema); 17 | 18 | assert.equal(failures.size, 0); 19 | }); 20 | 21 | it('should report error when produces is not present', () => { 22 | const schema = {}; 23 | 24 | const failures = rootProducesRule.validate(options, schema); 25 | 26 | assert.equal(failures.size, 1); 27 | assert.equal(failures.get(0).get('location'), 'produces'); 28 | assert.equal(failures.get(0).get('hint'), 'Missing produces'); 29 | }); 30 | 31 | it('should report error when produces is empty', () => { 32 | const schema = { 33 | produces: [] 34 | }; 35 | 36 | const failures = rootProducesRule.validate(options, schema); 37 | 38 | assert.equal(failures.size, 1); 39 | assert.equal(failures.get(0).get('location'), 'produces'); 40 | assert.equal(failures.get(0).get('hint'), 'Empty produces'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /lib/rules/no-param-required-default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const constants = require('../constants'); 6 | const RuleFailure = require('../RuleFailure'); 7 | const List = require('immutable').List; 8 | 9 | const rule = { 10 | description: 'enforce path parameter parity', 11 | validate(options, schema) { 12 | const errorList = []; 13 | 14 | if (schema.paths) { 15 | Object.keys(schema.paths).forEach((pathKey) => { 16 | const path = schema.paths[pathKey]; 17 | 18 | // check each operation 19 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 20 | const operation = path[operationKey]; 21 | 22 | if (operation.parameters) { 23 | operation.parameters.forEach((parameter, parameterIndex) => { 24 | if (parameter.required && !!parameter.default) { 25 | errorList.push(new RuleFailure( 26 | { 27 | location: `paths.${pathKey}.${operationKey}.parameters[${parameterIndex}]`, 28 | hint: 'Expected required parameter to not have a default value.' 29 | } 30 | )); 31 | } 32 | }); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | return new List(errorList); 39 | } 40 | }; 41 | 42 | module.exports = rule; 43 | -------------------------------------------------------------------------------- /lib/rules/tags-ref.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const constants = require('../constants'); 6 | const RuleFailure = require('../RuleFailure'); 7 | const List = require('immutable').List; 8 | 9 | const rule = { 10 | description: 'enforce operation tags are present in root level tags', 11 | validate(options, schema) { 12 | const errorList = []; 13 | 14 | if (schema.paths) { 15 | let tagNames; 16 | 17 | if (schema.tags) { 18 | tagNames = schema.tags.map(tag => tag.name); 19 | } else { 20 | tagNames = []; 21 | } 22 | 23 | Object.keys(schema.paths).forEach((pathKey) => { 24 | const path = schema.paths[pathKey]; 25 | 26 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 27 | const operation = path[operationKey]; 28 | 29 | if (operation.tags) { 30 | operation.tags.forEach((tag, tagIndex) => { 31 | if (!_.includes(tagNames, tag)) { 32 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.${operationKey}.tags[${tagIndex}]`, hint: 'Tag not found' })); 33 | } 34 | }); 35 | } else { 36 | // no tags to check 37 | } 38 | }); 39 | }); 40 | } 41 | 42 | return new List(errorList); 43 | } 44 | }; 45 | 46 | module.exports = rule; 47 | -------------------------------------------------------------------------------- /docs/rules/responses-custom.md: -------------------------------------------------------------------------------- 1 | # enforce responses comply with custom config constraints (responses-custom) 2 | 3 | Validates that responses match their provided constraints. Constraints are provided in a simple format: 4 | 5 | 1. When `whenField` matches `whenPattern`, 6 | 2. Then `thenField` MUST match `thenPattern`. 7 | 8 | This format works for almost any constraint. `xField` is the name of the response's field, or `$key` to indicate the response's key. `xPattern` is a properly escaped regex string. For more than one constraint, use an array of constraint options. 9 | 10 | ## Config A 11 | 12 | Validates that all response keys are either `200` or `201`. 13 | 14 | ```json 15 | { 16 | "whenField": "$key", 17 | "whenPattern": "\\.*", 18 | "thenField": "$key", 19 | "thenPattern": "^(200|201)$" 20 | } 21 | 22 | ``` 23 | 24 | ### Examples of *correct* usage with above config 25 | 26 | ```json 27 | { 28 | "paths": { 29 | "/pets": { 30 | "get": { 31 | "responses": { 32 | "200": { 33 | }, 34 | "201": { 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | ### Examples of *incorrect* usage with above config 44 | 45 | ```json 46 | { 47 | "paths": { 48 | "/pets": { 49 | "get": { 50 | "responses": { 51 | "999": { 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/rules/no-ref-overrides.md: -------------------------------------------------------------------------------- 1 | # enforce no refs have overrides (no-ref-overrides) 2 | 3 | Some projects decide to support overriding a `$ref`'s value. This isn't supported in OpenAPI, as mentioned in [this OpenAPI specification issue](https://github.com/OAI/OpenAPI-Specification/issues/556). 4 | 5 | This rule validates that there are no properties specified in the same object as a `$ref`, except those allowed by the configuration. 6 | 7 | ## Example of *correct* usage given the config: `{"allowProperties": ["description"]}` 8 | 9 | ```json 10 | { 11 | "definitions": { 12 | "Pet": {} 13 | }, 14 | "paths": { 15 | "/pets/{id}": { 16 | "get": { 17 | "responses": { 18 | "200": { 19 | "schema": { 20 | "description": "A description override.", 21 | "$ref": "#/definitions/Pet" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ## Example of **incorrect** usage 32 | 33 | ```json 34 | { 35 | "definitions": { 36 | "Pet": {} 37 | }, 38 | "paths": { 39 | "/pets/{id}": { 40 | "get": { 41 | "responses": { 42 | "200": { 43 | "schema": { 44 | "type": "object", 45 | "description": "A description override.", 46 | "$ref": "#/definitions/Pet" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/rules/operation-response-codes.md: -------------------------------------------------------------------------------- 1 | # enforce operation response codes comply with custom key constraints (operation-response-codes) 2 | 3 | Validates that operations of a specific http method match custom response code constraints. Constraints are provided in a simple format: 4 | 5 | 1. When `whenHttpMethod` matches the operation's http method, 6 | 2. Then the response key MUST match the `thenResponseCodePattern`. 7 | 8 | `whenHttpMethod` is the name of the operation's http method, such as `put`, `post`, or `get`. `thenResponseCodePattern` is a properly escaped regex string. For more than one constraint, use an array of constraint options. 9 | 10 | ## Config A 11 | 12 | Validates that `get` methods only emit `200` or `default` response keys. 13 | 14 | ```json 15 | { 16 | "whenHttpMethod": "get", 17 | "thenResponseCodePattern": "(200|default)" 18 | } 19 | ``` 20 | 21 | ### Examples of *correct* usage with above config 22 | 23 | ```json 24 | { 25 | "paths": { 26 | "/pets": { 27 | "get": { 28 | "responses": { 29 | "200": { 30 | }, 31 | "default": { 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | ### Examples of *incorrect* usage with above config 41 | 42 | ```json 43 | { 44 | "paths": { 45 | "/pets": { 46 | "get": { 47 | "responses": { 48 | "204": { 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /test/lib/rules/operation-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const operationCustomRule = require('../../../lib/rules/operation-custom'); 5 | 6 | describe('operation-custom', () => { 7 | describe('summary must have non-whitespace summaries', () => { 8 | const basicOptions = { 9 | whenField: '$key', 10 | whenPattern: '.*', 11 | thenField: 'summary', 12 | thenPattern: '[a-zA-Z]' 13 | }; 14 | 15 | it('should not report errors when titles are present and valid', () => { 16 | const schema = { 17 | paths: { 18 | '/pets': { 19 | get: { 20 | summary: 'The get pets summary' 21 | } 22 | } 23 | } 24 | }; 25 | 26 | const failures = operationCustomRule.validate(basicOptions, schema); 27 | 28 | assert.equal(failures.size, 0); 29 | }); 30 | 31 | it('should report errors when summary is not present', () => { 32 | const schema = { 33 | paths: { 34 | '/pets': { 35 | get: { 36 | } 37 | } 38 | } 39 | }; 40 | 41 | const failures = operationCustomRule.validate([basicOptions], schema); 42 | 43 | assert.equal(failures.size, 1); 44 | assert.equal(failures.get(0).get('location'), 'paths./pets.get'); 45 | assert.equal(failures.get(0).get('hint'), 'Expected operation summary to be present and to match "[a-zA-Z]"'); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /lib/rules/parameters-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const List = require('immutable').List; 6 | 7 | const constants = require('../constants'); 8 | const CustomValidator = require('../helpers/CustomValidator'); 9 | 10 | const rule = { 11 | description: 'enforce parameters comply with custom config constraints', 12 | validate(options, schema) { 13 | const errorList = []; 14 | 15 | const myCustomValidator = new CustomValidator(options, schema, errorList); 16 | 17 | myCustomValidator.validateOptions(); 18 | 19 | if (schema.paths) { 20 | Object.keys(schema.paths).forEach((pathKey) => { 21 | const path = schema.paths[pathKey]; 22 | 23 | // check each operation 24 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 25 | const operation = path[operationKey]; 26 | 27 | if (operation.parameters) { 28 | operation.parameters.forEach((parameter, parameterIndex) => { 29 | myCustomValidator.validateAllCustoms( 30 | undefined, // parameters have no key name coming from a list. 31 | parameter, 32 | `paths.${pathKey}.${operationKey}.parameters[${parameterIndex}]`, 33 | 'parameter', 34 | () => true 35 | ); 36 | }); 37 | } 38 | }); 39 | }); 40 | } 41 | 42 | return new List(errorList); 43 | } 44 | }; 45 | 46 | module.exports = rule; 47 | -------------------------------------------------------------------------------- /lib/rules/responses-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const List = require('immutable').List; 5 | 6 | const constants = require('../constants'); 7 | const CustomValidator = require('../helpers/CustomValidator'); 8 | 9 | const rule = { 10 | description: 'enforce responses comply with custom config constraints', 11 | validate(options, schema) { 12 | const errorList = []; 13 | 14 | const myCustomValidator = new CustomValidator(options, schema, errorList); 15 | 16 | myCustomValidator.validateOptions(); 17 | 18 | if (schema.paths) { 19 | Object.keys(schema.paths).forEach((pathKey) => { 20 | const path = schema.paths[pathKey]; 21 | 22 | // check each operation 23 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 24 | const operation = path[operationKey]; 25 | 26 | const responses = operation.responses; 27 | 28 | if (responses) { 29 | Object.keys(responses).forEach((responseKey) => { 30 | const response = responses[responseKey]; 31 | 32 | myCustomValidator.validateAllCustoms( 33 | responseKey, 34 | response, 35 | `paths.${pathKey}.${operationKey}.responses.${responseKey}`, 36 | 'responses', 37 | () => true 38 | ); 39 | }); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | 46 | return new List(errorList); 47 | } 48 | }; 49 | 50 | module.exports = rule; 51 | -------------------------------------------------------------------------------- /test/lib/rules/no-path-item-parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const noPathItemParametersRule = require('../../../lib/rules/no-path-item-parameters'); 5 | 6 | describe('no-path-item-parameters', () => { 7 | const options = true; 8 | 9 | it('should not report errors when all paths do not have parameters', () => { 10 | const schema = { 11 | paths: { 12 | '/pets': { 13 | get: { 14 | }, 15 | put: { 16 | } 17 | }, 18 | '/people': { 19 | get: { 20 | } 21 | } 22 | } 23 | }; 24 | 25 | const failures = noPathItemParametersRule.validate(options, schema); 26 | 27 | assert.equal(failures.size, 0); 28 | }); 29 | 30 | it('should report two errors when two paths have parameters', () => { 31 | const schema = { 32 | paths: { 33 | '/pets': { 34 | get: { 35 | }, 36 | put: { 37 | }, 38 | parameters: [] 39 | }, 40 | '/people': { 41 | parameters: [] 42 | } 43 | } 44 | }; 45 | 46 | const failures = noPathItemParametersRule.validate(options, schema); 47 | 48 | assert.equal(failures.size, 2); 49 | assert.equal(failures.get(0).get('location'), 'paths./pets.parameters'); 50 | assert.equal(failures.get(0).get('hint'), 'Found parameters'); 51 | assert.equal(failures.get(1).get('location'), 'paths./people.parameters'); 52 | assert.equal(failures.get(1).get('hint'), 'Found parameters'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | 4 | "env": { 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | 10 | "rules": { 11 | // Don't allow any dangling commas 12 | "comma-dangle": ["error", "never"], 13 | 14 | // Allow devDependencies in this directory, since it is for scripts only. 15 | "import/no-extraneous-dependencies": ["error", { 16 | "devDependencies": true 17 | }], 18 | 19 | // Require spaces before block comments... 20 | "lines-around-comment": [2, { 21 | // ...except when first line of array, or... 22 | "allowArrayStart": true, 23 | // ...except when first line of block, or... 24 | "allowBlockStart": true, 25 | // ...except when first line of object. 26 | "allowObjectStart": true 27 | }], 28 | 29 | // Visually set-off the var assignment from the rest of code. 30 | "newline-after-var": 2, 31 | // Visually set-off the return from the rest of code. 32 | "newline-before-return": 2, 33 | 34 | // It is ok if a property of an obj gets reassigned but not the object itself 35 | "no-param-reassign": ["error", { "props": false }], 36 | 37 | // This library needs to support node 4. Spread is not available there. 38 | "prefer-spread": 0, 39 | 40 | // This is an es6 rule, but enabling it requires node 6+ 41 | "strict": ["off"], 42 | 43 | // Require developers to describe function purpose, arguments, and returns. 44 | "require-jsdoc": 2, 45 | "valid-jsdoc": [2, { 46 | "requireReturn": false 47 | }] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/lib/rules/responses-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const responsesCustomRule = require('../../../lib/rules/responses-custom'); 5 | 6 | describe('responses-custom', () => { 7 | describe('$key must 200 or 201', () => { 8 | const options = { 9 | whenField: '$key', 10 | whenPattern: '\\.*', 11 | thenField: '$key', 12 | thenPattern: '^(200|201)$' 13 | }; 14 | 15 | it('should not report errors when key is correct', () => { 16 | const schema = { 17 | paths: { 18 | '/pets': { 19 | get: { 20 | responses: { 21 | 200: { 22 | }, 23 | 201: { 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }; 30 | 31 | const failures = responsesCustomRule.validate(options, schema); 32 | 33 | assert.equal(failures.size, 0); 34 | }); 35 | 36 | it('should report an error when a key is incorrect', () => { 37 | const schema = { 38 | paths: { 39 | '/pets': { 40 | get: { 41 | responses: { 42 | 999: { 43 | } 44 | } 45 | } 46 | } 47 | } 48 | }; 49 | 50 | const failures = responsesCustomRule.validate([options], schema); 51 | 52 | assert.equal(failures.size, 1); 53 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.999'); 54 | assert.equal(failures.get(0).get('hint'), 'Expected responses $key:"999" to match "^(200|201)$"'); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /docs/common.md: -------------------------------------------------------------------------------- 1 | # Common rule config 2 | 3 | ## `case` 4 | 5 | Any rule that takes `case` as an argument will accept the following values: 6 | 7 | * `spine` Example: `this-is-a-spine-case` 8 | * `cap-spine` Example: `THIS-IS-A-CAP-SPINE-CASE` 9 | * `snake` Example: `this_is_a_snake_case` 10 | * `any` Example: `thisIs-..Any_CASE` 11 | 12 | 13 | ## `*-custom` config 14 | 15 | All `*-custom` rules support the following common config: 16 | 17 | 1. When `whenField` matches `whenPattern`/`whenAbsent`, 18 | 2. Then `thenField` MUST match `thenPattern`/`thenAbsent`. 19 | 20 | For object types that have keys, and are not part of an array, the `whenField` name can be `$key` to denote its key. 21 | 22 | Any `*Pattern` can be made case insensitive by adding `IgnoreCase`, such as `thenPatternIgnoreCase`. 23 | 24 | The `*Absent` option is as it sounds, will match only if the field is not present at all. The following example shows how a `description` field should be absent if the `name` matches a `special_name`: 25 | 26 | ```json 27 | { 28 | "whenField": "name", 29 | "whenPattern": "special_name", 30 | "thenField": "description", 31 | "thenAbsent": true 32 | } 33 | ``` 34 | 35 | ## `nested properties` 36 | 37 | Properties within object types may be referenced in rules using dot notation. For example this is a custom response rule that ensures that all non-empty responses are of type object. 38 | 39 | ```json 40 | "responses-custom": [ 41 | { 42 | "whenField": "schema.type", 43 | "whenPattern": "^((?!undefined).)*$", 44 | "thenField": "schema.type", 45 | "thenPattern": "^object$" 46 | } 47 | ] 48 | ``` 49 | -------------------------------------------------------------------------------- /test/lib/rules/tags-ref.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const rootTagsRule = require('../../../lib/rules/tags-ref'); 5 | 6 | describe('tags-ref', () => { 7 | const options = true; 8 | 9 | it('should not report errors when all operations tags are in the root tags', () => { 10 | const schema = { 11 | tags: [ 12 | { 13 | name: 'pet' 14 | } 15 | ], 16 | paths: { 17 | '/pets': { 18 | get: { 19 | tags: ['pet'] 20 | } 21 | } 22 | } 23 | }; 24 | 25 | const failures = rootTagsRule.validate(options, schema); 26 | 27 | assert.equal(failures.size, 0); 28 | }); 29 | 30 | it('should report errors when some operation tags are not present in root tags', () => { 31 | const schema = { 32 | tags: [ 33 | { 34 | name: 'alligator' 35 | }, 36 | { 37 | name: 'fishbowl' 38 | } 39 | ], 40 | paths: { 41 | '/pets': { 42 | get: { 43 | tags: ['fishbowl', 'alligator', 'pet'] 44 | }, 45 | put: { 46 | tags: ['lion'] 47 | } 48 | } 49 | } 50 | }; 51 | 52 | const failures = rootTagsRule.validate(options, schema); 53 | 54 | assert.equal(failures.size, 2); 55 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.tags[2]'); 56 | assert.equal(failures.get(0).get('hint'), 'Tag not found'); 57 | assert.equal(failures.get(1).get('location'), 'paths./pets.put.tags[0]'); 58 | assert.equal(failures.get(1).get('hint'), 'Tag not found'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/lib/helpers/PatternOption.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const PatternOption = require('../../../lib/helpers/PatternOption'); 4 | 5 | const assert = chai.assert; 6 | 7 | describe('PatternOption', () => { 8 | describe('isValidPatternOption', () => { 9 | it('Returns true for valid options', () => { 10 | assert.isTrue(PatternOption.isValidPatternOption('testPrefix', { testPrefixPattern: 'pattern' })); 11 | assert.isTrue(PatternOption.isValidPatternOption('testPrefix', { testPrefixPatternIgnoreCase: 'pattern' })); 12 | assert.isTrue(PatternOption.isValidPatternOption('testPrefix', { testPrefixAbsent: true })); 13 | }); 14 | 15 | it('Returns false for invalid options', () => { 16 | assert.isFalse(PatternOption.isValidPatternOption('testPrefix', { })); 17 | assert.isFalse(PatternOption.isValidPatternOption('testPrefix', { testPrefixPattern: 'pattern', testPrefixPatternIgnoreCase: 'pattern' })); 18 | assert.isFalse(PatternOption.isValidPatternOption('testPrefix', { testPrefixAbsent: false })); 19 | }); 20 | }); 21 | 22 | describe('constructor', () => { 23 | it('Creates a new PatternOption that does not ignore case', () => { 24 | const option = new PatternOption('testPrefix', { testPrefixPattern: 'testPattern' }); 25 | 26 | assert.equal('testPattern', option.pattern); 27 | assert.isFalse(option.ignoreCase); 28 | }); 29 | 30 | it('Creates a new PatternOption that ignores case', () => { 31 | const option = new PatternOption('testPrefix', { testPrefixPatternIgnoreCase: 'testPattern' }); 32 | 33 | assert.equal('testPattern', option.pattern); 34 | assert.isTrue(option.ignoreCase); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/lib/rules/no-ref-overrides.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const noRefOverridesRule = require('../../../lib/rules/no-ref-overrides'); 5 | 6 | describe('no-ref-overrides', () => { 7 | const options = { allowProperties: ['description'] }; 8 | 9 | it('should not report errors there are no ref overrides beyond those configured', () => { 10 | const schema = { 11 | definitions: { 12 | Pet: {} 13 | }, 14 | paths: { 15 | '/pets/{id}': { 16 | get: { 17 | responses: { 18 | 200: { 19 | schema: { 20 | description: 'A description override.', 21 | $ref: '#/definitions/Pet' 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }; 29 | 30 | const failures = noRefOverridesRule.validate(options, schema); 31 | 32 | assert.equal(failures.size, 0); 33 | }); 34 | 35 | it('should report an error a ref override is found that is not allowed', () => { 36 | const schema = { 37 | definitions: { 38 | Pet: {} 39 | }, 40 | paths: { 41 | '/pets/{id}': { 42 | get: { 43 | responses: { 44 | 200: { 45 | schema: { 46 | type: 'object', 47 | description: 'A description override.', 48 | $ref: '#/definitions/Pet' 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | }; 56 | 57 | const failures = noRefOverridesRule.validate(options, schema); 58 | 59 | assert.equal(failures.size, 1); 60 | assert.equal(failures.get(0).get('location'), 'paths./pets/{id}.get.responses.200.schema.type#override'); 61 | assert.equal(failures.get(0).get('hint'), 'Found $ref object override'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/lib/rules/operation-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const operationTagsRule = require('../../../lib/rules/operation-tags'); 5 | 6 | describe('operation-tags', () => { 7 | const options = true; 8 | 9 | it('should not report errors when all operations have non-empty tags', () => { 10 | const schema = { 11 | paths: { 12 | '/pets': { 13 | get: { 14 | tags: ['pet'] 15 | }, 16 | put: { 17 | tags: ['pet'] 18 | }, 19 | parameters: [] 20 | }, 21 | '/people': { 22 | parameters: [], 23 | get: { 24 | tags: ['person'] 25 | } 26 | } 27 | } 28 | }; 29 | 30 | const failures = operationTagsRule.validate(options, schema); 31 | 32 | assert.equal(failures.size, 0); 33 | }); 34 | 35 | it('should report error when one operation tags is not present', () => { 36 | const schema = { 37 | paths: { 38 | '/pets': { 39 | get: { 40 | } 41 | } 42 | } 43 | }; 44 | 45 | const failures = operationTagsRule.validate(options, schema); 46 | 47 | assert.equal(failures.size, 1); 48 | assert.equal(failures.get(0).get('location'), 'paths./pets.get'); 49 | assert.equal(failures.get(0).get('hint'), 'Missing tags'); 50 | }); 51 | 52 | it('should report error when one operation tags is empty', () => { 53 | const schema = { 54 | paths: { 55 | '/pets': { 56 | get: { 57 | tags: [] 58 | } 59 | } 60 | } 61 | }; 62 | 63 | const failures = operationTagsRule.validate(options, schema); 64 | 65 | assert.equal(failures.size, 1); 66 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.tags'); 67 | assert.equal(failures.get(0).get('hint'), 'Empty tags'); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /docs/rules/path-style.md: -------------------------------------------------------------------------------- 1 | # enforce paths that conform to spec, and to a specified input style (path-style) 2 | 3 | Validates that the `paths` keys conform to the spec. The [spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathsObject) states: 4 | 5 | ``` 6 | A relative path to an individual endpoint. The field name MUST begin with a slash. The path is appended to the basePath in order to construct the full URL. Path templating is allowed. 7 | ``` 8 | 9 | Path templates are allowed, and are surrounded by curly braces. 10 | 11 | Not specified in the spec, but validated here: 12 | 13 | * The path MUST not end with a trailing slash. 14 | 15 | Not validated in this rule: 16 | 17 | * The style of the path templates within curly braces. A separate rule, or another config option for this rule can be added to validate the path template id style. 18 | 19 | ## Config 20 | 21 | The config for this rule consists of: 22 | 23 | * `case`: a string specifying the case style. Choices are defined in the [common docs](../common.md). 24 | 25 | ## Example of *correct* usage given config `{"case": "spine"}` 26 | 27 | ```json 28 | { 29 | "paths": { 30 | "/": {}, 31 | "/first/{id}/second-third": {} 32 | } 33 | } 34 | ``` 35 | 36 | ## Examples of *incorrect* usage given config `{"case": "spine"}` 37 | 38 | ```json 39 | { 40 | "paths": { 41 | "/pets//food": {} 42 | } 43 | } 44 | ``` 45 | 46 | ```json 47 | { 48 | "paths": { 49 | "pets": {} 50 | } 51 | } 52 | ``` 53 | 54 | ```json 55 | { 56 | "paths": { 57 | "/badCase": {} 58 | } 59 | } 60 | ``` 61 | 62 | ```json 63 | { 64 | "paths": { 65 | "/pets/": {} 66 | } 67 | } 68 | ``` 69 | 70 | ```json 71 | { 72 | "paths": { 73 | "/invalid-param/{id/more-stuff": {} 74 | } 75 | } 76 | ``` 77 | 78 | ```json 79 | { 80 | "paths": { 81 | "/another-invalid-param/{id/more-stuff}": {} 82 | } 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/rules/no-restricted-words.md: -------------------------------------------------------------------------------- 1 | # enforce certain words or phrases are not included in text fields, including title, summary, or description (no-restricted-words) 2 | 3 | Validates that title, summary, or description do not contain restricted strings. These are strings that may be used internally, or those which are no longer recommended branding terms. Any accepted term is matched, and case is ignored. 4 | 5 | ## Examples of *correct* usage given the config: `{"words": ["My Deprecated Brand", "SUPERSECRETACRONYM"]}` 6 | 7 | ```json 8 | { 9 | "info": { 10 | "title": "Sample title", 11 | "description": "Sample description" 12 | }, 13 | "paths": { 14 | "/pets": { 15 | "get": { 16 | "description": "Sample operation description", 17 | "parameters": [ 18 | { 19 | "name": "limit", 20 | "description": "Sample param description", 21 | } 22 | ], 23 | "responses": { 24 | "200": { 25 | "description": "sample response" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | ## Examples of *incorrect* usage given the config: `{"words": ["My Deprecated Brand", "SUPERSECRETACRONYM"]}` 35 | ```json 36 | { 37 | "info": { 38 | "title": "my deprecated brand", 39 | "description": "supersecretacronym" 40 | } 41 | } 42 | ``` 43 | 44 | ```json 45 | { 46 | "paths": { 47 | "/pets": { 48 | "get": { 49 | "description": "Sample operation description of SUPERSECRETACRONYM", 50 | "parameters": [ 51 | { 52 | "name": "limit", 53 | "description": "Sample param description for supersecretacronym" 54 | } 55 | ], 56 | "responses": { 57 | "200": { 58 | "description": "sample response for my deprecated brand", 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /lib/helpers/PatternOption.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class PatternOption { 4 | constructor(prefix, option) { 5 | this.prefix = prefix; 6 | 7 | const patternKey = `${prefix}Pattern`; 8 | const patternIgnoreCaseKey = `${prefix}PatternIgnoreCase`; 9 | const patternAbsentKey = `${prefix}Absent`; 10 | 11 | if (option[patternKey]) { 12 | this.pattern = option[patternKey]; 13 | this.ignoreCase = false; 14 | } else if (option[patternIgnoreCaseKey]) { 15 | this.pattern = option[patternIgnoreCaseKey]; 16 | this.ignoreCase = true; 17 | } else if (option[patternAbsentKey] && option[patternAbsentKey] === true) { 18 | this.isAbsent = true; 19 | } 20 | } 21 | 22 | /** 23 | * Returns a new RegExp object for this pattern. 24 | * @returns {RegExp} the new RegExp object. 25 | */ 26 | getRegex() { 27 | const i = (this.ignoreCase) ? 'i' : ''; 28 | 29 | return new RegExp(this.pattern, `${i}`); 30 | } 31 | 32 | /** 33 | * Returns true if a pattern option is correct, meaning only one of the pattern 34 | * or patternIgnoreCase is defined. 35 | * @param {Object} prefix The prefix for the option. 36 | * @param {Object} option The provided option object. 37 | * @returns {boolean} true if a valid pattern matching the prefix is found. 38 | */ 39 | static isValidPatternOption(prefix, option) { 40 | const patternKey = `${prefix}Pattern`; 41 | const patternIgnoreCaseKey = `${prefix}PatternIgnoreCase`; 42 | const patternAbsentKey = `${prefix}Absent`; 43 | 44 | return (!!option[patternKey] && !option[patternIgnoreCaseKey] && !option[patternAbsentKey]) 45 | || (!option[patternKey] && !!option[patternIgnoreCaseKey] && !option[patternAbsentKey]) 46 | || ( 47 | !!option[patternAbsentKey] 48 | && (option[patternAbsentKey] === true) 49 | && !option[patternKey] 50 | && !option[patternIgnoreCaseKey] 51 | ); 52 | } 53 | } 54 | 55 | module.exports = PatternOption; 56 | -------------------------------------------------------------------------------- /.releasinator.rb: -------------------------------------------------------------------------------- 1 | configatron.product_name = "openapilint" 2 | 3 | # List of items to confirm from the person releasing. Required, but empty list is ok. 4 | configatron.prerelease_checklist_items = [ 5 | "Sanity check the master branch.", 6 | "Unit tests passed." 7 | ] 8 | 9 | def package_version() 10 | file = File.read "package.json" 11 | data = JSON.parse(file) 12 | data["version"] 13 | end 14 | 15 | def validate_version_match() 16 | if 'v'+package_version() != @current_release.version 17 | Printer.fail("package.json version v#{package_version} does not match changelog version #{@current_release.version}.") 18 | abort() 19 | end 20 | Printer.success("package.json version v#{package_version} matches latest changelog version #{@current_release.version}.") 21 | end 22 | 23 | def validate_paths 24 | @validator.validate_in_path("npm") 25 | @validator.validate_in_path("jq") 26 | end 27 | 28 | configatron.custom_validation_methods = [ 29 | method(:validate_paths), 30 | method(:validate_version_match) 31 | ] 32 | 33 | def build_method 34 | CommandProcessor.command("npm test", live_output=true) 35 | end 36 | 37 | # The command that builds the project. Required. 38 | configatron.build_method = method(:build_method) 39 | 40 | def publish_to_package_manager(version) 41 | CommandProcessor.command("npm publish .") 42 | end 43 | 44 | # The method that publishes the project to the package manager. Required. 45 | configatron.publish_to_package_manager_method = method(:publish_to_package_manager) 46 | 47 | 48 | def wait_for_package_manager(version) 49 | CommandProcessor.wait_for("wget -U \"non-empty-user-agent\" -qO- https://registry.npmjs.org/openapilint | jq '.[\"dist-tags\"][\"latest\"]' | grep #{package_version()} | cat") 50 | end 51 | 52 | # The method that waits for the package manager to be done. Required. 53 | configatron.wait_for_package_manager_method = method(:wait_for_package_manager) 54 | 55 | # True if publishing the root repo to GitHub. Required. 56 | configatron.release_to_github = true 57 | -------------------------------------------------------------------------------- /docs/rules/operation-payload-put.md: -------------------------------------------------------------------------------- 1 | # enforce the `PUT` request payload matches the `GET` `200` response (operation-payload-put) 2 | 3 | Validates that the the `PUT` request payload matches the `GET` `200` response. 4 | 5 | ## Examples of *correct* request/responses 6 | 7 | ```json 8 | { 9 | "paths": { 10 | "/pets": { 11 | "get": { 12 | "responses": { 13 | "200": { 14 | "description": "sample response", 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "$ref": "#/definitions/pet" 19 | } 20 | } 21 | } 22 | } 23 | }, 24 | "put": { 25 | "parameters": [ 26 | { 27 | "name": "limit", 28 | "description": "Sample param description", 29 | "in": "body", 30 | "schema": { 31 | "type": "array", 32 | "items": { 33 | "$ref": "#/definitions/pet" 34 | } 35 | } 36 | } 37 | ] 38 | } 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | ## Examples of **incorrect** request/responses 45 | 46 | 47 | ```json 48 | { 49 | "paths": { 50 | "/pets": { 51 | "get": { 52 | "responses": { 53 | "200": { 54 | "description": "sample response", 55 | "schema": { 56 | "type": "array", 57 | "items": { 58 | "$ref": "#/definitions/pet" 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | "put": { 65 | "parameters": [ 66 | { 67 | "name": "limit", 68 | "description": "Sample param description", 69 | "in": "body", 70 | "schema": { 71 | "type": "array", 72 | "items": { 73 | "$ref": "#/definitions/alligator" 74 | } 75 | } 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/rules/properties-style.md: -------------------------------------------------------------------------------- 1 | # enforce all properties' keys conform to a specified input style (properties-style) 2 | 3 | Validates that all properties' keys conform to a specified input style. 4 | 5 | ## Config 6 | 7 | The config for this rule consists of: 8 | 9 | * `case`: a string specifying the case style. Choices are defined in the [common docs](../common.md). 10 | 11 | ## Example of *correct* usage given config `{"case": "snake"}` 12 | 13 | ```json 14 | { 15 | "paths": { 16 | "/pets": { 17 | "get": { 18 | "parameters": [ 19 | { 20 | "in": "body", 21 | "schema": { 22 | "type": "object", 23 | "properties": { 24 | "awesome_parameter_key": { 25 | "type": "string" 26 | } 27 | } 28 | } 29 | } 30 | ], 31 | "responses": { 32 | "200": { 33 | "schema": { 34 | "type": "object", 35 | "properties": { 36 | "another_awesome_parameter_key": { 37 | "type": "string" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | ## Examples of **incorrect** usage given config `{"case": "snake"}` 50 | 51 | ```json 52 | { 53 | "paths": { 54 | "/pets": { 55 | "get": { 56 | "parameters": [ 57 | { 58 | "in": "body", 59 | "schema": { 60 | "type": "object", 61 | "properties": { 62 | "notAwesomeParameterKey": { 63 | "type": "string" 64 | } 65 | } 66 | } 67 | } 68 | ], 69 | "responses": { 70 | "200": { 71 | "schema": { 72 | "type": "object", 73 | "properties": { 74 | "another-not-awesome_parameter_key": { 75 | "type": "string" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /test/lib/rules/no-param-required-default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const noParamRequiredDefaultRule = require('../../../lib/rules/no-param-required-default'); 5 | 6 | describe('no-param-required-default', () => { 7 | const options = true; 8 | 9 | it('should not report errors when there is no default value for a required parameter', () => { 10 | const schema = { 11 | paths: { 12 | '/first/{id}': { 13 | get: { 14 | parameters: [ 15 | { 16 | name: 'id', 17 | type: 'string', 18 | in: 'path', 19 | required: true 20 | } 21 | ] 22 | } 23 | } 24 | } 25 | }; 26 | 27 | const failures = noParamRequiredDefaultRule.validate(options, schema); 28 | 29 | assert.equal(failures.size, 0); 30 | }); 31 | 32 | it('should report errors when there are default values for required parameters', () => { 33 | const schema = { 34 | paths: { 35 | '/first/{id}': { 36 | get: { 37 | parameters: [ 38 | { 39 | name: 'path_param', 40 | type: 'string', 41 | in: 'path', 42 | required: true, 43 | default: 'default_value' 44 | }, 45 | { 46 | name: 'query_param', 47 | type: 'string', 48 | in: 'query', 49 | required: true, 50 | default: 2 51 | } 52 | ] 53 | } 54 | } 55 | } 56 | }; 57 | 58 | const failures = noParamRequiredDefaultRule.validate(options, schema); 59 | 60 | assert.equal(failures.size, 2); 61 | assert.equal(failures.get(0).get('location'), 'paths./first/{id}.get.parameters[0]'); 62 | assert.equal(failures.get(0).get('hint'), 'Expected required parameter to not have a default value.'); 63 | assert.equal(failures.get(1).get('location'), 'paths./first/{id}.get.parameters[1]'); 64 | assert.equal(failures.get(1).get('hint'), 'Expected required parameter to not have a default value.'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /lib/OpenApiLint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Map = require('immutable').Map; 5 | const List = require('immutable').List; 6 | 7 | const RuleResult = require('./RuleResult'); 8 | 9 | class OpenApiLint { 10 | constructor(config) { 11 | this.config = config || {}; 12 | this.config.rules = this.config.rules || {}; 13 | } 14 | 15 | /** 16 | * Lint specified OpenAPI spec. 17 | * 18 | * @param {Object} schema Schema to lint. 19 | * @returns {Promise} A promise to the Map object of the results. 20 | */ 21 | lint(schema) { 22 | return new Promise((resolve, reject) => { 23 | // Normalize inputs 24 | const errorList = []; 25 | 26 | const result = new Map().withMutations((lintResultMap) => { 27 | Object.keys(this.config.rules).forEach((ruleKey) => { 28 | const ruleConfig = this.config.rules[ruleKey]; 29 | let validRuleName; 30 | const ruleName = `./rules/${ruleKey}`; 31 | 32 | // Check that this is a valid rule by file name of the rules. 33 | try { 34 | require.resolve(ruleName); 35 | validRuleName = true; 36 | } catch (e) { 37 | validRuleName = false; 38 | } 39 | 40 | if (validRuleName) { 41 | // eslint-disable-next-line global-require, import/no-dynamic-require 42 | const rule = require(ruleName); 43 | const description = rule.description; 44 | let failures; 45 | 46 | if (ruleConfig) { 47 | failures = rule.validate(ruleConfig, _.cloneDeep(schema)); 48 | } else { 49 | // return empty failure list if listed, but disabled 50 | failures = List(); 51 | } 52 | 53 | const ruleResult = new RuleResult({ description, failures }); 54 | 55 | lintResultMap.set(ruleKey, ruleResult); 56 | } else { 57 | errorList.push(`${ruleKey} not a valid config option`); 58 | } 59 | }); 60 | }); 61 | 62 | if (errorList.length > 0) { 63 | reject(Error(`${errorList.length} errors with config: $errorList`)); 64 | } else { 65 | resolve(result); 66 | } 67 | }); 68 | } 69 | } 70 | 71 | module.exports = OpenApiLint; 72 | -------------------------------------------------------------------------------- /test/lib/rules/root-info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const rootinfoRule = require('../../../lib/rules/root-info'); 5 | 6 | describe('root-info', () => { 7 | const options = true; 8 | 9 | it('should not report errors when info is present and has a valid title and version', () => { 10 | const schema = { 11 | info: { 12 | title: 'The title', 13 | version: '1.3' 14 | } 15 | }; 16 | 17 | const failures = rootinfoRule.validate(options, schema); 18 | 19 | assert.equal(failures.size, 0); 20 | }); 21 | 22 | it('should report error when info is not present', () => { 23 | const schema = {}; 24 | 25 | const failures = rootinfoRule.validate(options, schema); 26 | 27 | assert.equal(failures.size, 1); 28 | assert.equal(failures.get(0).get('location'), 'info'); 29 | assert.equal(failures.get(0).get('hint'), 'Missing info'); 30 | }); 31 | 32 | it('should report error when info has no title', () => { 33 | const schema = { 34 | info: { 35 | version: '1.3' 36 | } 37 | }; 38 | 39 | const failures = rootinfoRule.validate(options, schema); 40 | 41 | assert.equal(failures.size, 1); 42 | assert.equal(failures.get(0).get('location'), 'info'); 43 | assert.equal(failures.get(0).get('hint'), 'Missing info.title'); 44 | }); 45 | 46 | it('should report error when info has no version', () => { 47 | const schema = { 48 | info: { 49 | title: 'The title' 50 | } 51 | }; 52 | 53 | const failures = rootinfoRule.validate(options, schema); 54 | 55 | assert.equal(failures.size, 1); 56 | assert.equal(failures.get(0).get('location'), 'info'); 57 | assert.equal(failures.get(0).get('hint'), 'Missing info.version'); 58 | }); 59 | 60 | it('should report 2 errors when info has both no version and no title', () => { 61 | const schema = { 62 | info: { 63 | } 64 | }; 65 | 66 | const failures = rootinfoRule.validate(options, schema); 67 | 68 | assert.equal(failures.size, 2); 69 | assert.equal(failures.get(0).get('location'), 'info'); 70 | assert.equal(failures.get(0).get('hint'), 'Missing info.title'); 71 | assert.equal(failures.get(1).get('location'), 'info'); 72 | assert.equal(failures.get(1).get('hint'), 'Missing info.version'); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/lib/rules/operation-response-codes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const operationResponseCodesRule = require('../../../lib/rules/operation-response-codes'); 5 | 6 | describe('operation-response-codes', () => { 7 | describe('get must be 200 or default', () => { 8 | const options = { 9 | whenHttpMethod: 'get', 10 | thenResponseCodePattern: '(200|default)' 11 | }; 12 | 13 | it('should not report errors when operation-response-codes are correct', () => { 14 | const schema = { 15 | paths: { 16 | '/pets': { 17 | get: { 18 | responses: { 19 | 200: { 20 | }, 21 | default: { 22 | } 23 | } 24 | } 25 | } 26 | } 27 | }; 28 | 29 | const failures = operationResponseCodesRule.validate(options, schema); 30 | 31 | assert.equal(failures.size, 0); 32 | }); 33 | 34 | it('should report an error when operation-response-codes include a non-matching code', () => { 35 | const schema = { 36 | paths: { 37 | '/pets': { 38 | get: { 39 | responses: { 40 | 204: { 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }; 47 | 48 | const failures = operationResponseCodesRule.validate([options], schema); 49 | 50 | assert.equal(failures.size, 1); 51 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.204'); 52 | assert.equal(failures.get(0).get('hint'), 'Expected responses $key:"204" to match "(200|default)"'); 53 | }); 54 | }); 55 | 56 | describe('options must be valid', () => { 57 | it('should throw error when options are invalid', () => { 58 | const badConfigRuleFunction = () => { 59 | operationResponseCodesRule.validate({}, {}); 60 | }; 61 | 62 | assert.throws(badConfigRuleFunction, Error, 'Invalid option specified: {}'); 63 | }); 64 | 65 | it('should throw error when options are invalid', () => { 66 | const badConfigRuleFunction = () => { 67 | operationResponseCodesRule.validate({ whenHttpMethod: 'get' }, {}); 68 | }; 69 | 70 | assert.throws(badConfigRuleFunction, Error, 'Invalid option specified: {"whenHttpMethod":"get"}'); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/lib/OpenApiLint.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiAsPromised = require('chai-as-promised'); 3 | 4 | const OpenApiLint = require('../../lib/OpenApiLint'); 5 | const Map = require('immutable').Map; 6 | 7 | const expect = chai.expect; 8 | const assert = chai.assert; 9 | 10 | chai.use(chaiAsPromised); 11 | 12 | describe('OpenApiLint', () => { 13 | it('should return no errors for empty config', () => { 14 | const config = {}; 15 | const schema = {}; 16 | 17 | const result = new OpenApiLint(config).lint(schema); 18 | 19 | return result.then((lintResult) => { 20 | assert.isTrue(lintResult instanceof Map); 21 | assert.equal(lintResult.size, 0); 22 | }); 23 | }); 24 | 25 | it('should return a single key with no failures for a basic no-restricted-words test', () => { 26 | const config = { 27 | rules: { 28 | 'no-restricted-words': { words: ['blah'] } 29 | } 30 | }; 31 | const schema = { 32 | info: { 33 | description: 'handy description' 34 | } 35 | }; 36 | 37 | const result = new OpenApiLint(config).lint(schema); 38 | 39 | return result.then((lintResult) => { 40 | assert.isTrue(lintResult instanceof Map); 41 | assert.equal(lintResult.size, 1); 42 | assert.isDefined(lintResult.get('no-restricted-words').get('description')); 43 | assert.equal(lintResult.get('no-restricted-words').get('failures').size, 0); 44 | }); 45 | }); 46 | 47 | 48 | it('should return a single key with one failure for a basic no-restricted-words test', () => { 49 | const config = { 50 | rules: { 51 | 'no-restricted-words': { words: ['blah'] } 52 | } 53 | }; 54 | const schema = { 55 | info: { 56 | description: 'blah' 57 | } 58 | }; 59 | 60 | const result = new OpenApiLint(config).lint(schema); 61 | 62 | return result.then((lintResult) => { 63 | assert.isTrue(lintResult instanceof Map); 64 | assert.equal(lintResult.size, 1); 65 | assert.isDefined(lintResult.get('no-restricted-words').get('description')); 66 | 67 | assert.equal(lintResult.get('no-restricted-words').get('failures').size, 1); 68 | assert.equal(lintResult.get('no-restricted-words').get('failures').get(0).get('location'), 'info.description'); 69 | }); 70 | }); 71 | 72 | it('should have a failure if a bad key is provided in the config', () => { 73 | const config = { 74 | rules: { 75 | 'fake-rule': 'blah' 76 | } 77 | }; 78 | 79 | const result = new OpenApiLint(config).lint({}); 80 | 81 | return expect(result).to.be.rejectedWith(Error); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/lib/helpers/TextParser.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const spies = require('chai-spies'); 3 | 4 | const expect = chai.expect; 5 | 6 | chai.use(spies); 7 | 8 | const TextParser = require('../../../lib/helpers/TextParser'); 9 | 10 | describe('TextParser', () => { 11 | describe('forEachTextField', () => { 12 | const schema = { 13 | definitions: { 14 | Pet: { 15 | description: 'Definition description', 16 | type: 'object', 17 | properties: { 18 | name: { 19 | description: 'Properties.name description' 20 | } 21 | } 22 | } 23 | }, 24 | info: { 25 | title: 'info.title' 26 | }, 27 | paths: { 28 | '/pets': { 29 | get: { 30 | summary: 'operation summary', 31 | parameters: [ 32 | { 33 | description: 'parameter description' 34 | } 35 | ], 36 | responses: { 37 | 200: { 38 | schema: { 39 | title: 'title ref-override', 40 | description: 'description ref-override', 41 | $ref: '#/definitions/Pet' 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | }; 49 | 50 | const textFunc = () => {}; 51 | const parser = new TextParser(schema); 52 | 53 | it('Calls function for each description', () => { 54 | const spy = chai.spy(textFunc); 55 | 56 | parser.forEachTextField(['description'], spy); 57 | 58 | expect(spy).to.have.been.called.exactly(3); 59 | }); 60 | 61 | it('Calls function for each description-ref-override', () => { 62 | const spy = chai.spy(textFunc); 63 | 64 | parser.forEachTextField(['description-ref-override'], spy); 65 | 66 | expect(spy).to.have.been.called.exactly(1); 67 | }); 68 | 69 | it('Calls function for each title', () => { 70 | const spy = chai.spy(textFunc); 71 | 72 | parser.forEachTextField(['title'], spy); 73 | 74 | expect(spy).to.have.been.called.exactly(1); 75 | }); 76 | 77 | it('Calls function for each title-ref-override', () => { 78 | const spy = chai.spy(textFunc); 79 | 80 | parser.forEachTextField(['title-ref-override'], spy); 81 | 82 | expect(spy).to.have.been.called.exactly(1); 83 | }); 84 | 85 | it('Calls function for each summary', () => { 86 | const spy = chai.spy(textFunc); 87 | 88 | parser.forEachTextField(['summary'], spy); 89 | 90 | expect(spy).to.have.been.called.exactly(1); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /lib/rules/path-style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../constants'); 4 | const RuleFailure = require('../RuleFailure'); 5 | const List = require('immutable').List; 6 | 7 | /** 8 | * Checks the path element for compliance. A path element is anything between the forward slashes. 9 | * 10 | * @param {Object} pathElement The path element being checked. 11 | * @param {Object} caseOption The expected case of the path element. 12 | * @param {Object} pathKey The entire path key used for logging errors. 13 | * @param {Object} errorList The local list of errors. 14 | */ 15 | function checkPathElement(pathElement, caseOption, pathKey, errorList) { 16 | if (pathElement === '') { 17 | errorList.push(new RuleFailure({ location: `paths.${pathKey}`, hint: 'Must not have empty path elements' })); 18 | } else if (pathElement.match(constants.reValidPathTemplateParam)) { 19 | // found a valid template id. Stop checking. 20 | } else if (!pathElement.match(constants.caseStyles[caseOption])) { 21 | errorList.push(new RuleFailure({ location: `paths.${pathKey}`, hint: `"${pathElement}" does not comply with case: "${caseOption}"` })); 22 | } 23 | } 24 | 25 | const rule = { 26 | description: 'enforce paths that conform to spec, and to a specified input case style', 27 | validate(options, schema) { 28 | if (!options 29 | || !options.case 30 | || Object.keys(constants.caseStyles).indexOf(options.case) === -1) { 31 | throw new Error('Invalid config specified'); 32 | } 33 | 34 | const errorList = []; 35 | 36 | if (schema.paths) { 37 | Object.keys(schema.paths).forEach((pathKey) => { 38 | if (pathKey === '/') { 39 | // a path of "/" is ok. 40 | return; 41 | } 42 | 43 | let trimmedPath = pathKey; 44 | 45 | if (!pathKey.startsWith('/')) { 46 | errorList.push(new RuleFailure({ location: `paths.${pathKey}`, hint: 'Missing a leading slash' })); 47 | } else { 48 | trimmedPath = trimmedPath.substr(1);// trim off first slash. 49 | } 50 | 51 | if (pathKey.endsWith('/')) { 52 | errorList.push(new RuleFailure({ location: `paths.${pathKey}`, hint: 'Must not have a trailing slash' })); 53 | trimmedPath = trimmedPath.slice(0, -1); // trim off last slash 54 | } 55 | 56 | // now check the trimmedPath for the other problems. 57 | const splitPath = trimmedPath.split('/'); 58 | 59 | splitPath.forEach((pathElement) => { 60 | checkPathElement(pathElement, options.case, pathKey, errorList); 61 | }); 62 | }); 63 | } 64 | 65 | return new List(errorList); 66 | } 67 | }; 68 | 69 | module.exports = rule; 70 | -------------------------------------------------------------------------------- /lib/rules/path-parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const constants = require('../constants'); 6 | const RuleFailure = require('../RuleFailure'); 7 | const List = require('immutable').List; 8 | 9 | const rule = { 10 | description: 'enforce path parameter parity', 11 | validate(options, schema) { 12 | const errorList = []; 13 | 14 | if (schema.paths) { 15 | Object.keys(schema.paths).forEach((pathKey) => { 16 | const path = schema.paths[pathKey]; 17 | const pathTemplateParameters = []; 18 | 19 | // get object parameters 20 | _.each(pathKey.split('/'), (param) => { 21 | if (param.match(constants.reValidPathTemplateParam)) { 22 | pathTemplateParameters.push(param.slice(1, -1)); 23 | } 24 | }); 25 | 26 | // check each operation 27 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 28 | const operation = path[operationKey]; 29 | let extraPathTemplateParameters = _.clone(pathTemplateParameters); 30 | 31 | if (operation.parameters) { 32 | operation.parameters.forEach((parameter, parameterIndex) => { 33 | if (parameter.in === 'path') { 34 | if (!_.contains(pathTemplateParameters, parameter.name)) { 35 | errorList.push(new RuleFailure( 36 | { 37 | location: `paths.${pathKey}.${operationKey}.parameters[${parameterIndex}]`, 38 | hint: `Missing from path template: ${parameter.name}` 39 | } 40 | )); 41 | } else { 42 | // remove validated param name 43 | extraPathTemplateParameters = 44 | _.without(extraPathTemplateParameters, parameter.name); 45 | } 46 | 47 | if (!parameter.required) { 48 | errorList.push(new RuleFailure( 49 | { 50 | location: `paths.${pathKey}.${operationKey}.parameters[${parameterIndex}]`, 51 | hint: `Found path parameter without required=true: ${parameter.name}` 52 | } 53 | )); 54 | } 55 | } 56 | }); 57 | } 58 | 59 | extraPathTemplateParameters.forEach((parameter) => { 60 | errorList.push(new RuleFailure( 61 | { 62 | location: `paths.${pathKey}.${operationKey}`, 63 | hint: `Missing from parameter list: ${parameter}` 64 | } 65 | )); 66 | }); 67 | }); 68 | }); 69 | } 70 | 71 | return new List(errorList); 72 | } 73 | }; 74 | 75 | module.exports = rule; 76 | -------------------------------------------------------------------------------- /lib/rules/text-content.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const List = require('immutable').List; 4 | 5 | const RuleFailure = require('../RuleFailure'); 6 | const PatternOption = require('../helpers/PatternOption'); 7 | const TextParser = require('../helpers/TextParser'); 8 | 9 | /** 10 | * Gets a normalized options object array. If options are just an object, wraps in an array. 11 | * @param {Object} options The provided options. 12 | * @returns {Array} An options array. 13 | */ 14 | function getNormalizedOptions(options) { 15 | const normalizedOptions = []; 16 | 17 | if (!Array.isArray(options)) { 18 | normalizedOptions.push(options); 19 | } else { 20 | normalizedOptions.push.apply(normalizedOptions, options); 21 | } 22 | 23 | return normalizedOptions; 24 | } 25 | 26 | /** 27 | * Validates the provided options. 28 | * @param {Object} options The provided options. 29 | */ 30 | function validateOptions(options) { 31 | if (!options) { 32 | throw new Error('Missing config'); 33 | } 34 | 35 | getNormalizedOptions(options).forEach((option) => { 36 | if (!option.applyTo 37 | || !Array.isArray(option.applyTo) 38 | || (!PatternOption.isValidPatternOption('match', option) 39 | && !PatternOption.isValidPatternOption('notMatch', option))) { 40 | throw new Error(`Invalid option specified: ${JSON.stringify(option)}`); 41 | } 42 | }); 43 | } 44 | 45 | const rule = { 46 | description: 'enforce text content either matches or does not match config constraints', 47 | 48 | validate(options, schema) { 49 | validateOptions(options); 50 | 51 | const errorList = []; 52 | const myTextParser = new TextParser(schema, errorList); 53 | 54 | getNormalizedOptions(options).forEach((option) => { 55 | let currentPatternOption; 56 | 57 | if (option.matchPattern || option.matchPatternIgnoreCase) { 58 | currentPatternOption = new PatternOption('match', option); 59 | } else { 60 | currentPatternOption = new PatternOption('notMatch', option); 61 | } 62 | 63 | myTextParser.forEachTextField(option.applyTo, (field, pathToField) => { 64 | const matchedRegex = currentPatternOption.getRegex().test(field); 65 | 66 | if (!matchedRegex && currentPatternOption.prefix === 'match') { 67 | errorList.push(new RuleFailure({ 68 | location: pathToField, 69 | hint: `Expected "${field}" to match "${currentPatternOption.pattern}"` 70 | })); 71 | } 72 | 73 | if (matchedRegex && currentPatternOption.prefix === 'notMatch') { 74 | errorList.push(new RuleFailure({ 75 | location: pathToField, 76 | hint: `Expected "${field}" to not match "${currentPatternOption.pattern}"` 77 | })); 78 | } 79 | }); 80 | }); 81 | 82 | return new List(errorList); 83 | } 84 | }; 85 | 86 | module.exports = rule; 87 | -------------------------------------------------------------------------------- /docs/rules/parameters-custom.md: -------------------------------------------------------------------------------- 1 | # enforce parameters comply with custom config constraints (parameters-custom) 2 | 3 | Validates that parameters match their provided constraints. Constraints are provided in a simple format: 4 | 5 | 1. When `whenField` matches `whenPattern`, 6 | 2. Then `thenField` MUST match `thenPattern`. 7 | 8 | This format works for almost any constraint. `xField` is the name of the parameter's field. `xPattern` is a properly escaped regex string. For more than one constraint, use an array of constraint options. 9 | 10 | ## Config A 11 | 12 | Validates that `PayPal-Request-Id` parameters have a `description` that matches `server stores keys for \\d+ [days|hours]`. 13 | 14 | ```json 15 | { 16 | "whenField": "name", 17 | "whenPattern": "^PayPal-Request-Id$", 18 | "thenField": "description", 19 | "thenPattern": "server stores keys for \\d+ [days|hours]" 20 | } 21 | 22 | ``` 23 | 24 | ### Examples of *correct* usage with above config 25 | 26 | ```json 27 | { 28 | "paths": { 29 | "/pets": { 30 | "get": { 31 | "parameters": [ 32 | { 33 | "name": "PayPal-Request-Id", 34 | "in": "header", 35 | "type": "string", 36 | "description": "The server stores keys for 24 hours.", 37 | "required": false 38 | } 39 | ] 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | ### Examples of *incorrect* usage with above config 47 | 48 | ```json 49 | { 50 | "paths": { 51 | "/pets": { 52 | "get": { 53 | "parameters": [ 54 | { 55 | "name": "PayPal-Request-Id", 56 | "in": "header", 57 | "type": "string", 58 | "description": "This header description is not awesome.", 59 | "required": false 60 | } 61 | ] 62 | } 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | 69 | ## Config B 70 | 71 | Validates that `PayPal-Request-Id` parameters have the correct case. 72 | 73 | ```json 74 | { 75 | "whenField": "name", 76 | "whenPatternIgnoreCase": "^PayPal-Request-Id$", 77 | "thenField": "name", 78 | "thenPattern": "^PayPal-Request-Id$" 79 | } 80 | ``` 81 | 82 | ### Examples of *correct* usage with above config 83 | 84 | ```json 85 | { 86 | "paths": { 87 | "/pets": { 88 | "get": { 89 | "parameters": [ 90 | { 91 | "name": "PayPal-Request-Id" 92 | } 93 | ] 94 | } 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | ### Examples of *incorrect* usage with above config 101 | 102 | ```json 103 | { 104 | "paths": { 105 | "/pets": { 106 | "get": { 107 | "parameters": [ 108 | { 109 | "name": "PAYPAL-REQUEST-ID" 110 | } 111 | ] 112 | } 113 | } 114 | } 115 | } 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /test/lib/rules/no-path-dupes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const noPathDupesRule = require('../../../lib/rules/no-path-dupes'); 5 | 6 | describe('no-path-dupes', () => { 7 | const options = true; 8 | 9 | it('should not report errors when all paths are unique', () => { 10 | const schema = { 11 | paths: { 12 | '{root_id}': {}, 13 | '/pets': {}, 14 | '/pets/{pet_id}': {}, 15 | '/pets/{pet_id}/feed': {}, 16 | '/pets/{pet_id}/feed/{another_id}': {}, 17 | '/pets/{pet_id}/feed/{another_id}/full': {} 18 | } 19 | }; 20 | 21 | const failures = noPathDupesRule.validate(options, schema); 22 | 23 | assert.equal(failures.size, 0); 24 | }); 25 | 26 | it('should report an error when two simple paths are functionally equivalent', () => { 27 | const schema = { 28 | paths: { 29 | '/{pet_id}': {}, 30 | '/{monster_id}': {} 31 | } 32 | }; 33 | 34 | const failures = noPathDupesRule.validate(options, schema); 35 | 36 | assert.equal(failures.size, 1); 37 | assert.equal(failures.get(0).get('location'), 'paths./{monster_id}'); 38 | assert.equal(failures.get(0).get('hint'), 'Found duplicate path'); 39 | }); 40 | 41 | it('should report an error when two complex paths are functionally equivalent', () => { 42 | const schema = { 43 | paths: { 44 | '/pets/{pet_id}/feed/{another_id}': {}, 45 | '/pets/{monster_id}/feed/{another_monster_id}': {} 46 | } 47 | }; 48 | 49 | const failures = noPathDupesRule.validate(options, schema); 50 | 51 | assert.equal(failures.size, 1); 52 | assert.equal(failures.get(0).get('location'), 'paths./pets/{monster_id}/feed/{another_monster_id}'); 53 | assert.equal(failures.get(0).get('hint'), 'Found duplicate path'); 54 | }); 55 | 56 | it('should report n-1 errors when n paths are duplicated', () => { 57 | const schema = { 58 | paths: { 59 | '/pets/{pet_id}': {}, 60 | '/pets/{pet_id2}': {}, 61 | '/pets/{pet_id3}': {}, 62 | '/pets/{pet_id4}': {}, 63 | '/pets/{pet_id5}': {} 64 | } 65 | }; 66 | 67 | const failures = noPathDupesRule.validate(options, schema); 68 | 69 | assert.equal(failures.size, 4); 70 | assert.equal(failures.get(0).get('location'), 'paths./pets/{pet_id2}'); 71 | assert.equal(failures.get(0).get('hint'), 'Found duplicate path'); 72 | assert.equal(failures.get(1).get('location'), 'paths./pets/{pet_id3}'); 73 | assert.equal(failures.get(1).get('hint'), 'Found duplicate path'); 74 | assert.equal(failures.get(2).get('location'), 'paths./pets/{pet_id4}'); 75 | assert.equal(failures.get(2).get('hint'), 'Found duplicate path'); 76 | assert.equal(failures.get(3).get('location'), 'paths./pets/{pet_id5}'); 77 | assert.equal(failures.get(3).get('hint'), 'Found duplicate path'); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | openapilint release notes 2 | ============================ 3 | 4 | v0.10.0 5 | ----- 6 | * Add support for nested properties (thanks `RachelMuller`!). 7 | 8 | v0.9.0 9 | ----- 10 | * Add `*Absent` option for `*-custom` rules. 11 | * Remove definition and implementation of `docs-path`. 12 | * Fix regexes to find only the first match, rather than global. 13 | 14 | v0.8.0 15 | ----- 16 | * Add support for ignoring case in any rule with `*Pattern` configs. 17 | * Add definition and implementation of `info-custom`. 18 | * Add definition and implementation of `operation-custom`. 19 | * Add definition and implementation of `operation-response-codes`. 20 | * Add definition and implementation of `responses-custom`. 21 | * Add definition and implementation of `root-info`. 22 | * Add definition and implementation of `text-content`. 23 | * Fix bug where input schema could be altered by rules. 24 | * Fix descriptions of some existing rules. 25 | * Remove definition and implementation of `no-inconsistent-param-visibility`. 26 | 27 | v0.7.0 28 | ----- 29 | * Add support for arrays in `*-custom` configs. 30 | * Add definition and implementation of `no-ref-overrides`. 31 | * Add definition and implementation of `parameters-custom`. 32 | * Add definition and implementation of `schema-custom`. 33 | * Fix `no-restricted-words` to be more correct. 34 | 35 | v0.6.1 36 | ----- 37 | * Fix bug where a type-less `allOf` was being ignored. 38 | 39 | v0.6.0 40 | ----- 41 | * Add definition and implementation of `no-orphan-refs`. 42 | * Add definition and implementation of `no-path-dupes`. 43 | * Add definition and implementation of `properties-custom`. 44 | * Add definition and implementation of `properties-style`. 45 | * Add definition and implementation of `root-consumes`. 46 | * Add definition and implementation of `root-produces`. 47 | * Add `required=true` check to `path-parameters`. 48 | 49 | v0.5.1 50 | ----- 51 | * Add support for `.` in caseStyle.any, allowing `info.x-publicDocsPath` to contain `.`'s. 52 | 53 | v0.5.0 54 | ----- 55 | * Add definition and implementation of `path-style`. 56 | 57 | v0.4.0 58 | ----- 59 | * Add hints to all rules. 60 | * Add `schema` object validation to `no-restricted-words`. 61 | * Add `-` character to allowed `docs-path` path. 62 | 63 | v0.3.0 64 | ----- 65 | * Add implementation of `no-inconsistent-param-visibility`. 66 | * Add implementation of `no-path-item-parameters`. 67 | * Add implementation of `path-parameters`. 68 | 69 | v0.2.1 70 | ----- 71 | * Fix bug where a path `parameters` object was being treated like an operation. 72 | 73 | v0.2.0 74 | ----- 75 | * Remove some unhelpful classes from response objects. 76 | * Add implementation of `operation-tags`. 77 | * Add implementation of `tags-ref`. 78 | * Rename `docs-path` to validate `info.x-publicDocsPath` instead of `info.x-docsPath`. 79 | 80 | v0.1.1 81 | ----- 82 | * Fix some bad docs. 83 | * Add some missing entries to `package.json`. 84 | 85 | v0.1.0 86 | ----- 87 | * Initial release! 88 | -------------------------------------------------------------------------------- /docs/rules/text-content.md: -------------------------------------------------------------------------------- 1 | # enforce text content either matches or does not match config constraints (text-content) 2 | 3 | Validates that `title`, `summary`, or `description` do or do not match a configured pattern. Constraints are provided in a simple format: 4 | 5 | 1. Use `applyTo` specifies at least one of `title`, `summary`, `description`, 6 | 2. and either `matchPattern` or `notMatchPattern` to specify a condition where the field above MUST match `matchPattern`, or MUST NOT match `notMatchPattern`. 7 | 8 | Add `title-ref-override` or `description-ref-override` to the `applyTo` array to indicate ref-overrides are to match the same constraints, if present. 9 | 10 | ## Config A 11 | 12 | Validates that `title`, `summary`, and `description` all start with a capital letter. 13 | 14 | ```json 15 | { 16 | "applyTo": [ 17 | "title", 18 | "summary", 19 | "description" 20 | ], 21 | "matchPattern": "^[A-Z]" 22 | } 23 | ``` 24 | 25 | ### Examples of *correct* usage with above config 26 | 27 | ```json 28 | { 29 | "info": { 30 | "title": "Good title with no leading spaces" 31 | }, 32 | "paths": { 33 | "/pets": { 34 | "get": { 35 | "summary": "The correct case summary", 36 | "parameters": [ 37 | { 38 | "description": "The correct case description" 39 | } 40 | ] 41 | } 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | ### Examples of *incorrect* usage with above config 48 | 49 | ```json 50 | { 51 | "info": { 52 | "title": " Title with spaces" 53 | }, 54 | "paths": { 55 | "/pets": { 56 | "get": { 57 | "summary": "the lower case summary", 58 | "parameters": [ 59 | { 60 | "description": "the lower case description" 61 | } 62 | ] 63 | } 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ## Config B 70 | 71 | Validates that `summary` and `description` all end with a period (`.`). 72 | 73 | ```json 74 | { 75 | "applyTo": [ 76 | "summary", 77 | "description" 78 | ], 79 | "matchPattern": "\\.$" 80 | } 81 | ``` 82 | 83 | ### Examples of *correct* usage with above config 84 | 85 | ```json 86 | { 87 | "paths": { 88 | "/pets": { 89 | "get": { 90 | "summary": "The correct punctuated summary.", 91 | "parameters": [ 92 | { 93 | "description": "The correct punctuated description." 94 | } 95 | ] 96 | } 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | ### Examples of *incorrect* usage with above config 103 | 104 | ```json 105 | { 106 | "paths": { 107 | "/pets": { 108 | "get": { 109 | "summary": "The incorrect summary without puncuation", 110 | "parameters": [ 111 | { 112 | "description": "The incorrect description with trailing spaces. " 113 | } 114 | ] 115 | } 116 | } 117 | } 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /test/lib/rules/no-orphan-refs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const noOrphanRefsRule = require('../../../lib/rules/no-orphan-refs'); 5 | 6 | describe('no-orphan-refs', () => { 7 | const options = true; 8 | 9 | it('should not report errors when all refs are reachable', () => { 10 | const schema = { 11 | definitions: { 12 | Pet: { 13 | }, 14 | Pets: { 15 | type: 'array', 16 | items: { 17 | $ref: '#/definitions/Pet' 18 | } 19 | } 20 | }, 21 | paths: { 22 | '/pets': { 23 | get: { 24 | responses: { 25 | 200: { 26 | schema: { 27 | $ref: '#/definitions/Pets' 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }; 35 | 36 | const failures = noOrphanRefsRule.validate(options, schema); 37 | 38 | assert.equal(failures.size, 0); 39 | }); 40 | 41 | it('should not report errors when all refs are reachable with an implicit allOf', () => { 42 | const schema = { 43 | definitions: { 44 | Pet: { 45 | } 46 | }, 47 | paths: { 48 | '/pets': { 49 | put: { 50 | parameters: [ 51 | { 52 | schema: { 53 | allOf: [ 54 | { 55 | properties: { 56 | related_resources: { 57 | type: 'array', 58 | items: { 59 | $ref: '#/definitions/Pet' 60 | } 61 | } 62 | } 63 | } 64 | ] 65 | } 66 | } 67 | ] 68 | } 69 | } 70 | } 71 | }; 72 | 73 | const failures = noOrphanRefsRule.validate(options, schema); 74 | 75 | assert.equal(failures.size, 0); 76 | }); 77 | 78 | it('should report an error when a definition ref is not reachable', () => { 79 | const schema = { 80 | definitions: { 81 | Pet: { 82 | }, 83 | Pets: { 84 | type: 'array', 85 | items: { 86 | type: 'object' 87 | } 88 | } 89 | }, 90 | paths: { 91 | '/pets': { 92 | get: { 93 | responses: { 94 | 200: { 95 | schema: { 96 | $ref: '#/definitions/Pets' 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | }; 104 | 105 | const failures = noOrphanRefsRule.validate(options, schema); 106 | 107 | assert.equal(failures.size, 1); 108 | assert.equal(failures.get(0).get('location'), 'definitions.Pet'); 109 | assert.equal(failures.get(0).get('hint'), 'Definition is not reachable'); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/lib/constants.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const constants = require('../../lib/constants'); 4 | 5 | const assert = chai.assert; 6 | 7 | describe('constants', () => { 8 | ['{MY_TEMPLATE}', '{template}', '{tempalteVar}'].forEach((pathElement) => { 9 | describe(`reValidPathTemplateParam ${pathElement}`, () => { 10 | it('should match regex', () => { 11 | assert.isTrue(!!pathElement.match(constants.reValidPathTemplateParam)); 12 | }); 13 | }); 14 | }); 15 | 16 | ['', '{', '}', '{}', '}{', '{pets', 'pets}', '{{pets}', '{pets}}', '{pe ts}', '}pets{'].forEach((pathElement) => { 17 | describe(`reValidPathTemplateParam ${pathElement}`, () => { 18 | it('should not match regex', () => { 19 | assert.isTrue(!pathElement.match(constants.reValidPathTemplateParam)); 20 | }); 21 | }); 22 | }); 23 | 24 | // -------- // 25 | 26 | ['word', 'some-words', 'a-b-c-word-d'].forEach((pathElement) => { 27 | describe(`style.spine ${pathElement}`, () => { 28 | it('should match regex', () => { 29 | assert.isTrue(!!pathElement.match(constants.caseStyles.spine)); 30 | }); 31 | }); 32 | }); 33 | 34 | ['PETS', 'peTS', 'PET_S', 'Pe_Ts', 'PE-ts'].forEach((pathElement) => { 35 | describe(`style.spine ${pathElement}`, () => { 36 | it('should not match regex', () => { 37 | assert.isTrue(!pathElement.match(constants.caseStyles.spine)); 38 | }); 39 | }); 40 | }); 41 | 42 | ['WORD', 'SOME-WORDS', 'A-B-C-WORD-D'].forEach((pathElement) => { 43 | describe(`style.cap-spine ${pathElement}`, () => { 44 | it('should match regex', () => { 45 | assert.isTrue(!!pathElement.match(constants.caseStyles['cap-spine'])); 46 | }); 47 | }); 48 | }); 49 | 50 | ['word', 'some-words', 'a-b-c-word-d', 'peTS', 'PET_S'].forEach((pathElement) => { 51 | describe(`style.cap-spine ${pathElement}`, () => { 52 | it('should not match regex', () => { 53 | assert.isTrue(!pathElement.match(constants.caseStyles['cap-spine'])); 54 | }); 55 | }); 56 | }); 57 | 58 | ['word', 'some_words', 'a_b_c_word_d'].forEach((pathElement) => { 59 | describe(`style.snake ${pathElement}`, () => { 60 | it('should match regex', () => { 61 | assert.isTrue(!!pathElement.match(constants.caseStyles.snake)); 62 | }); 63 | }); 64 | }); 65 | 66 | ['WORD', 'SOME-WORDS', 'some-words', 'PET_S'].forEach((pathElement) => { 67 | describe(`style.snake ${pathElement}`, () => { 68 | it('should not match regex', () => { 69 | assert.isTrue(!pathElement.match(constants.caseStyles.snake)); 70 | }); 71 | }); 72 | }); 73 | 74 | ['WORD', 'SOME-some_words', 'a-b-c-word-d---..', 'PET_S', 'Pe_Ts', 'PE.ts-'].forEach((pathElement) => { 75 | describe(`style.any ${pathElement}`, () => { 76 | it('should match regex', () => { 77 | assert.isTrue(!!pathElement.match(constants.caseStyles.any)); 78 | }); 79 | }); 80 | }); 81 | 82 | ['{', '}', '#', '@', '!', '^', '&', '(', ')', '+', '=', '?', ',', '<', '>', '/', '*'].forEach((pathElement) => { 83 | describe(`style.any ${pathElement}`, () => { 84 | it('should not match regex', () => { 85 | assert.isTrue(!pathElement.match(constants.caseStyles.any)); 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /docs/rules/path-parameters.md: -------------------------------------------------------------------------------- 1 | # enforce path parameter parity (path-parameters) 2 | 3 | Validates that the sum of the parameters with {"location": "path"} matches the number of path template parameters in the URI, and that required='true'. See the [spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#fixed-fields-7) for the full description. 4 | 5 | ## Examples of *correct* usage 6 | 7 | ```json 8 | { 9 | "paths": { 10 | "/first/{id}": { 11 | "get": { 12 | "parameters": [ 13 | { 14 | "name": "id", 15 | "type": "string", 16 | "in": "path", 17 | "required": true 18 | } 19 | ] 20 | } 21 | }, 22 | "/first/{first_id}/second/{id}": { 23 | "get": { 24 | "parameters": [ 25 | { 26 | "name": "first_id", 27 | "type": "string", 28 | "in": "path", 29 | "required": true 30 | }, 31 | { 32 | "name": "id", 33 | "type": "string", 34 | "in": "path", 35 | "required": true 36 | } 37 | ] 38 | } 39 | }, 40 | "/second/{customer_id}/details": { 41 | "get": { 42 | "parameters": [ 43 | { 44 | "name": "customer_id", 45 | "type": "string", 46 | "in": "path", 47 | "required": true 48 | }, 49 | { 50 | "name": "merchant_id", 51 | "type": "string", 52 | "in": "query" 53 | } 54 | ] 55 | } 56 | }, 57 | "/second/{customer_id}/{event_id}/details": { 58 | "get": { 59 | "parameters": [ 60 | { 61 | "name": "customer_id", 62 | "type": "string", 63 | "in": "path", 64 | "required": true 65 | }, 66 | { 67 | "name": "event_id", 68 | "type": "string", 69 | "in": "path", 70 | "required": true 71 | } 72 | ] 73 | } 74 | } 75 | } 76 | } 77 | 78 | ``` 79 | 80 | ## Examples of **incorrect** usage 81 | ```json 82 | { 83 | "paths": { 84 | "/first/{id}": { 85 | "get": {}, 86 | "post": {} 87 | }, 88 | "/second/{customer_id}/details": { 89 | "get": { 90 | "parameters": [ 91 | { 92 | "name": "customer_id", 93 | "type": "string", 94 | "in": "path", 95 | "required": true 96 | }, 97 | { 98 | "name": "extra_path_id", 99 | "type": "string", 100 | "in": "path", 101 | "required": true 102 | } 103 | ] 104 | }, 105 | "put": { 106 | "parameters": [ 107 | { 108 | "name": "customer", 109 | "type": "string", 110 | "in": "path", 111 | "required": true 112 | }, 113 | { 114 | "name": "extra_path_id", 115 | "type": "string", 116 | "in": "path", 117 | "required": true 118 | } 119 | ] 120 | } 121 | }, 122 | "/missing_required/{customer_id}": { 123 | "get": { 124 | "parameters": [ 125 | { 126 | "name": "customer_id", 127 | "type": "string", 128 | "in": "path" 129 | } 130 | ] 131 | } 132 | }, 133 | } 134 | } 135 | ``` 136 | -------------------------------------------------------------------------------- /lib/rules/operation-response-codes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const List = require('immutable').List; 5 | 6 | const constants = require('../constants'); 7 | const CustomValidator = require('../helpers/CustomValidator'); 8 | const PatternOption = require('../helpers/PatternOption'); 9 | 10 | /** 11 | * Gets a normalized options object array. If options are just an object, wraps in an array. 12 | * @param {Object} options The provided options. 13 | * @returns {Array} An options array. 14 | */ 15 | function getNormalizedOptions(options) { 16 | const normalizedOptions = []; 17 | 18 | if (!Array.isArray(options)) { 19 | normalizedOptions.push(options); 20 | } else { 21 | normalizedOptions.push.apply(normalizedOptions, options); 22 | } 23 | 24 | return normalizedOptions; 25 | } 26 | 27 | /** 28 | * Validates the provided options. 29 | * @param {Object} options The provided options. 30 | */ 31 | function validateOptions(options) { 32 | if (!options) { 33 | throw new Error('Missing config'); 34 | } 35 | 36 | getNormalizedOptions(options).forEach((option) => { 37 | if (!option.whenHttpMethod 38 | || !PatternOption.isValidPatternOption('thenResponseCode', option)) { 39 | throw new Error(`Invalid option specified: ${JSON.stringify(option)}`); 40 | } 41 | }); 42 | } 43 | 44 | const rule = { 45 | description: 'enforce operation response codes comply with custom key constraints', 46 | validate(options, schema) { 47 | const errorList = []; 48 | 49 | validateOptions(options); 50 | 51 | // convert into a custom format that the parser understands. 52 | const methodToValidatorsMap = {}; 53 | 54 | getNormalizedOptions(options).forEach((option) => { 55 | const convertedOption = { 56 | whenField: '$key', 57 | whenPattern: '.*', 58 | thenField: '$key' 59 | }; 60 | 61 | if (option.thenResponseCodePattern) { 62 | convertedOption.thenPattern = option.thenResponseCodePattern; 63 | } else { 64 | convertedOption.thenPatternIgnoreCase = option.thenResponseCodePatternIgnoreCase; 65 | } 66 | const myCustomValidator = new CustomValidator(convertedOption, schema, errorList); 67 | 68 | myCustomValidator.validateOptions(); 69 | if (!methodToValidatorsMap[option.whenHttpMethod]) { 70 | methodToValidatorsMap[option.whenHttpMethod] = []; 71 | } 72 | methodToValidatorsMap[option.whenHttpMethod].push(myCustomValidator); 73 | }); 74 | 75 | if (schema.paths) { 76 | Object.keys(schema.paths).forEach((pathKey) => { 77 | const path = schema.paths[pathKey]; 78 | 79 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 80 | const operation = path[operationKey]; 81 | 82 | const applicableValidations = methodToValidatorsMap[operationKey]; 83 | 84 | if (applicableValidations && operation.responses) { 85 | Object.keys(operation.responses).forEach((responseKey) => { 86 | applicableValidations.forEach((validator) => { 87 | validator.validateAllCustoms( 88 | responseKey, 89 | undefined, // only care about keys 90 | `paths.${pathKey}.${operationKey}.responses.${responseKey}`, 91 | 'responses', 92 | () => true 93 | ); 94 | }); 95 | }); 96 | } 97 | }); 98 | }); 99 | } 100 | 101 | return new List(errorList); 102 | } 103 | }; 104 | 105 | module.exports = rule; 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version][npm-image]][npm-url] 2 | [![build status][travis-image]][travis-url] 3 | [![Downloads][downloads-image]][downloads-url] 4 | 5 | # openapilint 6 | 7 | This project uses Node.js to implement an OpenAPI linter. As with any linter, there are configurable options for validating your OpenAPI specs. 8 | 9 | ## Install openapilint 10 | 11 | ``` 12 | npm install openapilint --save 13 | ``` 14 | 15 | ## Usage 16 | 17 | `openapilint` takes as input a json schema, and json config object: 18 | 19 | ```js 20 | const schema = { 21 | info: { 22 | description: 'handy description' 23 | } 24 | }; 25 | const config = { 26 | "rules": { 27 | "docs-path": true, // rule will be run, and has no special config 28 | "no-restricted-words": {"words": ["supersecretacronym"]}, // rule will be run with the specified config 29 | "root-tags": false // rule will not be run 30 | } 31 | }; 32 | 33 | ``` 34 | 35 | and returns a promise of the results: 36 | 37 | ```js 38 | const result = new OpenApiLint(config).lint(schema); 39 | 40 | return result.then((lintResult) => { 41 | // Do something with the result Map. 42 | }).catch((error) => { 43 | // Do something with the Error. 44 | }); 45 | ``` 46 | 47 | `lintResult` is a `String -> RuleResult` [immutable Map](http://facebook.github.io/immutable-js/docs/#/Map) of nested immutable objects for consumption. Specifically: 48 | 49 | * `RuleResult` is a `String -> Object` [immutable Record](http://facebook.github.io/immutable-js/docs/#/Record) with two keys, `description` (`String`) & `failures` (`List`). 50 | * `RuleFailure` is a `String -> String` [immutable Record](http://facebook.github.io/immutable-js/docs/#/Record) with two keys, `location` (`String`) & `hint` (`String`) 51 | 52 | It is up to the implementer to parse this data and provide a useful error response to the user. 53 | 54 | ## Rules 55 | 56 | By default, only the rules in `lib/rules` are supported. Details of these rules can be found in [`docs/rules`](https://github.com/braintree/openapilint/tree/master/docs/rules). 57 | 58 | ## Dereferencing 59 | 60 | Due to the complex nature of multi-file references, `openapilint` rules assume that all references are contained within the input. For simplicity, references to anything other than internally are treated as errors. 61 | 62 | ## OpenAPI supported versions 63 | 64 | `openapilint` supports Swagger 2.0. Support for OpenAPI 3.0 is not planned. 65 | 66 | ## Comparison to other validators 67 | 68 | `openapilint` does have some overlapping features with other json validators, such as [`joi`](https://github.com/hapijs/joi) and [`jsonschema`](https://github.com/tdegrunt/jsonschema). A developer using this project may choose to use those validators as a first wave of checks against a particular spec before running it through the `openapilint` set of rules. This is expected and encouraged. The rules implemented within `openapilint` go above and beyond those validators by targeting the common OpenAPI-specific problems. 69 | 70 | ## License 71 | 72 | See [License](LICENSE). 73 | 74 | ## Contributing 75 | 76 | See [Contributing](CONTRIBUTING.md). 77 | 78 | ## Acknowledgements 79 | 80 | This project was inspired by - and heavily influenced by - [`eslint`](https://github.com/eslint/eslint/), [`markdownlint`](https://github.com/DavidAnson/markdownlint), and [`swagger-api-checkstyle`](https://github.com/jharmn/swagger-api-checkstyle). The configuration schema and some code was modified for usage in this project. 81 | 82 | [npm-image]: https://img.shields.io/npm/v/openapilint.svg?style=flat-square 83 | [npm-url]: https://www.npmjs.com/package/openapilint 84 | [travis-image]: https://img.shields.io/travis/braintree/openapilint/master.svg?style=flat-square 85 | [travis-url]: https://travis-ci.org/braintree/openapilint 86 | [downloads-image]: https://img.shields.io/npm/dm/openapilint.svg?style=flat-square 87 | [downloads-url]: https://www.npmjs.com/package/openapilint 88 | -------------------------------------------------------------------------------- /docs/rules/schema-custom.md: -------------------------------------------------------------------------------- 1 | # enforce schema objects comply with custom config constraints (schema-custom) 2 | 3 | Validates that schema objects match their provided constraints. Constraints are provided in a simple format: 4 | 5 | 1. When `whenField` matches `whenPattern`, 6 | 2. Then `thenField` MUST match `thenPattern`. 7 | 8 | This format works for almost any constraint. `xField` is the name of the schema's field. `xPattern` is a properly escaped regex string. For more than one constraint, use an array of constraint options. 9 | 10 | Since `allOf` objects can be handled specially, they are ignored by default. To enable them in a config, add the config `alsoApplyTo` with a list of items: `alsoApplyTo: [ "allOf" ]`. 11 | 12 | ## Config A 13 | 14 | Validates that when schema objects have `type` = `object`, they must have a `title` property with at least one non-whitespace character. 15 | 16 | ```json 17 | { 18 | "whenField": "type", 19 | "whenPattern": "object", 20 | "thenField": "title", 21 | "thenPattern": "^\\s", 22 | "alsoApplyTo": [ 23 | "allOf" 24 | ] 25 | } 26 | 27 | ``` 28 | 29 | ### Examples of *correct* usage with above config 30 | 31 | ```json 32 | { 33 | "definitions": { 34 | "Pet": { 35 | "type": "object", 36 | "title": "Pet title", 37 | "properties": { 38 | "country_code": { 39 | "type": "string" 40 | } 41 | } 42 | }, 43 | "Pets": { 44 | "type": "array", 45 | "items": { 46 | "$ref": "#/definitions/Pet" 47 | } 48 | } 49 | }, 50 | "paths": { 51 | "/pets": { 52 | "get": { 53 | "parameters": [ 54 | { 55 | "in": "body", 56 | "schema": { 57 | "title": "Body schema", 58 | "type": "object" 59 | } 60 | } 61 | ], 62 | "responses": { 63 | "200": { 64 | "schema": { 65 | "$ref": "#/definitions/Pets" 66 | } 67 | } 68 | } 69 | }, 70 | "put": { 71 | "parameters": [ 72 | { 73 | "in": "body", 74 | "schema": { 75 | "allOf": [ 76 | { 77 | "type": "object", 78 | "title": "allOf object title" 79 | }, 80 | { 81 | "type": "object", 82 | "title": "allOf 2 object title" 83 | }, 84 | { 85 | "type": "string" 86 | } 87 | ] 88 | } 89 | } 90 | ] 91 | } 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | ### Examples of *incorrect* usage with above config 98 | 99 | ```json 100 | { 101 | "definitions": { 102 | "Pet": { 103 | "type": "object", 104 | "properties": { 105 | "country_code": { 106 | "type": "string" 107 | } 108 | } 109 | }, 110 | "Pets": { 111 | "type": "array", 112 | "items": { 113 | "$ref": "#/definitions/Pet" 114 | } 115 | } 116 | }, 117 | "paths": { 118 | "/pets": { 119 | "get": { 120 | "parameters": [ 121 | { 122 | "in": "body", 123 | "schema": { 124 | "type": "object" 125 | } 126 | } 127 | ], 128 | "responses": { 129 | "200": { 130 | "schema": { 131 | "$ref": "#/definitions/Pets" 132 | } 133 | } 134 | } 135 | }, 136 | "put": { 137 | "parameters": [ 138 | { 139 | "in": "body", 140 | "schema": { 141 | "allOf": [ 142 | { 143 | "type": "object" 144 | }, 145 | { 146 | "type": "object" 147 | }, 148 | { 149 | "type": "string" 150 | } 151 | ] 152 | } 153 | } 154 | ] 155 | } 156 | } 157 | } 158 | } 159 | ``` 160 | 161 | -------------------------------------------------------------------------------- /lib/helpers/TextParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const constants = require('../constants'); 6 | const SchemaObjectParser = require('../helpers/SchemaObjectParser'); 7 | 8 | class TextParser { 9 | constructor(rootSchema) { 10 | this.rootSchema = rootSchema; 11 | } 12 | 13 | /** 14 | * Runs the propertyFunction for each property found. 15 | * 16 | * @param {Array} applyTo An array of which objects to apply the function to. 17 | * The array only allows: 18 | * [ "title", "description", "summary", 19 | * "title-ref-override", and/or "description-ref-override"]. 20 | * @param {Function} textContentFunction The function that checks text content. 21 | */ 22 | forEachTextField(applyTo, textContentFunction) { 23 | const applyToTitle = _.includes(applyTo, 'title'); 24 | const applyToDescription = _.includes(applyTo, 'description'); 25 | const applyToSummary = _.includes(applyTo, 'summary'); 26 | 27 | if (applyToTitle && this.rootSchema.info && this.rootSchema.info.title) { 28 | textContentFunction(this.rootSchema.info.title, 'info.title'); 29 | } 30 | 31 | if (applyToDescription && this.rootSchema.info && this.rootSchema.info.description) { 32 | textContentFunction(this.rootSchema.info.description, 'info.description'); 33 | } 34 | 35 | if (this.rootSchema.paths) { 36 | const mySchemaObjectParser = new SchemaObjectParser(this.rootSchema); 37 | 38 | mySchemaObjectParser.forEachSchema((schemaObject, pathToSchema) => { 39 | if (applyToDescription && schemaObject.description) { 40 | textContentFunction(schemaObject.description, `${pathToSchema}.description`); 41 | } 42 | if (applyToTitle && schemaObject.title) { 43 | textContentFunction(schemaObject.title, `${pathToSchema}.title`); 44 | } 45 | }, 46 | undefined, 47 | (schemaObject, pathToSchema) => { 48 | const applyToTitleRefOverride = _.includes(applyTo, 'title-ref-override'); 49 | const applyToDescriptionRefOverride = _.includes(applyTo, 'description-ref-override'); 50 | 51 | if (applyToTitleRefOverride && schemaObject.title) { 52 | textContentFunction(schemaObject.title, `${pathToSchema}.title#override`); 53 | } 54 | if (applyToDescriptionRefOverride && schemaObject.description) { 55 | textContentFunction(schemaObject.description, `${pathToSchema}.description#override`); 56 | } 57 | }); 58 | 59 | Object.keys(this.rootSchema.paths).forEach((pathKey) => { 60 | const path = this.rootSchema.paths[pathKey]; 61 | 62 | // no path descriptions or titles 63 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 64 | const operation = path[operationKey]; 65 | const parameters = operation.parameters; 66 | const responses = operation.responses; 67 | 68 | if (applyToDescription && operation.description) { 69 | textContentFunction(operation.description, `paths.${pathKey}.${operationKey}.description`); 70 | } 71 | 72 | if (applyToSummary && operation.summary) { 73 | textContentFunction(operation.summary, `paths.${pathKey}.${operationKey}.summary`); 74 | } 75 | 76 | if (parameters) { 77 | parameters.forEach((parameter, parameterIndex) => { 78 | if (applyToDescription && parameter.description) { 79 | textContentFunction(parameter.description, `paths.${pathKey}.${operationKey}.parameters[${parameterIndex}].description`); 80 | } 81 | }); 82 | } 83 | 84 | if (responses) { 85 | Object.keys(responses).forEach((responseKey) => { 86 | const response = responses[responseKey]; 87 | 88 | if (applyToDescription && response.description) { 89 | textContentFunction(response.description, `paths.${pathKey}.${operationKey}.responses.${responseKey}.description`); 90 | } 91 | }); 92 | } 93 | }); 94 | }); 95 | } 96 | } 97 | } 98 | 99 | module.exports = TextParser; 100 | -------------------------------------------------------------------------------- /lib/rules/operation-payload-put.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const RuleFailure = require('../RuleFailure'); 6 | const List = require('immutable').List; 7 | 8 | /** 9 | * Returns true if get is valid enough to compare. Inserts errors and returns false if not. 10 | * 11 | * @param {Object} pathKey The entire path key used for logging errors. 12 | * @param {Object} get The get object being checked. 13 | * @param {Object} errorList The local list of errors. 14 | * 15 | * @returns {boolean} isValid returns true if the get object is valid. 16 | */ 17 | function validateGet(pathKey, get, errorList) { 18 | let isValid = false; 19 | 20 | const get200Response = get.responses['200']; 21 | 22 | if (!get200Response) { 23 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.get.responses.200`, hint: 'Missing 200 response' })); 24 | } else if (!get200Response.schema) { 25 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.get.responses.200.schema`, hint: 'Missing 200 response schema' })); 26 | } else { 27 | isValid = true; 28 | } 29 | 30 | return isValid; 31 | } 32 | 33 | /** 34 | * Returns an object with info about the put parameter. 35 | * Inserts errors and sets isValid to false if not valid. 36 | * 37 | * @param {Object} pathKey The entire path key used for logging errors. 38 | * @param {Object} put The put object being checked. 39 | * @param {Object} errorList The local list of errors. 40 | * 41 | * @returns {Object} object A complex object as a cache of fields that have already been fetched. 42 | */ 43 | function validatePut(pathKey, put, errorList) { 44 | let isValid = false; 45 | let putBodyLocation; 46 | let putBodyParamIndex; 47 | 48 | if (!put.parameters) { 49 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.put.parameters`, hint: 'Missing put parameters' })); 50 | } else { 51 | const putBodyParameter = _.find(put.parameters, (param, paramIndex) => { 52 | putBodyParamIndex = paramIndex; 53 | 54 | return param.in === 'body'; 55 | }); 56 | 57 | putBodyLocation = `paths.${pathKey}.put.parameters[${putBodyParamIndex}].schema`; 58 | if (!putBodyParameter) { 59 | errorList.push(new RuleFailure({ location: `paths.${pathKey}.put.parameters`, hint: 'Missing put parameters body' })); 60 | } else if (!putBodyParameter.schema) { 61 | errorList.push(new RuleFailure({ location: putBodyLocation, hint: 'Missing put parameters body schema' })); 62 | } else { 63 | isValid = true; 64 | } 65 | } 66 | 67 | // return complex object as a cache of fields that have already been fetched 68 | return { 69 | isValid, 70 | putBodyLocation, 71 | putBodyParamIndex 72 | }; 73 | } 74 | 75 | const rule = { 76 | description: 'enforce the PUT request payload matches the GET 200 response', 77 | validate(options, schema) { 78 | const errorList = []; 79 | 80 | if (schema.paths) { 81 | Object.keys(schema.paths).forEach((pathKey) => { 82 | const path = schema.paths[pathKey]; 83 | 84 | // Path must have get and put to be considered 85 | if (path.get && path.put) { 86 | const validGet = validateGet(pathKey, path.get, errorList); 87 | const validatePutResponse = validatePut(pathKey, path.put, errorList); 88 | 89 | if (validGet && validatePutResponse.isValid) { 90 | const getSchema = path.get.responses['200'].schema; 91 | const putSchema = path.put.parameters[validatePutResponse.putBodyParamIndex].schema; 92 | 93 | if (getSchema.$ref && putSchema.$ref) { 94 | if (getSchema.$ref !== putSchema.$ref) { 95 | errorList.push(new RuleFailure({ location: validatePutResponse.putBodyLocation, hint: 'Does not match' })); 96 | } else { 97 | // $refs successfully match 98 | } 99 | } else if (!_.isEqual(getSchema, putSchema)) { 100 | errorList.push(new RuleFailure({ location: validatePutResponse.putBodyLocation, hint: 'Does not match' })); 101 | } else { 102 | // non-$refs successfully match 103 | } 104 | } else { 105 | // errors are already inserted for invalid cases in validate*() methods above. 106 | } 107 | } else { 108 | // Ignore operations without both put/get 109 | } 110 | }); 111 | } 112 | 113 | return new List(errorList); 114 | } 115 | }; 116 | 117 | module.exports = rule; 118 | -------------------------------------------------------------------------------- /test/lib/rules/info-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const infoCustomRule = require('../../../lib/rules/info-custom'); 5 | 6 | describe('info-custom', () => { 7 | describe('description must be present', () => { 8 | const options = { 9 | whenField: '$key', 10 | whenPattern: '.*', 11 | thenField: 'description', 12 | thenPattern: '[a-zA-Z]' 13 | }; 14 | 15 | it('should not report errors when description is present', () => { 16 | const schema = { 17 | info: { 18 | description: 'The description' 19 | } 20 | }; 21 | 22 | const failures = infoCustomRule.validate(options, schema); 23 | 24 | assert.equal(failures.size, 0); 25 | }); 26 | 27 | it('should report error when description is not present', () => { 28 | const schema = { 29 | info: { 30 | } 31 | }; 32 | 33 | const failures = infoCustomRule.validate([options], schema); 34 | 35 | assert.equal(failures.size, 1); 36 | assert.equal(failures.get(0).get('location'), 'info'); 37 | assert.equal(failures.get(0).get('hint'), 'Expected info description to be present and to match "[a-zA-Z]"'); 38 | }); 39 | }); 40 | 41 | 42 | describe('x-publicDocsPath should be present and valid', () => { 43 | const options = { 44 | whenField: '$key', 45 | whenPattern: '.*', 46 | thenField: 'x-publicDocsPath', 47 | thenPattern: '^[a-zA-Z0-9-_.]+$' 48 | }; 49 | 50 | it('should not report errors when x-publicDocsPath is present', () => { 51 | const schema = { 52 | info: { 53 | 'x-publicDocsPath': 'myApiPath-includes-dashes' 54 | } 55 | }; 56 | 57 | const failures = infoCustomRule.validate(options, schema); 58 | 59 | assert.equal(failures.size, 0); 60 | }); 61 | 62 | it('should report error when x-publicDocsPath is not present', () => { 63 | const schema = { 64 | info: { 65 | } 66 | }; 67 | 68 | const failures = infoCustomRule.validate(options, schema); 69 | 70 | assert.equal(failures.size, 1); 71 | 72 | assert.equal(failures.get(0).get('location'), 'info'); 73 | assert.equal(failures.get(0).get('hint'), 'Expected info x-publicDocsPath to be present and to match "^[a-zA-Z0-9-_.]+$"'); 74 | }); 75 | 76 | it('should report error when x-publicDocsPath is not well formed', () => { 77 | const schema = { 78 | info: { 79 | 'x-publicDocsPath': 'my invalid #path' 80 | } 81 | }; 82 | 83 | const failures = infoCustomRule.validate(options, schema); 84 | 85 | assert.equal(failures.size, 1); 86 | assert.equal(failures.get(0).get('location'), 'info'); 87 | assert.equal(failures.get(0).get('hint'), 'Expected info x-publicDocsPath:"my invalid #path" to match "^[a-zA-Z0-9-_.]+$"'); 88 | }); 89 | 90 | it('should not report error when x-publicDocsPath has a period', () => { 91 | const schema = { 92 | info: { 93 | 'x-publicDocsPath': 'path.subpath' 94 | } 95 | }; 96 | 97 | const failures = infoCustomRule.validate(options, schema); 98 | 99 | assert.equal(failures.size, 0); 100 | }); 101 | }); 102 | describe('process nested properties in rules', () => { 103 | const options = { 104 | whenField: 'license', 105 | whenPattern: '.*', 106 | thenField: 'license.name', 107 | thenPattern: '^License Name$' 108 | }; 109 | 110 | it('should not report errors when name property matches', () => { 111 | const schema = { 112 | info: { 113 | license: { 114 | name: 'License Name', 115 | url: 'https://license.com' 116 | } 117 | } 118 | }; 119 | 120 | const failures = infoCustomRule.validate(options, schema); 121 | 122 | assert.equal(failures.size, 0); 123 | }); 124 | 125 | it('should report errors when name property does not match', () => { 126 | const schema = { 127 | info: { 128 | license: { 129 | name: 'none', 130 | url: 'https://license.com' 131 | } 132 | } 133 | }; 134 | 135 | const failures = infoCustomRule.validate([options], schema); 136 | 137 | assert.equal(failures.size, 1); 138 | assert.equal(failures.get(0).get('location'), 'info'); 139 | assert.equal(failures.get(0).get('hint'), 'Expected info license.name:"none" to match "^License Name$"'); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /lib/helpers/CustomValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RuleFailure = require('../RuleFailure'); 4 | const PatternOption = require('./PatternOption'); 5 | 6 | /** 7 | * This function enables field names with dot notation to be 8 | * used, allowing the rules to look for nested field names. 9 | * 10 | * @param {Object} obj The key's object 11 | * @param {String} path The option config for the field. 12 | * @returns {String} The value of the field referenced in the path 13 | */ 14 | const getDescendantProp = (obj, path) => ( 15 | path.split('.').reduce((acc, part) => acc && acc[part], obj) 16 | ); 17 | 18 | /** 19 | * Get the correct field, either the key or the object's field. 20 | * 21 | * @param {String} fieldValueSpecifier The option config for the field. 22 | * @param {Object} key The key name. 23 | * @param {Object} object The key's object. 24 | * @returns {String} The value of the field fieldValueSpecifier 25 | */ 26 | function getFieldValue(fieldValueSpecifier, key, object) { 27 | if (fieldValueSpecifier === '$key') { 28 | if (key === undefined) { 29 | throw new Error('$key is not valid for this rule.'); 30 | } 31 | 32 | return key; 33 | } 34 | 35 | return getDescendantProp(object, fieldValueSpecifier); 36 | } 37 | 38 | /** 39 | * Gets a normalized options object array. If options are just an object, wraps in an array. 40 | * @param {Object} options The provided options. 41 | * @returns {Array} An options array. 42 | */ 43 | function getNormalizedOptions(options) { 44 | const normalizedOptions = []; 45 | 46 | if (!Array.isArray(options)) { 47 | normalizedOptions.push(options); 48 | } else { 49 | normalizedOptions.push.apply(normalizedOptions, options); 50 | } 51 | 52 | return normalizedOptions; 53 | } 54 | 55 | class CustomValidator { 56 | constructor(options, rootSchema, errorList) { 57 | this.options = options; 58 | this.rootSchema = rootSchema; 59 | this.errorList = errorList; 60 | } 61 | 62 | /** 63 | * Validates the provided options. 64 | */ 65 | validateOptions() { 66 | if (!this.options) { 67 | throw new Error('Missing config'); 68 | } 69 | 70 | getNormalizedOptions(this.options).forEach((option) => { 71 | if (!option.whenField 72 | || (!PatternOption.isValidPatternOption('then', option)) 73 | || !option.thenField 74 | || (!PatternOption.isValidPatternOption('when', option))) { 75 | throw new Error(`Invalid option specified: ${JSON.stringify(option)}`); 76 | } 77 | }); 78 | } 79 | 80 | /** 81 | * Validates each object. 82 | * @param {Object} key The key name. 83 | * @param {Object} object The key's object. 84 | * @param {Object} pathToObject The path to the object for error reporting. 85 | * @param {Object} objectName The object name for error reporting. 86 | * @param {Function} predicateFunction A required function to let the calling method 87 | * run an additional check. 88 | */ 89 | validateAllCustoms(key, object, pathToObject, objectName, predicateFunction) { 90 | getNormalizedOptions(this.options).forEach((option) => { 91 | if (predicateFunction === undefined) { 92 | throw new Error('predicateFunction cannot be undefined'); 93 | } 94 | 95 | const shouldExecute = predicateFunction(option); 96 | 97 | if (shouldExecute) { 98 | const whenFieldValue = getFieldValue(option.whenField, key, object); 99 | const thenFieldValue = getFieldValue(option.thenField, key, object); 100 | 101 | const whenPatternOption = new PatternOption('when', option); 102 | const thenPatternOption = new PatternOption('then', option); 103 | 104 | if ((whenPatternOption.isAbsent && whenFieldValue === undefined) || 105 | (!whenPatternOption.isAbsent && whenPatternOption.getRegex().test(whenFieldValue))) { 106 | if (!thenPatternOption.isAbsent && thenFieldValue === undefined) { 107 | this.errorList.push(new RuleFailure({ 108 | location: pathToObject, 109 | hint: `Expected ${objectName} ${option.thenField} to be present and to match "${thenPatternOption.pattern}"` 110 | })); 111 | } else if (thenPatternOption.isAbsent && thenFieldValue !== undefined) { 112 | this.errorList.push(new RuleFailure({ 113 | location: pathToObject, 114 | hint: `Expected ${objectName} ${option.thenField}:"${thenFieldValue}" to be absent` 115 | })); 116 | } else if (!thenPatternOption.getRegex().test(thenFieldValue)) { 117 | this.errorList.push(new RuleFailure({ 118 | location: pathToObject, 119 | hint: `Expected ${objectName} ${option.thenField}:"${thenFieldValue}" to match "${thenPatternOption.pattern}"` 120 | })); 121 | } 122 | } 123 | } 124 | }); 125 | } 126 | } 127 | 128 | module.exports = CustomValidator; 129 | -------------------------------------------------------------------------------- /test/lib/rules/path-style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const pathStyleRule = require('../../../lib/rules/path-style'); 5 | 6 | describe('path-style', () => { 7 | const spineCaseOptions = { case: 'spine' }; 8 | const capSpineCaseOptions = { case: 'cap-spine' }; 9 | const snakeCaseOptions = { case: 'snake' }; 10 | const anyCaseOptions = { case: 'any' }; 11 | 12 | it('should not report errors for no paths', () => { 13 | const schema = { paths: { } }; 14 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 15 | 16 | assert.equal(failures.size, 0); 17 | }); 18 | 19 | it('should not report errors when the paths match the spine case', () => { 20 | const schema = { paths: { '/first/{id}/second-third': {}, '/fourth': {} } }; 21 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 22 | 23 | assert.equal(failures.size, 0); 24 | }); 25 | 26 | it('should not report errors when the paths match the cap-spine case', () => { 27 | const schema = { paths: { '/FIRST/{id}/SECOND-THIRD': { }, '/FOURTH': {} } }; 28 | const failures = pathStyleRule.validate(capSpineCaseOptions, schema); 29 | 30 | assert.equal(failures.size, 0); 31 | }); 32 | 33 | it('should not report errors when the paths match the snake case', () => { 34 | const schema = { paths: { '/first/{id}/second_third': {} } }; 35 | const failures = pathStyleRule.validate(snakeCaseOptions, schema); 36 | 37 | assert.equal(failures.size, 0); 38 | }); 39 | 40 | it('should not report errors when the paths match the any case', () => { 41 | const schema = { paths: { '/fiRsT/{id}/AFDS_0-awer': {} } }; 42 | const failures = pathStyleRule.validate(anyCaseOptions, schema); 43 | 44 | assert.equal(failures.size, 0); 45 | }); 46 | 47 | it('should not report errors when the paths is just a slash', () => { 48 | const schema = { paths: { '/': {} } }; 49 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 50 | 51 | assert.equal(failures.size, 0); 52 | }); 53 | 54 | it('should report an error when the config is not properly specified', () => { 55 | const badConfigRuleFunction = () => { 56 | const schema = {}; 57 | 58 | pathStyleRule.validate({}, schema); 59 | }; 60 | 61 | assert.throws(badConfigRuleFunction, Error, 'Invalid config specified'); 62 | }); 63 | 64 | it('should report an error for a path without a starting slash', () => { 65 | const schema = { paths: { pets: { } } }; 66 | 67 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 68 | 69 | assert.equal(failures.size, 1); 70 | assert.equal(failures.get(0).get('location'), 'paths.pets'); 71 | assert.equal(failures.get(0).get('hint'), 'Missing a leading slash'); 72 | }); 73 | 74 | it('should report an error for a path not matching the case', () => { 75 | const schema = { paths: { '/badCase': { } } }; 76 | 77 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 78 | 79 | assert.equal(failures.size, 1); 80 | assert.equal(failures.get(0).get('location'), 'paths./badCase'); 81 | assert.equal(failures.get(0).get('hint'), '"badCase" does not comply with case: "spine"'); 82 | }); 83 | 84 | it('should report an error for two slashes together', () => { 85 | const schema = { paths: { '/pets//food': { } } }; 86 | 87 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 88 | 89 | assert.equal(failures.size, 1); 90 | assert.equal(failures.get(0).get('location'), 'paths./pets//food'); 91 | assert.equal(failures.get(0).get('hint'), 'Must not have empty path elements'); 92 | }); 93 | 94 | it('should report an error for a path with a trailing slash', () => { 95 | const schema = { paths: { '/pets/': { } } }; 96 | 97 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 98 | 99 | assert.equal(failures.size, 1); 100 | assert.equal(failures.get(0).get('location'), 'paths./pets/'); 101 | assert.equal(failures.get(0).get('hint'), 'Must not have a trailing slash'); 102 | }); 103 | 104 | it('should report an error for a path with invalid path params', () => { 105 | const schema = { paths: { '/incomplete-param/{id/more-stuff': { } } }; 106 | 107 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 108 | 109 | assert.equal(failures.size, 1); 110 | assert.equal(failures.get(0).get('location'), 'paths./incomplete-param/{id/more-stuff'); 111 | assert.equal(failures.get(0).get('hint'), '"{id" does not comply with case: "spine"'); 112 | }); 113 | 114 | it('should report errors for a path with invalid path params', () => { 115 | const schema = { paths: { '/another-invalid-param/{id/more-stuff}': { } } }; 116 | 117 | const failures = pathStyleRule.validate(spineCaseOptions, schema); 118 | 119 | assert.equal(failures.size, 2); 120 | assert.equal(failures.get(0).get('location'), 'paths./another-invalid-param/{id/more-stuff}'); 121 | assert.equal(failures.get(0).get('hint'), '"{id" does not comply with case: "spine"'); 122 | assert.equal(failures.get(1).get('location'), 'paths./another-invalid-param/{id/more-stuff}'); 123 | assert.equal(failures.get(1).get('hint'), '"more-stuff}" does not comply with case: "spine"'); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/lib/rules/operation-payload-put.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const assert = require('chai').assert; 5 | const operationPayloadPutRule = require('../../../lib/rules/operation-payload-put'); 6 | 7 | describe('operation-payload-put', () => { 8 | const options = true; 9 | 10 | // validSchema is used as the base for all tests to make them easier to read. 11 | const validSchema = { 12 | paths: { 13 | '/pets': { 14 | get: { 15 | responses: { 16 | 200: { 17 | schema: { 18 | $ref: '#/definitions/pet' 19 | } 20 | } 21 | } 22 | }, 23 | put: { 24 | parameters: [ 25 | { 26 | in: 'body', 27 | schema: { 28 | $ref: '#/definitions/pet' 29 | } 30 | } 31 | ] 32 | }, 33 | parameters: [] 34 | } 35 | } 36 | }; 37 | 38 | it('should not report errors when the put request body parameter matches a get response', () => { 39 | const failures = operationPayloadPutRule.validate(options, validSchema); 40 | 41 | assert.equal(failures.size, 0); 42 | }); 43 | 44 | it('should report error when valid put request body parameter does not match its valid get response', () => { 45 | const schema = _.cloneDeep(validSchema); 46 | 47 | schema.paths['/pets'].put.parameters[0].schema.$ref = '#/definitions/alligator'; 48 | 49 | const failures = operationPayloadPutRule.validate(options, schema); 50 | 51 | assert.equal(failures.size, 1); 52 | assert.equal(failures.get(0).get('location'), 'paths./pets.put.parameters[0].schema'); 53 | assert.equal(failures.get(0).get('hint'), 'Does not match'); 54 | }); 55 | 56 | it('should report error when get 200 response is missing', () => { 57 | const schema = _.cloneDeep(validSchema); 58 | 59 | schema.paths['/pets'].get.responses['200'] = undefined; 60 | 61 | const failures = operationPayloadPutRule.validate(options, schema); 62 | 63 | assert.equal(failures.size, 1); 64 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.200'); 65 | assert.equal(failures.get(0).get('hint'), 'Missing 200 response'); 66 | }); 67 | 68 | it('should report error when get 200 schema is missing', () => { 69 | const schema = _.cloneDeep(validSchema); 70 | 71 | schema.paths['/pets'].get.responses['200'].schema = undefined; 72 | 73 | const failures = operationPayloadPutRule.validate(options, schema); 74 | 75 | assert.equal(failures.size, 1); 76 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.200.schema'); 77 | assert.equal(failures.get(0).get('hint'), 'Missing 200 response schema'); 78 | }); 79 | 80 | it('should report error when put parameters is missing', () => { 81 | const schema = _.cloneDeep(validSchema); 82 | 83 | schema.paths['/pets'].put.parameters = undefined; 84 | 85 | const failures = operationPayloadPutRule.validate(options, schema); 86 | 87 | assert.equal(failures.size, 1); 88 | assert.equal(failures.get(0).get('location'), 'paths./pets.put.parameters'); 89 | assert.equal(failures.get(0).get('hint'), 'Missing put parameters'); 90 | }); 91 | 92 | it('should report error when put parameters body is missing', () => { 93 | const schema = _.cloneDeep(validSchema); 94 | 95 | schema.paths['/pets'].put.parameters = []; 96 | 97 | const failures = operationPayloadPutRule.validate(options, schema); 98 | 99 | assert.equal(failures.size, 1); 100 | assert.equal(failures.get(0).get('location'), 'paths./pets.put.parameters'); 101 | assert.equal(failures.get(0).get('hint'), 'Missing put parameters body'); 102 | }); 103 | 104 | it('should report error when put parameters body schema is missing', () => { 105 | const schema = _.cloneDeep(validSchema); 106 | 107 | schema.paths['/pets'].put.parameters[0].schema = undefined; 108 | 109 | const failures = operationPayloadPutRule.validate(options, schema); 110 | 111 | assert.equal(failures.size, 1); 112 | assert.equal(failures.get(0).get('location'), 'paths./pets.put.parameters[0].schema'); 113 | assert.equal(failures.get(0).get('hint'), 'Missing put parameters body schema'); 114 | }); 115 | 116 | it('should report two errors when put and get are missing something', () => { 117 | const schema = _.cloneDeep(validSchema); 118 | 119 | schema.paths['/pets'].get.responses['200'] = undefined; 120 | schema.paths['/pets'].put.parameters[0].schema = undefined; 121 | 122 | const failures = operationPayloadPutRule.validate(options, schema); 123 | 124 | assert.equal(failures.size, 2); 125 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.200'); 126 | assert.equal(failures.get(0).get('hint'), 'Missing 200 response'); 127 | assert.equal(failures.get(1).get('location'), 'paths./pets.put.parameters[0].schema'); 128 | assert.equal(failures.get(1).get('hint'), 'Missing put parameters body schema'); 129 | }); 130 | 131 | it('should report two errors when put and get are missing other things', () => { 132 | const schema = _.cloneDeep(validSchema); 133 | 134 | schema.paths['/pets'].get.responses['200'].schema = undefined; 135 | schema.paths['/pets'].put.parameters = []; 136 | 137 | const failures = operationPayloadPutRule.validate(options, schema); 138 | 139 | assert.equal(failures.size, 2); 140 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.200.schema'); 141 | assert.equal(failures.get(0).get('hint'), 'Missing 200 response schema'); 142 | assert.equal(failures.get(1).get('location'), 'paths./pets.put.parameters'); 143 | assert.equal(failures.get(1).get('hint'), 'Missing put parameters body'); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /lib/helpers/SchemaObjectParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const RuleFailure = require('../RuleFailure'); 6 | const constants = require('../constants'); 7 | 8 | class SchemaObjectParser { 9 | constructor(rootSchema, errorList) { 10 | this.rootSchema = rootSchema; 11 | this.errorList = errorList; 12 | } 13 | 14 | /** 15 | * Validates the schemaObject (not the global schema). 16 | * 17 | * @param {Function} schemaFunction The function that checks schema objects. Can be undefined. 18 | * @param {Function} propertyFunction The function that checks properties. Can be undefined. 19 | * @param {Function} refFunction The function that checks schemas pointing to a $ref. 20 | * Can be undefined. 21 | * @param {Object} schemaObject The schema object being checked. 22 | * @param {Object} pathToSchema The path to the schema used for error messages. 23 | */ 24 | checkSchemaObject(schemaFunction, propertyFunction, refFunction, schemaObject, pathToSchema) { 25 | if (!schemaObject) { 26 | return; 27 | } 28 | 29 | if (schemaObject.$ref) { 30 | if (_.includes(this.visitedRefs, schemaObject.$ref)) { 31 | // found a ref that's already been visited. Skip it. 32 | return; 33 | } 34 | this.visitedRefs.push(schemaObject.$ref); 35 | 36 | if (refFunction) { 37 | refFunction(schemaObject, pathToSchema); 38 | } 39 | 40 | if (schemaObject.$ref.startsWith('#/definitions/')) { 41 | const trimmedRef = schemaObject.$ref.substr(2); 42 | const splitRef = trimmedRef.split('/'); 43 | 44 | const referencedSchema = this.rootSchema.definitions[splitRef[1]]; 45 | 46 | this.checkSchemaObject( 47 | schemaFunction, propertyFunction, refFunction, referencedSchema, pathToSchema); 48 | } else { 49 | this.errorList.push(new RuleFailure({ 50 | location: `${pathToSchema}`, 51 | hint: 'Found a non-internal reference' 52 | })); 53 | } 54 | } else { 55 | // found a schema, not a ref. 56 | if (schemaFunction) { 57 | schemaFunction(schemaObject, pathToSchema); 58 | } 59 | 60 | if (schemaObject.type === 'object' || schemaObject.openapilintType === 'allOf') { 61 | if (schemaObject.properties) { 62 | Object.keys(schemaObject.properties).forEach((propertyKey) => { 63 | const propertyObject = schemaObject.properties[propertyKey]; 64 | const pathToProperty = `${pathToSchema}.properties.${propertyKey}`; 65 | 66 | if (propertyFunction) { 67 | propertyFunction(propertyKey, propertyObject, pathToProperty); 68 | } 69 | 70 | this.checkSchemaObject( 71 | schemaFunction, propertyFunction, refFunction, propertyObject, pathToProperty); 72 | }); 73 | } 74 | } else if (schemaObject.type === 'array' && schemaObject.items) { 75 | this.checkSchemaObject(schemaFunction, propertyFunction, refFunction, schemaObject.items, `${pathToSchema}.items`); 76 | } 77 | } 78 | 79 | if (schemaObject.allOf) { 80 | schemaObject.allOf.forEach((allOfValue, allOfIndex) => { 81 | const implicitAllOfValue = allOfValue; 82 | 83 | // allOfs can include pieces that aren't defined within a full object type. 84 | // Therefore, assign it a special flag for the purposes of checking all properties. 85 | implicitAllOfValue.openapilintType = 'allOf'; 86 | this.checkSchemaObject(schemaFunction, propertyFunction, refFunction, implicitAllOfValue, `${pathToSchema}.allOf[${allOfIndex}]`); 87 | }); 88 | } 89 | } 90 | 91 | /** 92 | * Runs the propertyFunction for each property found. 93 | * 94 | * @param {Function} schemaFunction The function that checks schema objects. Can be undefined. 95 | * @param {Function} propertyFunction The function that checks properties. Can be undefined. 96 | * @param {Function} refFunction The function that checks schemas pointing to a $ref. 97 | * Can be undefined. 98 | */ 99 | forEachSchema(schemaFunction, propertyFunction, refFunction) { 100 | this.visitedRefs = []; 101 | if (this.rootSchema.paths) { 102 | Object.keys(this.rootSchema.paths).forEach((pathKey) => { 103 | const path = this.rootSchema.paths[pathKey]; 104 | 105 | // check each operation 106 | Object.keys(_.pick(path, constants.httpMethods)).forEach((operationKey) => { 107 | const operation = path[operationKey]; 108 | 109 | if (operation.parameters) { 110 | operation.parameters.forEach((parameter, parameterIndex) => { 111 | this.checkSchemaObject(schemaFunction, propertyFunction, refFunction, parameter.schema, `paths.${pathKey}.${operationKey}.parameters[${parameterIndex}].schema`); 112 | }); 113 | } 114 | 115 | if (operation.responses) { 116 | Object.keys(operation.responses).forEach((responseKey) => { 117 | const response = operation.responses[responseKey]; 118 | 119 | this.checkSchemaObject(schemaFunction, propertyFunction, refFunction, response.schema, `paths.${pathKey}.${operationKey}.responses.${responseKey}.schema`); 120 | }); 121 | } 122 | }); 123 | }); 124 | } 125 | } 126 | 127 | /** 128 | * Runs the propertyFunction for each property found. 129 | * 130 | * @param {Function} propertyFunction The function that checks properties. 131 | */ 132 | forEachProperty(propertyFunction) { 133 | this.forEachSchema(undefined, propertyFunction); 134 | } 135 | 136 | /** 137 | * Returns the list of all visited refs. Useful for checking orphans. 138 | * 139 | * @returns {array} the array of all visited refs. 140 | */ 141 | getVisitedRefs() { 142 | if (!this.visitedRefs) { 143 | // visit each node so that the visitedRefs get initialized. 144 | this.forEachProperty(); 145 | } 146 | 147 | return this.visitedRefs; 148 | } 149 | } 150 | 151 | module.exports = SchemaObjectParser; 152 | -------------------------------------------------------------------------------- /test/lib/rules/path-parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const assert = require('chai').assert; 5 | const pathParametersRule = require('../../../lib/rules/path-parameters'); 6 | 7 | describe('path-parameters', () => { 8 | const options = true; 9 | 10 | // validSchema is used as the base for all tests to make them easier to read. 11 | const validSchema = { 12 | paths: { 13 | '/first/{first_id}/second/{id}': { 14 | get: { 15 | parameters: [ 16 | { 17 | name: 'first_id', 18 | type: 'string', 19 | in: 'path', 20 | required: true 21 | }, 22 | { 23 | name: 'id', 24 | type: 'string', 25 | in: 'path', 26 | required: true 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | }; 33 | 34 | it('should not report errors when path parameters match the path parameters in the URI', () => { 35 | const failures = pathParametersRule.validate(options, validSchema); 36 | 37 | assert.equal(failures.size, 0); 38 | }); 39 | 40 | it('should report error when a parameter specified in path is missing from parameter list', () => { 41 | const schema = _.cloneDeep(validSchema); 42 | 43 | schema.paths['/first/{first_id}/second/{id}'].get.parameters = []; 44 | 45 | const failures = pathParametersRule.validate(options, schema); 46 | 47 | assert.equal(failures.size, 2); 48 | assert.equal(failures.get(0).get('location'), 'paths./first/{first_id}/second/{id}.get'); 49 | assert.equal(failures.get(0).get('hint'), 'Missing from parameter list: first_id'); 50 | assert.equal(failures.get(1).get('location'), 'paths./first/{first_id}/second/{id}.get'); 51 | assert.equal(failures.get(1).get('hint'), 'Missing from parameter list: id'); 52 | }); 53 | 54 | it('should report error when there is an extra path parameter in the parameters list not in path', () => { 55 | const schema = _.cloneDeep(validSchema); 56 | 57 | schema.paths['/first/{first_id}/second/{id}'].get.parameters.push({ 58 | name: 'third_id', 59 | type: 'string', 60 | in: 'path', 61 | required: true 62 | }); 63 | 64 | const failures = pathParametersRule.validate(options, schema); 65 | 66 | assert.equal(failures.size, 1); 67 | assert.equal(failures.get(0).get('location'), 'paths./first/{first_id}/second/{id}.get.parameters[2]'); 68 | assert.equal(failures.get(0).get('hint'), 'Missing from path template: third_id'); 69 | }); 70 | 71 | it('should report error when there is an path parameter without any required property', () => { 72 | const schema = _.cloneDeep(validSchema); 73 | 74 | delete schema.paths['/first/{first_id}/second/{id}'].get.parameters[0].required; 75 | 76 | const failures = pathParametersRule.validate(options, schema); 77 | 78 | assert.equal(failures.size, 1); 79 | assert.equal(failures.get(0).get('location'), 'paths./first/{first_id}/second/{id}.get.parameters[0]'); 80 | assert.equal(failures.get(0).get('hint'), 'Found path parameter without required=true: first_id'); 81 | }); 82 | 83 | it('should report error when there is an path parameter with required=false', () => { 84 | const schema = _.cloneDeep(validSchema); 85 | 86 | schema.paths['/first/{first_id}/second/{id}'].get.parameters[0].required = false; 87 | 88 | const failures = pathParametersRule.validate(options, schema); 89 | 90 | assert.equal(failures.size, 1); 91 | assert.equal(failures.get(0).get('location'), 'paths./first/{first_id}/second/{id}.get.parameters[0]'); 92 | assert.equal(failures.get(0).get('hint'), 'Found path parameter without required=true: first_id'); 93 | }); 94 | 95 | it('should report several errors for lots of problems', () => { 96 | const schema = { 97 | paths: { 98 | '/first/{id}': { 99 | get: {}, 100 | post: { 101 | parameters: [] 102 | } 103 | }, 104 | '/second/{customer_id}/details': { 105 | get: { 106 | parameters: [ 107 | { 108 | name: 'customer_id', 109 | type: 'string', 110 | in: 'path', 111 | required: true 112 | }, 113 | { 114 | name: 'extra_path_id', 115 | type: 'string', 116 | in: 'path', 117 | required: true 118 | } 119 | ] 120 | }, 121 | put: { 122 | parameters: [ 123 | { 124 | name: 'customer', 125 | type: 'string', 126 | in: 'path', 127 | required: true 128 | }, 129 | { 130 | name: 'extra_path_id', 131 | type: 'string', 132 | in: 'path', 133 | required: true 134 | } 135 | ] 136 | } 137 | }, 138 | '/missing_required/{customer_id}': { 139 | get: { 140 | parameters: [ 141 | { 142 | name: 'customer_id', 143 | type: 'string', 144 | in: 'path' 145 | } 146 | ] 147 | } 148 | } 149 | } 150 | }; 151 | 152 | const failures = pathParametersRule.validate(options, schema); 153 | 154 | assert.equal(failures.size, 7); 155 | assert.equal(failures.get(0).get('location'), 'paths./first/{id}.get'); 156 | assert.equal(failures.get(0).get('hint'), 'Missing from parameter list: id'); 157 | assert.equal(failures.get(1).get('location'), 'paths./first/{id}.post'); 158 | assert.equal(failures.get(1).get('hint'), 'Missing from parameter list: id'); 159 | assert.equal(failures.get(2).get('location'), 'paths./second/{customer_id}/details.get.parameters[1]'); 160 | assert.equal(failures.get(2).get('hint'), 'Missing from path template: extra_path_id'); 161 | assert.equal(failures.get(3).get('location'), 'paths./second/{customer_id}/details.put.parameters[0]'); 162 | assert.equal(failures.get(3).get('hint'), 'Missing from path template: customer'); 163 | assert.equal(failures.get(4).get('location'), 'paths./second/{customer_id}/details.put.parameters[1]'); 164 | assert.equal(failures.get(4).get('hint'), 'Missing from path template: extra_path_id'); 165 | assert.equal(failures.get(5).get('location'), 'paths./second/{customer_id}/details.put'); 166 | assert.equal(failures.get(5).get('hint'), 'Missing from parameter list: customer_id'); 167 | assert.equal(failures.get(6).get('location'), 'paths./missing_required/{customer_id}.get.parameters[0]'); 168 | assert.equal(failures.get(6).get('hint'), 'Found path parameter without required=true: customer_id'); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /docs/rules/properties-custom.md: -------------------------------------------------------------------------------- 1 | # enforce properties comply with custom config constraints (properties-custom) 2 | 3 | Validates that properties match their provided constraints. Constraints are provided in a simple format: 4 | 5 | 1. When `whenField` matches `whenPattern`, 6 | 2. Then `thenField` MUST match `thenPattern`. 7 | 8 | This format works for almost any constraint. `xField` is the name of the property's field, or `$key` to indicate the property's key. `xPattern` is a properly escaped regex string. For more than one constraint, use an array of constraint options. 9 | 10 | ## Config A 11 | 12 | Validates that properties with the word `country` are named `country_code`, or end with `_country_code`. 13 | 14 | ```json 15 | { 16 | "whenField": "$key", 17 | "whenPattern": "country", 18 | "thenField": "$key", 19 | "thenPattern": "^(?:.+_|)country_code$" 20 | } 21 | 22 | ``` 23 | 24 | ### Examples of *correct* usage with above config 25 | 26 | ```json 27 | { 28 | "definitions": { 29 | "Pet": { 30 | "properties": { 31 | "country_code": { 32 | "type": "string" 33 | } 34 | } 35 | }, 36 | "Pets": { 37 | "type": "array", 38 | "items": { 39 | "$ref": "#/definitions/Pet" 40 | } 41 | } 42 | }, 43 | "paths": { 44 | "/pets": { 45 | "get": { 46 | "parameters": [ 47 | { 48 | "in": "body", 49 | "schema": { 50 | "type": "object", 51 | "properties": { 52 | "super_special_country_code": { 53 | "type": "string" 54 | } 55 | } 56 | } 57 | } 58 | ], 59 | "responses": { 60 | "200": { 61 | "schema": { 62 | "$ref": "#/definitions/Pets" 63 | } 64 | } 65 | } 66 | }, 67 | "put": { 68 | "parameters": [ 69 | { 70 | "in": "body", 71 | "schema": { 72 | "allOf": [ 73 | { 74 | "type": "object", 75 | "properties": { 76 | "foreign_pet_country_code": { 77 | "type": "string" 78 | } 79 | } 80 | }, 81 | { 82 | "type": "object", 83 | "properties": { 84 | "moon_pet_country_code": { 85 | "type": "string" 86 | } 87 | } 88 | } 89 | ] 90 | } 91 | } 92 | ] 93 | } 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ### Examples of *incorrect* usage with above config 100 | 101 | ```json 102 | { 103 | "definitions": { 104 | "Pet": { 105 | "type": "object", 106 | "properties": { 107 | "my_country": { 108 | "type": "string" 109 | } 110 | } 111 | }, 112 | "Pets": { 113 | "type": "array", 114 | "items": { 115 | "$ref": "#/definitions/Pet" 116 | } 117 | } 118 | }, 119 | "paths": { 120 | "/pets": { 121 | "get": { 122 | "parameters": [ 123 | { 124 | "in": "body", 125 | "schema": { 126 | "type": "object", 127 | "properties": { 128 | "country": { 129 | "type": "string" 130 | } 131 | } 132 | } 133 | } 134 | ], 135 | "responses": { 136 | "200": { 137 | "schema": { 138 | "$ref": "#/definitions/Pets" 139 | } 140 | } 141 | } 142 | }, 143 | "put": { 144 | "parameters": [ 145 | { 146 | "in": "body", 147 | "schema": { 148 | "allOf": [ 149 | { 150 | "type": "object", 151 | "properties": { 152 | "my_country_tis_of_thee": { 153 | "type": "string" 154 | } 155 | } 156 | }, 157 | { 158 | "type": "object", 159 | "properties": { 160 | "country_specific_mega_code": { 161 | "type": "string" 162 | } 163 | } 164 | } 165 | ] 166 | } 167 | } 168 | ] 169 | } 170 | } 171 | } 172 | } 173 | ``` 174 | 175 | 176 | ## Config B 177 | 178 | Validates that any properties ending in `url` or `uri` include `{"format": "uri"}`. 179 | 180 | ```json 181 | { 182 | "whenField": "$key", 183 | "whenPattern": "ur[l|i]$", 184 | "thenField": "format", 185 | "thenPattern": "^uri$" 186 | } 187 | 188 | ``` 189 | 190 | 191 | ### Examples of *correct* usage with above config 192 | 193 | ```json 194 | { 195 | "paths": { 196 | "/pets": { 197 | "get": { 198 | "parameters": [ 199 | { 200 | "in": "body", 201 | "schema": { 202 | "type": "object", 203 | "properties": { 204 | "service_uri": { 205 | "type": "string", 206 | "format": "uri" 207 | }, 208 | "profile_url": { 209 | "type": "string", 210 | "format": "uri" 211 | } 212 | } 213 | } 214 | } 215 | ], 216 | "responses": { 217 | "200": { 218 | "schema": { 219 | "type": "object", 220 | "properties": { 221 | "my_return_url": { 222 | "type": "string", 223 | "format": "uri" 224 | } 225 | } 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | ``` 234 | 235 | ### Examples of **incorrect** usage with above config 236 | 237 | ```json 238 | { 239 | "paths": { 240 | "/pets": { 241 | "get": { 242 | "parameters": [ 243 | { 244 | "in": "body", 245 | "schema": { 246 | "type": "object", 247 | "properties": { 248 | "service_uri": { 249 | "type": "string" 250 | }, 251 | "profile_url": { 252 | "type": "string" 253 | } 254 | } 255 | } 256 | } 257 | ], 258 | "responses": { 259 | "200": { 260 | "schema": { 261 | "type": "object", 262 | "properties": { 263 | "my_return_url": { 264 | "type": "string" 265 | } 266 | } 267 | } 268 | } 269 | } 270 | } 271 | } 272 | } 273 | } 274 | ``` 275 | -------------------------------------------------------------------------------- /test/lib/rules/properties-style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const propertiesStyleRule = require('../../../lib/rules/properties-style'); 5 | 6 | describe('properties-style', () => { 7 | const snakeCaseOptions = { case: 'snake' }; 8 | 9 | it('should not report errors when the properties match the snake case', () => { 10 | const schema = { 11 | paths: { 12 | '/pets': { 13 | get: { 14 | parameters: [ 15 | { 16 | schema: { 17 | type: 'object', 18 | properties: { 19 | super_special_country_code: { 20 | type: 'string' 21 | } 22 | } 23 | } 24 | } 25 | ], 26 | responses: { 27 | 200: { 28 | schema: { 29 | type: 'object', 30 | properties: { 31 | country_code: { 32 | type: 'string' 33 | } 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | put: { 40 | parameters: [ 41 | { 42 | schema: { 43 | allOf: [ 44 | { 45 | type: 'object', 46 | properties: { 47 | foreign_pet_country_code: { 48 | type: 'string' 49 | } 50 | } 51 | }, 52 | { 53 | type: 'object', 54 | properties: { 55 | moon_pet_country_code: { 56 | type: 'string' 57 | } 58 | } 59 | } 60 | ] 61 | } 62 | } 63 | ] 64 | } 65 | } 66 | } 67 | }; 68 | 69 | const failures = propertiesStyleRule.validate(snakeCaseOptions, schema); 70 | 71 | assert.equal(failures.size, 0); 72 | }); 73 | 74 | it('should not report errors when properties defined in refs are snake case', () => { 75 | const schema = { 76 | definitions: { 77 | Pet: { 78 | type: 'object', 79 | properties: { 80 | country_code: { 81 | type: 'string' 82 | }, 83 | circular_ref: { 84 | $ref: '#/definitions/Pets' 85 | } 86 | } 87 | }, 88 | Pets: { 89 | type: 'array', 90 | items: { 91 | $ref: '#/definitions/Pet' 92 | } 93 | } 94 | }, 95 | paths: { 96 | '/pets': { 97 | get: { 98 | responses: { 99 | 200: { 100 | schema: { 101 | $ref: '#/definitions/Pets' 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | }; 109 | 110 | const failures = propertiesStyleRule.validate(snakeCaseOptions, schema); 111 | 112 | assert.equal(failures.size, 0); 113 | }); 114 | 115 | 116 | it('should report an error when a property defined in refs does not match snake case', () => { 117 | const schema = { 118 | definitions: { 119 | Pet: { 120 | type: 'object', 121 | properties: { 122 | MY_COUNTRY: { 123 | type: 'string' 124 | } 125 | } 126 | }, 127 | Pets: { 128 | type: 'array', 129 | items: { 130 | $ref: '#/definitions/Pet' 131 | } 132 | } 133 | }, 134 | paths: { 135 | '/pets': { 136 | get: { 137 | responses: { 138 | 200: { 139 | schema: { 140 | $ref: '#/definitions/Pets' 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | }; 148 | 149 | const failures = propertiesStyleRule.validate(snakeCaseOptions, schema); 150 | 151 | assert.equal(failures.size, 1); 152 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.200.schema.items.properties.MY_COUNTRY'); 153 | assert.equal(failures.get(0).get('hint'), '"MY_COUNTRY" does not comply with case: "snake"'); 154 | }); 155 | 156 | it('should report errors when a parameter property does not match snake case', () => { 157 | const schema = { 158 | paths: { 159 | '/pets': { 160 | get: { 161 | parameters: [ 162 | { 163 | schema: { 164 | type: 'object', 165 | properties: { 166 | 'NOT-SNAKE': { 167 | type: 'string' 168 | } 169 | } 170 | } 171 | } 172 | ] 173 | } 174 | } 175 | } 176 | }; 177 | 178 | const failures = propertiesStyleRule.validate(snakeCaseOptions, schema); 179 | 180 | assert.equal(failures.size, 1); 181 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.parameters[0].schema.properties.NOT-SNAKE'); 182 | assert.equal(failures.get(0).get('hint'), '"NOT-SNAKE" does not comply with case: "snake"'); 183 | }); 184 | 185 | it('should report an error when a response property is snot snake case', () => { 186 | const schema = { 187 | paths: { 188 | '/pets': { 189 | get: { 190 | responses: { 191 | 200: { 192 | schema: { 193 | type: 'object', 194 | properties: { 195 | country_code_blah_BLAH: { 196 | type: 'string' 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } 205 | }; 206 | 207 | const failures = propertiesStyleRule.validate(snakeCaseOptions, schema); 208 | 209 | assert.equal(failures.size, 1); 210 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.200.schema.properties.country_code_blah_BLAH'); 211 | assert.equal(failures.get(0).get('hint'), '"country_code_blah_BLAH" does not comply with case: "snake"'); 212 | }); 213 | 214 | it('should report two errors when two allOf parameters property are not snake case', () => { 215 | const schema = { 216 | paths: { 217 | '/pets': { 218 | put: { 219 | parameters: [ 220 | { 221 | schema: { 222 | allOf: [ 223 | { 224 | type: 'object', 225 | properties: { 226 | 'NOT-SNAKE': { 227 | type: 'string' 228 | } 229 | } 230 | }, 231 | { 232 | type: 'object', 233 | properties: { 234 | 'NOT-SNAKE_2': { 235 | type: 'string' 236 | } 237 | } 238 | } 239 | ] 240 | } 241 | } 242 | ] 243 | } 244 | } 245 | } 246 | }; 247 | 248 | const failures = propertiesStyleRule.validate(snakeCaseOptions, schema); 249 | 250 | assert.equal(failures.size, 2); 251 | assert.equal(failures.get(0).get('location'), 'paths./pets.put.parameters[0].schema.allOf[0].properties.NOT-SNAKE'); 252 | assert.equal(failures.get(0).get('hint'), '"NOT-SNAKE" does not comply with case: "snake"'); 253 | assert.equal(failures.get(1).get('location'), 'paths./pets.put.parameters[0].schema.allOf[1].properties.NOT-SNAKE_2'); 254 | assert.equal(failures.get(1).get('hint'), '"NOT-SNAKE_2" does not comply with case: "snake"'); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /test/lib/rules/schema-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const assert = require('chai').assert; 5 | const schemaCustomRule = require('../../../lib/rules/schema-custom'); 6 | 7 | describe('schema-custom', () => { 8 | describe('type = object must have non-whitespace titles', () => { 9 | const basicOptions = { 10 | whenField: 'type', 11 | whenPattern: 'object', 12 | thenField: 'title', 13 | thenPattern: '\\s' 14 | }; 15 | const allOfOptions = _.cloneDeep(basicOptions); 16 | 17 | allOfOptions.alsoApplyTo = ['allOf']; 18 | 19 | it('should not report errors when titles are present and valid', () => { 20 | const schema = { 21 | definitions: { 22 | Pet: { 23 | type: 'object', 24 | title: 'Pet title', 25 | properties: { 26 | country_code: { 27 | type: 'string' 28 | } 29 | } 30 | }, 31 | Pets: { 32 | type: 'array', 33 | items: { 34 | $ref: '#/definitions/Pet' 35 | } 36 | } 37 | }, 38 | paths: { 39 | '/pets': { 40 | get: { 41 | parameters: [ 42 | { 43 | in: 'body', 44 | schema: { 45 | title: 'Body schema', 46 | type: 'object' 47 | } 48 | } 49 | ], 50 | responses: { 51 | 200: { 52 | schema: { 53 | $ref: '#/definitions/Pets' 54 | } 55 | } 56 | } 57 | }, 58 | put: { 59 | parameters: [ 60 | { 61 | in: 'body', 62 | schema: { 63 | allOf: [ 64 | { 65 | type: 'object' 66 | }, 67 | { 68 | type: 'object' 69 | }, 70 | { 71 | type: 'string' 72 | } 73 | ] 74 | } 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | }; 81 | 82 | const failures = schemaCustomRule.validate(basicOptions, schema); 83 | 84 | assert.equal(failures.size, 0); 85 | }); 86 | 87 | it('should report errors when titles are not present', () => { 88 | const schema = { 89 | definitions: { 90 | Pet: { 91 | type: 'object', 92 | properties: { 93 | country_code: { 94 | type: 'string' 95 | } 96 | } 97 | }, 98 | Pets: { 99 | type: 'array', 100 | items: { 101 | $ref: '#/definitions/Pet' 102 | } 103 | } 104 | }, 105 | paths: { 106 | '/pets': { 107 | get: { 108 | parameters: [ 109 | { 110 | in: 'body', 111 | schema: { 112 | type: 'object' 113 | } 114 | } 115 | ], 116 | responses: { 117 | 200: { 118 | schema: { 119 | $ref: '#/definitions/Pets' 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | }; 127 | 128 | const failures = schemaCustomRule.validate([basicOptions], schema); 129 | 130 | assert.equal(failures.size, 2); 131 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.parameters[0].schema'); 132 | assert.equal(failures.get(0).get('hint'), 'Expected schema title to be present and to match "\\s"'); 133 | assert.equal(failures.get(1).get('location'), 'paths./pets.get.responses.200.schema.items'); 134 | assert.equal(failures.get(1).get('hint'), 'Expected schema title to be present and to match "\\s"'); 135 | }); 136 | 137 | 138 | it('should not report errors when titles are present and valid with alsoApplyTo:[allOf]', () => { 139 | const schema = { 140 | paths: { 141 | '/pets': { 142 | put: { 143 | parameters: [ 144 | { 145 | in: 'body', 146 | schema: { 147 | allOf: [ 148 | { 149 | type: 'object', 150 | title: 'allOf object title' 151 | }, 152 | { 153 | type: 'object', 154 | title: 'allOf 2 object title' 155 | }, 156 | { 157 | type: 'string' 158 | } 159 | ] 160 | } 161 | } 162 | ] 163 | } 164 | } 165 | } 166 | }; 167 | 168 | const failures = schemaCustomRule.validate(allOfOptions, schema); 169 | 170 | assert.equal(failures.size, 0); 171 | }); 172 | 173 | it('should report errors when titles are not present in allOf with alsoApplyTo:[allOf]', () => { 174 | const schema = { 175 | paths: { 176 | '/pets': { 177 | put: { 178 | parameters: [ 179 | { 180 | in: 'body', 181 | schema: { 182 | allOf: [ 183 | { 184 | type: 'object' 185 | }, 186 | { 187 | type: 'object' 188 | }, 189 | { 190 | type: 'string' 191 | } 192 | ] 193 | } 194 | } 195 | ] 196 | } 197 | } 198 | } 199 | }; 200 | 201 | const failures = schemaCustomRule.validate([allOfOptions], schema); 202 | 203 | assert.equal(failures.size, 2); 204 | assert.equal(failures.get(0).get('location'), 'paths./pets.put.parameters[0].schema.allOf[0]'); 205 | assert.equal(failures.get(0).get('hint'), 'Expected schema title to be present and to match "\\s"'); 206 | assert.equal(failures.get(1).get('location'), 'paths./pets.put.parameters[0].schema.allOf[1]'); 207 | assert.equal(failures.get(1).get('hint'), 'Expected schema title to be present and to match "\\s"'); 208 | }); 209 | 210 | 211 | it('should report errors when titles are not present', () => { 212 | const optionsReferencingMissingField = { 213 | whenField: 'type', 214 | whenPattern: 'object', 215 | thenField: 'title', 216 | thenPattern: '[a-zA-Z]' 217 | }; 218 | 219 | const schema = { 220 | definitions: { 221 | Pet: { 222 | type: 'object', 223 | properties: { 224 | country_code: { 225 | type: 'string' 226 | } 227 | } 228 | }, 229 | Pets: { 230 | type: 'array', 231 | items: { 232 | $ref: '#/definitions/Pet' 233 | } 234 | } 235 | }, 236 | paths: { 237 | '/pets': { 238 | get: { 239 | parameters: [ 240 | { 241 | in: 'body', 242 | schema: { 243 | type: 'object' 244 | } 245 | } 246 | ], 247 | responses: { 248 | 200: { 249 | schema: { 250 | $ref: '#/definitions/Pets' 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } 257 | }; 258 | 259 | const failures = schemaCustomRule.validate(optionsReferencingMissingField, schema); 260 | 261 | assert.equal(failures.size, 2); 262 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.parameters[0].schema'); 263 | assert.equal(failures.get(0).get('hint'), 'Expected schema title to be present and to match "[a-zA-Z]"'); 264 | assert.equal(failures.get(1).get('location'), 'paths./pets.get.responses.200.schema.items'); 265 | assert.equal(failures.get(1).get('hint'), 'Expected schema title to be present and to match "[a-zA-Z]"'); 266 | }); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /test/lib/rules/parameters-custom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const parametersCustomRule = require('../../../lib/rules/parameters-custom'); 5 | 6 | describe('parameters-custom', () => { 7 | describe('PayPal-Request-Id parameters must have a good description', () => { 8 | const options = { 9 | whenField: 'name', 10 | whenPattern: '^PayPal-Request-Id$', 11 | thenField: 'description', 12 | thenPattern: '^(The server stores keys ((for \\d+ (day(s)?|hour(s)?))|forever))|((When .*, the server .*){2,})\\.$' 13 | }; 14 | 15 | it('should not report errors when PayPal-Request-Id parameter is correct', () => { 16 | const schema = { 17 | paths: { 18 | '/pets': { 19 | post: { 20 | parameters: [ 21 | { 22 | name: 'PayPal-Request-Id', 23 | in: 'header', 24 | type: 'string', 25 | description: 'The server stores keys for 24 hours.', 26 | required: false 27 | } 28 | ] 29 | } 30 | }, 31 | '/wild-animals': { 32 | post: { 33 | parameters: [ 34 | { 35 | name: 'PayPal-Request-Id', 36 | in: 'header', 37 | type: 'string', 38 | description: 'The server stores keys forever.', 39 | required: false 40 | } 41 | ] 42 | } 43 | }, 44 | '/rabid-creatures': { 45 | post: { 46 | parameters: [ 47 | { 48 | name: 'PayPal-Request-Id', 49 | in: 'header', 50 | type: 'string', 51 | description: 'The server stores keys for 30 days.', 52 | required: false 53 | } 54 | ] 55 | } 56 | }, 57 | '/krazy-kats': { 58 | post: { 59 | parameters: [ 60 | { 61 | name: 'PayPal-Request-Id', 62 | in: 'header', 63 | type: 'string', 64 | description: 'When intent=action, the server stores keys for 5 days. When intent=badaction, the server ignores this header.', 65 | required: false 66 | } 67 | ] 68 | } 69 | } 70 | } 71 | }; 72 | 73 | const failures = parametersCustomRule.validate(options, schema); 74 | 75 | assert.equal(failures.size, 0); 76 | }); 77 | 78 | it('should report an error when the description does not match pattern', () => { 79 | const schema = { 80 | paths: { 81 | '/pets': { 82 | get: { 83 | parameters: [ 84 | { 85 | name: 'PayPal-Request-Id', 86 | in: 'header', 87 | type: 'string', 88 | description: 'This header description is not awesome.', 89 | required: false 90 | } 91 | ] 92 | } 93 | } 94 | } 95 | }; 96 | 97 | const failures = parametersCustomRule.validate([options], schema); 98 | 99 | assert.equal(failures.size, 1); 100 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.parameters[0]'); 101 | assert.equal(failures.get(0).get('hint'), 'Expected parameter description:"This header description is not awesome." to match "^(The server stores keys ((for \\d+ (day(s)?|hour(s)?))|forever))|((When .*, the server .*){2,})\\.$"'); 102 | }); 103 | }); 104 | 105 | describe('PayPal-Request-Id parameters must be capitalized correctly', () => { 106 | const options = { 107 | whenField: 'name', 108 | whenPatternIgnoreCase: '^PayPal-Request-Id$', 109 | thenField: 'name', 110 | thenPattern: '^PayPal-Request-Id$' 111 | }; 112 | 113 | it('should not report errors when PayPal-Request-Id parameter is correct', () => { 114 | const schema = { 115 | paths: { 116 | '/pets': { 117 | get: { 118 | parameters: [ 119 | { 120 | name: 'PayPal-Request-Id' 121 | } 122 | ] 123 | } 124 | } 125 | } 126 | }; 127 | 128 | const failures = parametersCustomRule.validate(options, schema); 129 | 130 | assert.equal(failures.size, 0); 131 | }); 132 | 133 | it('should report an error when the case is incorrect', () => { 134 | const schema = { 135 | paths: { 136 | '/pets': { 137 | get: { 138 | parameters: [ 139 | { 140 | name: 'PAYPAL-REQUEST-ID' 141 | } 142 | ] 143 | } 144 | } 145 | } 146 | }; 147 | 148 | const failures = parametersCustomRule.validate([options], schema); 149 | 150 | assert.equal(failures.size, 1); 151 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.parameters[0]'); 152 | assert.equal(failures.get(0).get('hint'), 'Expected parameter name:"PAYPAL-REQUEST-ID" to match "^PayPal-Request-Id$"'); 153 | }); 154 | }); 155 | 156 | 157 | describe('PayPal-Partner-Attribution-Id parameters must not have a description', () => { 158 | const options = { 159 | whenField: 'name', 160 | whenPatternIgnoreCase: 'PayPal-Partner-Attribution-Id', 161 | thenField: 'description', 162 | thenAbsent: true 163 | }; 164 | 165 | it('should not report errors when correct', () => { 166 | const schema = { 167 | paths: { 168 | '/pets': { 169 | get: { 170 | parameters: [ 171 | { 172 | name: 'PayPal-Partner-Attribution-Id' 173 | } 174 | ] 175 | } 176 | } 177 | } 178 | }; 179 | 180 | const failures = parametersCustomRule.validate(options, schema); 181 | 182 | assert.equal(failures.size, 0); 183 | }); 184 | 185 | 186 | it('should report an error when the description is present', () => { 187 | const schema = { 188 | paths: { 189 | '/pets': { 190 | get: { 191 | parameters: [ 192 | { 193 | name: 'PayPal-Partner-Attribution-Id', 194 | description: 'This should not be here' 195 | } 196 | ] 197 | } 198 | } 199 | } 200 | }; 201 | 202 | const failures = parametersCustomRule.validate([options], schema); 203 | 204 | assert.equal(failures.size, 1); 205 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.parameters[0]'); 206 | assert.equal(failures.get(0).get('hint'), 'Expected parameter description:"This should not be here" to be absent'); 207 | }); 208 | }); 209 | 210 | describe('If name is absent, description must say "blah"', () => { 211 | const options = { 212 | whenField: 'name', 213 | whenAbsent: true, 214 | thenField: 'description', 215 | thenPattern: 'blah' 216 | }; 217 | 218 | it('should not report errors when correct', () => { 219 | const schema = { 220 | paths: { 221 | '/pets': { 222 | get: { 223 | parameters: [ 224 | { 225 | description: 'blah' 226 | }, 227 | { 228 | name: 'my_param', 229 | description: 'not blah' 230 | } 231 | ] 232 | } 233 | } 234 | } 235 | }; 236 | 237 | const failures = parametersCustomRule.validate(options, schema); 238 | 239 | assert.equal(failures.size, 0); 240 | }); 241 | 242 | 243 | it('should report an error when the description is present', () => { 244 | const schema = { 245 | paths: { 246 | '/pets': { 247 | get: { 248 | parameters: [ 249 | { 250 | description: 'bad description' 251 | } 252 | ] 253 | } 254 | } 255 | } 256 | }; 257 | 258 | const failures = parametersCustomRule.validate([options], schema); 259 | 260 | assert.equal(failures.size, 1); 261 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.parameters[0]'); 262 | assert.equal(failures.get(0).get('hint'), 'Expected parameter description:"bad description" to match "blah"'); 263 | }); 264 | }); 265 | 266 | describe('bad config', () => { 267 | it('should throw an error with empty config', () => { 268 | const badConfigRuleFunction = () => { 269 | const schema = {}; 270 | 271 | parametersCustomRule.validate({}, schema); 272 | }; 273 | 274 | assert.throws(badConfigRuleFunction, Error, 'Invalid option specified: {}'); 275 | }); 276 | 277 | it('should throw an error missing option fields', () => { 278 | const badConfigRuleFunction = () => { 279 | const schema = {}; 280 | const options = { 281 | whenField: 'name' 282 | }; 283 | 284 | parametersCustomRule.validate(options, schema); 285 | }; 286 | 287 | assert.throws(badConfigRuleFunction, Error, 'Invalid option specified: {"whenField":"name"}'); 288 | }); 289 | 290 | it('should throw an error when a pattern and patternIgnoreCase is provided', () => { 291 | const badConfigRuleFunction = () => { 292 | const schema = {}; 293 | const options = { 294 | whenField: 'name', 295 | whenPattern: 'X', 296 | whenPatternIgnoreCase: 'X', 297 | thenField: 'description', 298 | thenPattern: 'X' 299 | }; 300 | 301 | parametersCustomRule.validate(options, schema); 302 | }; 303 | 304 | assert.throws(badConfigRuleFunction, Error, 'Invalid option specified: {"whenField":"name","whenPattern":"X","whenPatternIgnoreCase":"X","thenField":"description","thenPattern":"X"}'); 305 | }); 306 | }); 307 | }); 308 | -------------------------------------------------------------------------------- /test/lib/rules/text-content.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const textContentRule = require('../../../lib/rules/text-content'); 5 | 6 | describe('text-content', () => { 7 | describe('title, summary, and description all start with a capital letter', () => { 8 | const options = { 9 | applyTo: [ 10 | 'title', 11 | 'summary', 12 | 'description' 13 | ], 14 | matchPattern: '^[A-Z]' 15 | }; 16 | 17 | it('should not report errors when valid', () => { 18 | const schema = { 19 | info: { 20 | title: 'Good title with no leading spaces' 21 | }, 22 | paths: { 23 | '/pets': { 24 | get: { 25 | summary: 'The correct case summary', 26 | parameters: [ 27 | { 28 | description: 'The correct case description' 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | }; 35 | 36 | const failures = textContentRule.validate(options, schema); 37 | 38 | assert.equal(failures.size, 0); 39 | }); 40 | 41 | it('should report 3 errors when does not start with capital letter', () => { 42 | const schema = { 43 | info: { 44 | title: ' Title with spaces' 45 | }, 46 | paths: { 47 | '/pets': { 48 | get: { 49 | summary: 'the lower case summary', 50 | parameters: [ 51 | { 52 | description: 'the lower case description' 53 | } 54 | ] 55 | } 56 | } 57 | } 58 | }; 59 | 60 | const failures = textContentRule.validate(options, schema); 61 | 62 | assert.equal(failures.size, 3); 63 | assert.equal(failures.get(0).get('location'), 'info.title'); 64 | assert.equal(failures.get(0).get('hint'), 'Expected " Title with spaces" to match "^[A-Z]"'); 65 | assert.equal(failures.get(1).get('location'), 'paths./pets.get.summary'); 66 | assert.equal(failures.get(1).get('hint'), 'Expected "the lower case summary" to match "^[A-Z]"'); 67 | assert.equal(failures.get(2).get('location'), 'paths./pets.get.parameters[0].description'); 68 | assert.equal(failures.get(2).get('hint'), 'Expected "the lower case description" to match "^[A-Z]"'); 69 | }); 70 | }); 71 | 72 | describe('title, summary, and description all start with a letter', () => { 73 | const options = { 74 | applyTo: [ 75 | 'title', 76 | 'summary', 77 | 'description' 78 | ], 79 | matchPatternIgnoreCase: '^[A-Z]' 80 | }; 81 | 82 | it('should not report errors when valid', () => { 83 | const schema = { 84 | info: { 85 | title: 'Good title with no leading spaces' 86 | }, 87 | paths: { 88 | '/pets': { 89 | get: { 90 | summary: 'the correct case summary', 91 | parameters: [ 92 | { 93 | description: 'the correct case description' 94 | } 95 | ] 96 | } 97 | } 98 | } 99 | }; 100 | 101 | const failures = textContentRule.validate(options, schema); 102 | 103 | assert.equal(failures.size, 0); 104 | }); 105 | 106 | it('should report 1 error when there are spaces in front of a title', () => { 107 | const schema = { 108 | info: { 109 | title: ' Title with spaces' 110 | }, 111 | paths: { 112 | '/pets': { 113 | get: { 114 | summary: 'the lower case summary', 115 | parameters: [ 116 | { 117 | description: 'The upper case description' 118 | } 119 | ] 120 | } 121 | } 122 | } 123 | }; 124 | 125 | const failures = textContentRule.validate(options, schema); 126 | 127 | assert.equal(failures.size, 1); 128 | assert.equal(failures.get(0).get('location'), 'info.title'); 129 | assert.equal(failures.get(0).get('hint'), 'Expected " Title with spaces" to match "^[A-Z]"'); 130 | }); 131 | }); 132 | 133 | 134 | describe('summary and description all end with a period (`.`).', () => { 135 | const options = { 136 | applyTo: [ 137 | 'summary', 138 | 'description' 139 | ], 140 | matchPattern: '\\.$' 141 | }; 142 | 143 | it('should not report errors when valid', () => { 144 | const schema = { 145 | definitions: { 146 | Pet: {} 147 | }, 148 | info: { 149 | title: 'Should ignore this title' 150 | }, 151 | paths: { 152 | '/pets': { 153 | get: { 154 | summary: 'The correct punctuated summary.', 155 | parameters: [ 156 | { 157 | description: 'The correct punctuated description.' 158 | } 159 | ], 160 | responses: { 161 | 200: { 162 | schema: { 163 | $ref: '#/definitions/Pet' 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | }; 171 | 172 | const failures = textContentRule.validate(options, schema); 173 | 174 | assert.equal(failures.size, 0); 175 | }); 176 | 177 | it('should report only 2 errors when punctuation is incorrect', () => { 178 | const schema = { 179 | paths: { 180 | '/pets': { 181 | get: { 182 | summary: 'The incorrect summary without punctuation', 183 | parameters: [ 184 | { 185 | description: 'The incorrect description with trailing spaces. ' 186 | } 187 | ], 188 | responses: { 189 | 200: { 190 | schema: { 191 | title: 'Any issue with titles should be ignored' 192 | } 193 | } 194 | } 195 | } 196 | } 197 | } 198 | }; 199 | 200 | const failures = textContentRule.validate(options, schema); 201 | 202 | assert.equal(failures.size, 2); 203 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.summary'); 204 | assert.equal(failures.get(0).get('hint'), 'Expected "The incorrect summary without punctuation" to match "\\.$"'); 205 | assert.equal(failures.get(1).get('location'), 'paths./pets.get.parameters[0].description'); 206 | assert.equal(failures.get(1).get('hint'), 'Expected "The incorrect description with trailing spaces. " to match "\\.$"'); 207 | }); 208 | }); 209 | 210 | 211 | describe('descriptions should not have the word supersecret, any case', () => { 212 | const options = { 213 | applyTo: [ 214 | 'description' 215 | ], 216 | notMatchPatternIgnoreCase: 'supersecret' 217 | }; 218 | 219 | it('should not report errors when valid', () => { 220 | const schema = { 221 | info: { 222 | description: 'Good description with no secret words' 223 | } 224 | }; 225 | 226 | const failures = textContentRule.validate(options, schema); 227 | 228 | assert.equal(failures.size, 0); 229 | }); 230 | 231 | it('should report errors when secret words are present', () => { 232 | const schema = { 233 | info: { 234 | description: 'Bad description with superSECRET word' 235 | } 236 | }; 237 | 238 | const failures = textContentRule.validate(options, schema); 239 | 240 | assert.equal(failures.size, 1); 241 | assert.equal(failures.get(0).get('location'), 'info.description'); 242 | assert.equal(failures.get(0).get('hint'), 'Expected "Bad description with superSECRET word" to not match "supersecret"'); 243 | }); 244 | }); 245 | 246 | 247 | describe('title and description ref overrides all start with capital letters.', () => { 248 | const options = { 249 | applyTo: [ 250 | 'title-ref-override', 251 | 'description-ref-override' 252 | ], 253 | matchPattern: '^[A-Z]' 254 | }; 255 | 256 | it('should not report errors when valid', () => { 257 | const schema = { 258 | definitions: { 259 | Pet: {} 260 | }, 261 | info: { 262 | title: 'should ignore this title' 263 | }, 264 | paths: { 265 | '/pets': { 266 | get: { 267 | summary: 'should ignore this one too', 268 | parameters: [ 269 | { 270 | description: 'should ignore this one too' 271 | } 272 | ], 273 | responses: { 274 | 200: { 275 | schema: { 276 | title: 'Good title in override', 277 | description: 'Good description in override', 278 | $ref: '#/definitions/Pet' 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | }; 286 | 287 | const failures = textContentRule.validate(options, schema); 288 | 289 | assert.equal(failures.size, 0); 290 | }); 291 | 292 | it('should report only 2 errors when punctuation is incorrect', () => { 293 | const schema = { 294 | definitions: { 295 | Pet: {} 296 | }, 297 | info: { 298 | title: 'should ignore this title' 299 | }, 300 | paths: { 301 | '/pets': { 302 | get: { 303 | summary: 'should ignore this one too', 304 | parameters: [ 305 | { 306 | description: 'should ignore this one too' 307 | } 308 | ], 309 | responses: { 310 | 200: { 311 | schema: { 312 | title: 'bad title in override', 313 | description: 'bad description in override', 314 | $ref: '#/definitions/Pet' 315 | } 316 | } 317 | } 318 | } 319 | } 320 | } 321 | }; 322 | 323 | const failures = textContentRule.validate(options, schema); 324 | 325 | assert.equal(failures.size, 2); 326 | assert.equal(failures.get(0).get('location'), 'paths./pets.get.responses.200.schema.title#override'); 327 | assert.equal(failures.get(0).get('hint'), 'Expected "bad title in override" to match "^[A-Z]"'); 328 | assert.equal(failures.get(1).get('location'), 'paths./pets.get.responses.200.schema.description#override'); 329 | assert.equal(failures.get(1).get('hint'), 'Expected "bad description in override" to match "^[A-Z]"'); 330 | }); 331 | }); 332 | 333 | 334 | describe('no http: links', () => { 335 | const options = { 336 | applyTo: [ 337 | 'title' 338 | ], 339 | notMatchPattern: '\\(http:' 340 | }; 341 | 342 | it('should not report errors when valid', () => { 343 | const schema = { 344 | info: { 345 | title: 'This is a good [link](https://example.com).' 346 | } 347 | }; 348 | 349 | const failures = textContentRule.validate(options, schema); 350 | 351 | assert.equal(failures.size, 0); 352 | }); 353 | 354 | it('should report only 2 errors when punctuation is incorrect', () => { 355 | const schema = { 356 | info: { 357 | title: 'This is not a good [link](http://example.com).' 358 | } 359 | }; 360 | 361 | const failures = textContentRule.validate(options, schema); 362 | 363 | assert.equal(failures.size, 1); 364 | assert.equal(failures.get(0).get('location'), 'info.title'); 365 | assert.equal(failures.get(0).get('hint'), 'Expected "This is not a good [link](http://example.com)." to not match "\\(http:"'); 366 | }); 367 | }); 368 | }); 369 | --------------------------------------------------------------------------------