Added Swagger
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
## Semantic Validators
|
||||
Ie: anything not covered by the json-schema validation
|
||||
|
||||
All the following belongs to the `validate` namespace
|
||||
Eg: a plugin with an action ( to validate things ) and a selector, to get nodes and stuff.
|
||||
```js
|
||||
export function SomeAwesomePlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
validate: { // "validate" Namespace
|
||||
actions: {
|
||||
validateSomeNewValidateFunction() {}
|
||||
},
|
||||
selectors: {
|
||||
someNewSelector() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Make a new one
|
||||
```js
|
||||
|
||||
// Under the validate namespace
|
||||
export const validateOnlyFoos = () => (system) => {
|
||||
system.validateSelectors.allSchemas().then(schemas => {
|
||||
const errors = []
|
||||
schemas.forEach( schema => {
|
||||
if(schema.node.type === "array") { // `node` is the value at that point
|
||||
errors.push({
|
||||
level: "error",
|
||||
message: "We can do something with this, array.",
|
||||
path: schema.path // it'll figure out the line # from this
|
||||
})
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Make a selector, to later validate
|
||||
We use a single traverser although its performance leaves a little to be desired.
|
||||
The idea is that you provide a name and a filter function and
|
||||
in turn your validators can then iterate over those "nodes" to validate them.
|
||||
|
||||
```js
|
||||
export const allParameters = () => (system) => {
|
||||
return system.fn.traverseOnce({ // Returns a promise
|
||||
name: "allParameterSchemas",
|
||||
fn: (node) => { // called for each node, you need to return the node if you want it in the collecction
|
||||
if(system.validateSelectors.isParameter(node)) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
import debounce from "lodash/debounce"
|
||||
|
||||
export const SOURCE = "semantic"
|
||||
|
||||
// the test system does not tolerate slowness!
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const CI = process.env.CI
|
||||
const DEBOUNCE_MS = (NODE_ENV === "test" || CI === "true") ? 0 : 30
|
||||
|
||||
// System for buffering/batching errors
|
||||
var errorCollector = []
|
||||
const debNewSpecErrBatch = debounce(() => {
|
||||
const system = errorCollector.system // Just a reference to the "latest" system
|
||||
try {
|
||||
errorCollector.forEach(obj => {
|
||||
obj.line = obj.line || system.fn.AST.getLineNumberForPath(system.specSelectors.specStr(), obj.path)
|
||||
obj.source = SOURCE
|
||||
})
|
||||
system.errActions.newSpecErrBatch(errorCollector)
|
||||
delete errorCollector.system
|
||||
errorCollector = [] // Clear stack
|
||||
} catch(e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
}
|
||||
}, DEBOUNCE_MS)
|
||||
|
||||
const bufferedNewSpecErrBatch = (system, obj) => {
|
||||
errorCollector.push(obj)
|
||||
errorCollector.system = system
|
||||
debNewSpecErrBatch()
|
||||
}
|
||||
|
||||
export const all = () => system => {
|
||||
if (!system.validateSelectors.shouldValidate()) {
|
||||
return
|
||||
}
|
||||
|
||||
system.validateActions.beforeValidate()
|
||||
|
||||
const errCb = (obj) => bufferedNewSpecErrBatch(system, obj)
|
||||
|
||||
system.validateSelectors.validators().forEach(name => {
|
||||
const fn = system.validateActions[name]
|
||||
// nothing about oas3 or swagger2
|
||||
if(name.indexOf("validateAsync") === 0) {
|
||||
fn(errCb) // Function send messages on its own, it won't be cached ( due to the nature of async operations )
|
||||
} else {
|
||||
Promise.resolve(fn())
|
||||
.then(validationObjs => {
|
||||
if(validationObjs) {
|
||||
validationObjs.forEach(errCb)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const beforeValidate = () => (system) => {
|
||||
system.errActions.clear({
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
const HARD_CYCLE_LIMIT = 200
|
||||
|
||||
export function getRootNode(node) {
|
||||
var i = 0
|
||||
while(node.notRoot && i < HARD_CYCLE_LIMIT) {
|
||||
node = node.parent
|
||||
i++
|
||||
}
|
||||
return node || {}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as selectors from "./selectors"
|
||||
import * as actions from "./actions"
|
||||
import traverse from "traverse"
|
||||
import {createSelector} from "reselect"
|
||||
import debounce from "lodash/debounce"
|
||||
import memoize from "lodash/memoize"
|
||||
|
||||
import * as formDataValidateActions from "./validators/form-data"
|
||||
import * as schemaValidateActions from "./validators/schema"
|
||||
import * as pathsValidateActions from "./validators/paths"
|
||||
import * as securityValidateActions from "./validators/security"
|
||||
import * as parametersValidateActions from "./validators/parameters"
|
||||
import * as operationsOAS3ValidateActions from "./validators/oas3/operations"
|
||||
import * as parametersOAS3ValidateActions from "./validators/oas3/parameters"
|
||||
import * as componentsOAS3ValidateActions from "./validators/oas3/components"
|
||||
import * as refsOAS3ValidateActions from "./validators/oas3/refs"
|
||||
import * as refs2and3ValidateActions from "./validators/2and3/refs"
|
||||
import * as parameters2and3ValidateActions from "./validators/2and3/parameters"
|
||||
import * as paths2and3ValidateActions from "./validators/2and3/paths"
|
||||
import * as schemas2and3ValidateActions from "./validators/2and3/schemas"
|
||||
import * as operations2and3ValidateActions from "./validators/2and3/operations"
|
||||
import * as security2and3ValidateActions from "./validators/2and3/security"
|
||||
import * as tags2and3ValidateActions from "./validators/2and3/tags"
|
||||
|
||||
export default function SemanticValidatorsPlugin({getSystem}) {
|
||||
|
||||
const debAll = debounce((system) => system.validateActions.all(), 300)
|
||||
const traverseOnce = makeTraverseOnce(getSystem)
|
||||
|
||||
return {
|
||||
fn: {
|
||||
traverse,
|
||||
traverseOnce,
|
||||
memoizedResolveSubtree: makeMemoizedResolveSubtree(getSystem())
|
||||
},
|
||||
statePlugins: {
|
||||
spec: {
|
||||
selectors: {
|
||||
jsonAsJS: createSelector(
|
||||
state => state.get("json"),
|
||||
(spec) => spec ? spec.toJS() : null
|
||||
)
|
||||
},
|
||||
wrapActions: {
|
||||
validateSpec: (ori, system) => (...args) => {
|
||||
// verify editor plugin already loaded and function is available (for tests)
|
||||
if (system.specSelectors.specOrigin) {
|
||||
const specOrigin = system.specSelectors.specOrigin()
|
||||
if (specOrigin === "editor") {
|
||||
ori(...args)
|
||||
debAll(system)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
validate: {
|
||||
selectors,
|
||||
actions: {
|
||||
...actions,
|
||||
...formDataValidateActions,
|
||||
...schemaValidateActions,
|
||||
...pathsValidateActions,
|
||||
...securityValidateActions,
|
||||
...parametersValidateActions,
|
||||
...operations2and3ValidateActions,
|
||||
...refs2and3ValidateActions,
|
||||
...operationsOAS3ValidateActions,
|
||||
...parametersOAS3ValidateActions,
|
||||
...componentsOAS3ValidateActions,
|
||||
...refsOAS3ValidateActions,
|
||||
...parameters2and3ValidateActions,
|
||||
...paths2and3ValidateActions,
|
||||
...schemas2and3ValidateActions,
|
||||
...security2and3ValidateActions,
|
||||
...tags2and3ValidateActions
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeTraverseOnce(getSystem) {
|
||||
let traversers = {}
|
||||
let results = {}
|
||||
let deferred = null
|
||||
|
||||
const debTraverse = debounce(() => {
|
||||
// Setup collections
|
||||
for(let name in traversers) {
|
||||
results[name] = []
|
||||
}
|
||||
|
||||
const system = getSystem()
|
||||
|
||||
const json = system.specSelectors.jsonAsJS()
|
||||
|
||||
getSystem().fn.traverse(json)
|
||||
.forEach(function() { // Remember: this cannot be a fat-arrow function, because we need to read "this"
|
||||
for(let name in traversers) {
|
||||
const fn = traversers[name]
|
||||
const fnRes = fn(this)
|
||||
if(fnRes) {
|
||||
results[name].push(fnRes)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
deferred.resolve(results)
|
||||
deferred = null
|
||||
|
||||
traversers = {}
|
||||
results = {}
|
||||
}, 20) // 20ms might be more than enough, since most of these are called immediately (within a tick)
|
||||
|
||||
const defer = () => {
|
||||
let d = {}
|
||||
d.promise = new Promise((resolve, reject) => {
|
||||
d.resolve = resolve
|
||||
d.reject = reject
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
return ({fn, name}) => {
|
||||
traversers[name] = fn
|
||||
deferred = deferred || defer()
|
||||
debTraverse()
|
||||
return deferred.promise.then( a => a[name] )
|
||||
}
|
||||
}
|
||||
|
||||
function makeMemoizedResolveSubtree(system) {
|
||||
const cacheKeymaker = (obj, path) => {
|
||||
return `${obj.toString()} ${path.join("<>")}`
|
||||
}
|
||||
return memoize(async (obj, path, opts) => {
|
||||
const res = await system.fn.resolveSubtree(obj.toJS(), path, opts)
|
||||
return res
|
||||
}, cacheKeymaker)
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
import flatten from "lodash/flatten"
|
||||
|
||||
export const isVendorExt = (state,node) => node.path.some(a => a.indexOf("x-") === 0)
|
||||
export const isDefinition = (state,node) => node.path[0] == "definitions" && node.path.length == 2
|
||||
export const isTag = (state, node) => node.path[0] === "tags" && node.path.length === 2
|
||||
export const isRootParameter = (state, node) => node.path[0] === "parameters" && node.path.length === 2
|
||||
export const isPathItemParameter = (state, node) => node.path[2] === "parameters" && node.path.length === 4
|
||||
export const isRootParameters = (state, node) => node.path[0] === "parameters" && node.path.length === 1
|
||||
export const isPathItemParameters = (state, node) => node.path[2] === "parameters" && node.path.length === 3
|
||||
export const isOperationParameters = (state, node) => node.path[3] === "parameters" && node.path.length === 4
|
||||
export const isRootResponse = (state, node) => node.path[0] === "responses" && node.path.length === 2
|
||||
export const isRootHeader = (state, node) => node.path[0] === "headers" && node.path.length === 2
|
||||
export const isRef = (state, node) => node.key === "$ref" && typeof node.node === "string" // This selector can be fooled.
|
||||
export const isRefArtifact = (state, node) => node.key === "$$ref" && typeof node.node === "string"
|
||||
export const isOAS3RootRequestBody = (state, node) => node.path.length === 3 && node.path[1] === "requestBodies"
|
||||
export const isOAS3OperationRequestBody = (state, node) => node.path.length === 4 && node.path[3] === "requestBody"
|
||||
export const isOAS3OperationCallbackRequestBody = (state, node) => node.path.length === 8 && node.path[7] === "requestBody"
|
||||
export const isOAS3RootParameter = (state, node) => node.path[0] === "components" && node.path[1] === "parameters" && node.path.length === 3
|
||||
export const isOAS3RootResponse = (state, node) => node.path[0] === "components" && node.path[1] === "responses" && node.path.length === 3
|
||||
export const isOAS3RootSchema = (state, node) => node.path[0] === "components" && node.path[1] === "schemas" && node.path.length === 3
|
||||
|
||||
export const isSubSchema = (state, node) => (sys) => {
|
||||
const path = node.path
|
||||
if(path.length < 3) {
|
||||
return false
|
||||
}
|
||||
if(node.parent.key == "properties") {
|
||||
if(node.parent.parent && node.parent.parent.node && node.parent.parent.node.type === "object") {
|
||||
return !sys.validateSelectors.isVendorExt(node)
|
||||
}
|
||||
} else if(node.key === "additionalProperties") {
|
||||
if(node.parent && node.parent.node && node.parent.node.type === "object") {
|
||||
return !sys.validateSelectors.isVendorExt(node)
|
||||
}
|
||||
} else if(node.key == "items") {
|
||||
if(node.parent.node && node.parent.node.type === "array") {
|
||||
return !sys.validateSelectors.isVendorExt(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isParameter = (state, node) => (sys) => {
|
||||
if(sys.validateSelectors.isVendorExt(node)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
sys.validateSelectors.isRootParameter(node)
|
||||
|| sys.validateSelectors.isOAS3RootParameter(node)
|
||||
|| sys.validateSelectors.isPathItemParameter(node)
|
||||
|| (node.path[0] === "paths"
|
||||
&& node.path[3] === "parameters"
|
||||
&& node.path.length === 5)
|
||||
)
|
||||
}
|
||||
|
||||
export const isOAS3RequestBody = (state, node) => (sys) => {
|
||||
if(sys.validateSelectors.isVendorExt(node)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
sys.validateSelectors.isOAS3RootRequestBody(node)
|
||||
|| sys.validateSelectors.isOAS3OperationRequestBody(node)
|
||||
|| sys.validateSelectors.isOAS3OperationCallbackRequestBody(node)
|
||||
)
|
||||
}
|
||||
|
||||
export const isParameterSchema = (state, node) => (sys) => {
|
||||
if(sys.specSelectors.isOAS3 && sys.specSelectors.isOAS3()) {
|
||||
// OAS3
|
||||
return node.key === "schema" && sys.validateSelectors.isParameter(node.parent)
|
||||
}
|
||||
// parameter.x.in != body
|
||||
if(sys.validateSelectors.isParameter(node) && node.node.in !== "body") {
|
||||
return true
|
||||
}
|
||||
// parameter.x.in == body
|
||||
if(node.key === "schema" && node.parent && sys.validateSelectors.isParameter(node.parent) && node.parent.node.in === "body") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const isOAS3RequestBodySchema = (state, node) => () => {
|
||||
const [key,, gpKey, ggpKey] = node.path.slice().reverse()
|
||||
|
||||
return key === "schema"
|
||||
&& gpKey === "content"
|
||||
&& ggpKey === "requestBody"
|
||||
}
|
||||
|
||||
export const isOAS3ResponseSchema = (state, node) => () => {
|
||||
const [key,, gpKey,, gggpKey] = node.path.slice().reverse()
|
||||
|
||||
return key === "schema"
|
||||
&& gpKey === "content"
|
||||
&& gggpKey === "responses"
|
||||
}
|
||||
|
||||
export const isResponse = (state, node) => (sys) => {
|
||||
const isOperationResponse = (
|
||||
node.path[0] === "paths"
|
||||
&& node.path[3] === "responses"
|
||||
&& node.path.length === 5
|
||||
&& !sys.validateSelectors.isVendorExt(node)
|
||||
)
|
||||
|
||||
return (
|
||||
isOperationResponse
|
||||
|| sys.validateSelectors.isRootResponse(node)
|
||||
|| sys.validateSelectors.isOAS3RootResponse(node)
|
||||
)
|
||||
}
|
||||
|
||||
export const allResponses = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allResponses",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isResponse(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const isHeader = (state, node) => (sys) => {
|
||||
if(sys.validateSelectors.isVendorExt(node)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
sys.validateSelectors.isRootHeader(node)
|
||||
|| ( node.path[0] === "paths"
|
||||
&& node.path[3] === "responses"
|
||||
&& node.path[5] === "headers"
|
||||
&& node.path.length === 7)
|
||||
)
|
||||
}
|
||||
|
||||
export const isResponseSchema = (state, node) => (sys) => {
|
||||
// paths.<operation>.<method>.responses.XXX.schema
|
||||
// respones.<response>.schema
|
||||
if(node.key === "schema" && node.parent && sys.validateSelectors.isResponse(node.parent)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const allSchemas = () => (system) => {
|
||||
const { validateSelectors } = system
|
||||
|
||||
const selectors = [
|
||||
validateSelectors.allParameterSchemas(),
|
||||
validateSelectors.allResponseSchemas(),
|
||||
validateSelectors.allDefinitions(),
|
||||
validateSelectors.allHeaders(),
|
||||
validateSelectors.allSubSchemas(),
|
||||
validateSelectors.allOAS3OperationSchemas()
|
||||
]
|
||||
|
||||
return Promise.all(selectors)
|
||||
.then((schemasAr) => {
|
||||
return flatten(schemasAr)
|
||||
})
|
||||
}
|
||||
|
||||
export const allParameters = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allParameters",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isParameter(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3RequestBodies = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3RequestBodies",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isOAS3RequestBody(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allParameterArrays = () => (system) => {
|
||||
return system.validateSelectors.allParameters()
|
||||
.then(parameters => {
|
||||
return parameters.map(node => node.parent)
|
||||
.filter((node, i, arr) => {
|
||||
return Array.isArray(node.node) && arr.indexOf(node) === i
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const allTags = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allTags",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isTag(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allSubSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allSubSchemas",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isSubSchema(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const all$refs = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "all$refs",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isRef(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const all$refArtifacts = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "all$refArtifacts",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isRefArtifact(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allDefinitions = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allDefinitions",
|
||||
fn: (node) => {
|
||||
if(
|
||||
system.validateSelectors.isDefinition(node)
|
||||
|| system.validateSelectors.isOAS3RootSchema(node)
|
||||
) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allParameterSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allParameterSchemas",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isParameterSchema(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3OperationSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3OperationSchemas",
|
||||
fn: (node) => {
|
||||
if(
|
||||
system.validateSelectors.isOAS3RequestBodySchema(node)
|
||||
|| system.validateSelectors.isOAS3ResponseSchema(node)
|
||||
) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3RequestBodySchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3RequestBodySchemas",
|
||||
fn: (node) => {
|
||||
if(
|
||||
system.validateSelectors.isOAS3RequestBodySchema(node)
|
||||
) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allHeaders = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allHeader",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isHeader(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allResponseSchemas = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allResponseSchemas",
|
||||
fn: (node) => {
|
||||
if(system.validateSelectors.isResponseSchema(node)) {
|
||||
return node
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const allOperations = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOperations",
|
||||
fn: (node) => {
|
||||
const isOperation = (
|
||||
node.path[0] == "paths"
|
||||
&& node.path.length === 3
|
||||
&& !system.validateSelectors.isVendorExt(node)
|
||||
)
|
||||
|
||||
if(isOperation) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allPathItems = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allPathItems",
|
||||
fn: (node) => {
|
||||
const isPathItem = (
|
||||
node.path[0] == "paths"
|
||||
&& node.path.length === 2
|
||||
&& !system.validateSelectors.isVendorExt(node)
|
||||
)
|
||||
|
||||
if(isPathItem) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allSecurityDefinitions = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allSecurityDefinitions",
|
||||
fn: (node) => {
|
||||
const isSecurityDefinition = (
|
||||
node.path[0] == "securityDefinitions"
|
||||
&& node.path.length === 2
|
||||
)
|
||||
|
||||
const isOAS3SecurityScheme = (
|
||||
node.path[0] == "components"
|
||||
&& node.path[1] == "securitySchemes"
|
||||
&& node.path.length === 3
|
||||
)
|
||||
|
||||
if(isSecurityDefinition || isOAS3SecurityScheme) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allSecurityRequirements = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allSecurityRequirements",
|
||||
fn: (node) => {
|
||||
const isGlobalSecurityRequirement = (
|
||||
node.path[0] == "security"
|
||||
&& node.path.length === 2
|
||||
)
|
||||
|
||||
const isOperationSecurityRequirement = (
|
||||
node.path[0] == "paths"
|
||||
&& node.path[3] == "security"
|
||||
&& node.path.length === 5
|
||||
&& !system.validateSelectors.isVendorExt(node.parent) // ignore extension keys in path items
|
||||
&& !system.validateSelectors.isVendorExt(node.parent.parent.parent) // ignore extension keys in "paths"
|
||||
)
|
||||
|
||||
if(isGlobalSecurityRequirement || isOperationSecurityRequirement) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allOAS3Components = () => (system) => {
|
||||
return system.fn.traverseOnce({
|
||||
name: "allOAS3Components",
|
||||
fn: (node) => {
|
||||
const isComponent = (
|
||||
node.path[0] === "components"
|
||||
&& node.path.length === 3
|
||||
&& !system.validateSelectors.isVendorExt(node.parent)
|
||||
)
|
||||
|
||||
if(isComponent) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// List of validators to run...
|
||||
export const validators = () => (system) => {
|
||||
return Object.keys(system.validateActions)
|
||||
.filter(name => {
|
||||
// The action needs to start with the prefix "validate..."
|
||||
if(name.indexOf("validate") !== 0)
|
||||
return false
|
||||
|
||||
// This is for both types...
|
||||
if(name.startsWith("validate2And3"))
|
||||
return true
|
||||
|
||||
// Now for the exclusive validations...
|
||||
if(system.specSelectors.isOAS3())
|
||||
return name.startsWith("validateOAS3")
|
||||
|
||||
// Swagger2 only...
|
||||
return !name.startsWith("validateOAS3")
|
||||
|
||||
//TODO: This doesn't account for validateAsync with oas3 or swagger2...
|
||||
})
|
||||
}
|
||||
|
||||
// Should we validate at all?
|
||||
export const shouldValidate = () => (system) => {
|
||||
// don't run validation if spec is empty
|
||||
if(system.specSelectors.specStr().trim().length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't validate if ambiguous version...
|
||||
const { specSelectors: { isSwagger2=Function.prototype, isOAS3=Function.prototype } } = system
|
||||
|
||||
// Can't handle TWO versions!
|
||||
if(isSwagger2() && isOAS3())
|
||||
return false
|
||||
|
||||
// Can't handle no version!
|
||||
if(!isSwagger2() && !isOAS3())
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export const validate2And3OperationHasUniqueId = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOperations()
|
||||
.then(nodes => {
|
||||
const seen = []
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
|
||||
const id = value.operationId
|
||||
|
||||
if (id) {
|
||||
if (seen.indexOf(id) > -1) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Operations must have unique operationIds.",
|
||||
path: [...node.path, "operationId"]
|
||||
})
|
||||
}
|
||||
seen.push(id)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
export const validate2And3ParametersHaveUniqueNameAndInCombinations = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameterArrays()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const parameters = node.node || []
|
||||
|
||||
const seen = []
|
||||
|
||||
parameters.forEach((param, i) => {
|
||||
const { name: paramName, in: paramIn } = param
|
||||
|
||||
if(!paramName || !paramIn) {
|
||||
// name or in is missing, so we can't match the param to anything else
|
||||
return
|
||||
}
|
||||
const key = `${paramName}::${paramIn}`
|
||||
if(seen.indexOf(key) > -1) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Sibling parameters must have unique name + in values",
|
||||
path: [
|
||||
...node.path,
|
||||
(param.__i || i).toString()
|
||||
]
|
||||
})
|
||||
}
|
||||
seen.push(key)
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3PathParameterIsDefinedInPath = () => (system) => {
|
||||
const refArray = []
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const parameter = node.node || {}
|
||||
const path = node.path
|
||||
const isFromPath = path[0] === "paths" ? true : false
|
||||
const pathString = path[1]
|
||||
const paramName = parameter.name
|
||||
const paramInPath = `{${paramName}}`
|
||||
const ref = parameter.$ref
|
||||
const pathStringIncludesParamInPath = pathString && !pathString.toUpperCase().includes("" + paramInPath.toUpperCase())
|
||||
if (parameter.in === "path") {
|
||||
if (isFromPath && pathStringIncludesParamInPath) {
|
||||
acc.push({
|
||||
message: `Path parameter "${paramName}" must have the corresponding ${paramInPath} segment in the "${pathString}" path`,
|
||||
path: [...node.path, "name"],
|
||||
level: "error"
|
||||
})
|
||||
} else {
|
||||
const paramReference = refArray.find(({ referenceParamName }) => referenceParamName === node.key)
|
||||
if (paramReference && paramReference.pathString && !paramReference.pathString.toUpperCase().includes("" + paramInPath.toUpperCase())) {
|
||||
acc.push({
|
||||
message: `Path parameter "${paramName}" must have the corresponding ${paramInPath} segment in the "${paramReference.pathString}" path`,
|
||||
path: [...paramReference.node.path, "name"],
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (ref !== undefined) {
|
||||
const refStrings = ref.split("/")
|
||||
refArray.push({referenceParamName:refStrings[refStrings.length-1], pathString:pathString, node: node})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
checkForDefinition,
|
||||
PATH_TEMPLATES_REGEX
|
||||
} from "../helpers"
|
||||
|
||||
export const validate2And3PathParameterKeysDontContainQuestionMarks = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(node.key.indexOf("?") > -1) {
|
||||
acc.push({
|
||||
message: `Query strings in paths are not allowed.`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3PathParameterDeclarationHasMatchingDefiniton = () => async system => {
|
||||
const nodes = await system.validateSelectors.allPathItems()
|
||||
|
||||
return nodes.reduce(async (prev, node) => {
|
||||
const acc = await prev
|
||||
const pathTemplates = (node.key.match(PATH_TEMPLATES_REGEX) || [])
|
||||
.map(str => str.replace("{", "").replace("}", ""))
|
||||
if(pathTemplates.length) {
|
||||
for (let paramName of pathTemplates) {
|
||||
if(paramName.length === 0) {
|
||||
// don't validate empty param names... they're invalid anyway
|
||||
continue
|
||||
}
|
||||
const resolverResult = await system.fn.memoizedResolveSubtree(system.specSelectors.specJson(), node.path)
|
||||
const res = checkForDefinition(paramName, resolverResult.spec)
|
||||
if(res.inOperation && res.missingFromOperations.length) {
|
||||
const missingStr = res.missingFromOperations
|
||||
.map(str => `"${str}"`)
|
||||
.join(", ")
|
||||
|
||||
acc.push({
|
||||
message: `Declared path parameter "${paramName}" needs to be defined within every operation in the path (missing in ${missingStr}), or moved to the path-level parameters object`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
} else if(res.caseMatch) {
|
||||
acc.push({
|
||||
message: `Parameter names are case-sensitive. The parameter named "${res.paramCase}" does not match the case used in the path "${node.key}".`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
} else if(!res.found) {
|
||||
acc.push({
|
||||
message: `Declared path parameter "${paramName}" needs to be defined as a path parameter at either the path or operation level`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, Promise.resolve([]))
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import get from "lodash/get"
|
||||
import { escapeJsonPointerToken } from "../../../refs-util"
|
||||
import qs from "querystring-browser"
|
||||
import { pathFromPtr } from "json-refs"
|
||||
|
||||
export const validate2And3RefHasNoSiblings = () => system => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((nodes) => {
|
||||
const immSpecJson = system.specSelectors.specJson()
|
||||
const specJson = immSpecJson.toJS ? immSpecJson.toJS() : {}
|
||||
|
||||
return nodes.reduce((acc, node) => {
|
||||
const unresolvedValue = get(specJson, node.parent.path) || {}
|
||||
const unresolvedKeys = Object.keys(unresolvedValue) || []
|
||||
const isPathItem = node.parent.key === "paths" && node.path.length === 2
|
||||
|
||||
unresolvedKeys.forEach(k => {
|
||||
if(!isPathItem && k !== "$ref" && unresolvedKeys.indexOf("$ref") > -1) {
|
||||
acc.push({
|
||||
message: `Sibling values alongside $refs are ignored.\nTo add properties to a $ref, wrap the $ref into allOf, or move the extra properties into the referenced definition (if applicable).`,
|
||||
path: [...node.path.slice(0, -1), k],
|
||||
level: "warning"
|
||||
})
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
// Add warnings for unused definitions
|
||||
export const validate2And3UnusedDefinitions = () => (system) => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((nodes) => {
|
||||
const references = nodes.map(node => node.node)
|
||||
const errors = []
|
||||
const basePath = system.specSelectors.isOAS3() ?
|
||||
["components", "schemas"] :
|
||||
["definitions"]
|
||||
|
||||
system.specSelectors.definitions()
|
||||
.forEach((val, key) => {
|
||||
const escapedKey = escapeJsonPointerToken(key)
|
||||
if(references.indexOf(`#/${basePath.join("/")}/${escapedKey}`) < 0) {
|
||||
const path = [...basePath, key]
|
||||
errors.push({
|
||||
level: "warning",
|
||||
path,
|
||||
message: "Definition was declared but never used in document"
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3RefPathFormatting = () => (system) => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((refArtifacts) => {
|
||||
|
||||
const errors = []
|
||||
refArtifacts.forEach((node) => {
|
||||
const value = node.node
|
||||
if(typeof value === "string") {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [refUrl, refPath] = value.split("#")
|
||||
|
||||
if(refPath && refPath[0] !== "/") {
|
||||
errors.push({
|
||||
path: [...node.path.slice(0, -1), "$ref"],
|
||||
message: "$ref paths must begin with `#/`",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3RefPointersExist = () => (system) => {
|
||||
const json = system.specSelectors.specJson()
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((refs) => {
|
||||
const errors = []
|
||||
|
||||
refs.forEach((node) => {
|
||||
const value = node.node
|
||||
if(typeof value === "string" && value[0] === "#") {
|
||||
// if pointer starts with "#", it is a local ref
|
||||
const path = pathFromPtr(qs.unescape(value))
|
||||
|
||||
if(json.getIn(path) === undefined) {
|
||||
errors.push({
|
||||
path: [...node.path.slice(0, -1), "$ref"],
|
||||
message: "$refs must reference a valid location in the document",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
|
||||
// from RFC3986: https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
// plus "%", since it is needed for encoding.
|
||||
const RFC3986_UNRESERVED_CHARACTERS = /[A-Za-z0-9\-_\.~%]/g
|
||||
|
||||
export const validate2And3RefPointersAreProperlyEscaped = () => (system) => {
|
||||
return system.validateSelectors.all$refs()
|
||||
.then((refs) => {
|
||||
const errors = []
|
||||
|
||||
refs.forEach((node) => {
|
||||
const value = node.node
|
||||
const hashIndex = value.indexOf("#")
|
||||
const fragment = hashIndex > -1 ? value.slice(hashIndex + 1) : null
|
||||
if(typeof fragment === "string") {
|
||||
const rawPath = fragment.split("/")
|
||||
const hasReservedChars = rawPath
|
||||
.some(p => p.replace(RFC3986_UNRESERVED_CHARACTERS, "").length > 0)
|
||||
|
||||
if(hasReservedChars) {
|
||||
errors.push({
|
||||
path: [...node.path.slice(0, -1), "$ref"],
|
||||
message: "$ref values must be RFC3986-compliant percent-encoded URIs",
|
||||
level: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
export const validate2And3TypeArrayRequiresItems = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const { type, items } = schemaObj || {}
|
||||
if(type === "array" && typeof items === "undefined") {
|
||||
acc.push({
|
||||
message: "Schemas with 'type: array', require a sibling 'items: ' field",
|
||||
path: node.path,
|
||||
level: "error",
|
||||
})
|
||||
} else if(type === "array" && (typeof items !== "object" || Array.isArray(items))) {
|
||||
acc.push({
|
||||
message: "`items` must be an object",
|
||||
path: [...node.path, "items"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const validate2And3TypesInDefaultValuesMatchesWithEnum = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const { type } = schemaObj || {}
|
||||
const isNullable = !!schemaObj.nullable
|
||||
const enumeration = schemaObj.enum
|
||||
if (enumeration !== null && typeof enumeration !== "undefined") {
|
||||
var enumIndex = 0
|
||||
enumeration.forEach((element, index) => {
|
||||
var isValidFormat = true
|
||||
if (element === null && isNullable) {
|
||||
return
|
||||
}
|
||||
if (type === "array" && (!Array.isArray(element) || element === null)) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
} else if ((type === "number" || type === "string" || type === "boolean") && !(typeof element === type)) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
} else if (type === "integer" && !Number.isInteger(element)) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
} else if (type === "object" && ((element === null) || !(typeof element === type) || Array.isArray(element))) {
|
||||
isValidFormat = false
|
||||
enumIndex = index
|
||||
}
|
||||
if (!isValidFormat) {
|
||||
acc.push({
|
||||
message: "enum value should conform to its schema's `type`",
|
||||
path: [...node.path, "enum", enumIndex],
|
||||
level: "warning",
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3SchemasDefaultsMatchAnEnum = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const element = node.node || {}
|
||||
|
||||
if(!element || element.enum === undefined || element.default === undefined) {
|
||||
// nothing to do
|
||||
return acc
|
||||
}
|
||||
|
||||
if(element.enum.indexOf(element.default) === -1) {
|
||||
acc.push({
|
||||
message: "Default values must be present in `enum`",
|
||||
path: [...node.path, "default"]
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3MinAndMax = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const {minimum, maximum, minLength, maxLength, minProperties, maxProperties, minItems, maxItems} = schemaObj
|
||||
if(typeof minimum === "number" && typeof maximum === "number" && (minimum > maximum)) {
|
||||
acc.push({
|
||||
message: "'minimum' must be lower value than 'maximum'",
|
||||
path: [...node.path, "minimum"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
|
||||
if(typeof minLength === "number" && typeof maxLength === "number" && (minLength > maxLength)) {
|
||||
acc.push({
|
||||
message: "'minLength' must be lower value than 'maxLength'",
|
||||
path: [...node.path, "minLength"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
|
||||
if(typeof minProperties === "number" && typeof maxProperties === "number" && (minProperties > maxProperties)) {
|
||||
acc.push({
|
||||
message: "'minProperties' must be lower value than 'maxProperties'",
|
||||
path: [...node.path, "minProperties"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
|
||||
if(typeof minItems === "number" && typeof maxItems === "number" && (minItems > maxItems)) {
|
||||
acc.push({
|
||||
message: "'minItems' must be lower value than 'maxItems'",
|
||||
path: [...node.path, "minItems"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export const validate2And3SecurityRequirementsHaveDefinitions = () => (system) => {
|
||||
const { allSecurityRequirements, allSecurityDefinitions } = system.validateSelectors
|
||||
|
||||
return Promise.all([allSecurityRequirements(), allSecurityDefinitions()])
|
||||
.then(([requirementNodes, definitionNodes]) => {
|
||||
const definedSecuritySchemes = definitionNodes
|
||||
.map(node => node.key)
|
||||
|
||||
return requirementNodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const requiredSecurityDefinitions = Object.keys(value) || []
|
||||
|
||||
requiredSecurityDefinitions.forEach(name => {
|
||||
if(definedSecuritySchemes.indexOf(name) < 0) {
|
||||
acc.push({
|
||||
message: "Security requirements must match a security definition",
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validate2And3UnusedSecuritySchemes = () => (system) => {
|
||||
const { allSecurityRequirements, allSecurityDefinitions } = system.validateSelectors
|
||||
|
||||
return Promise.all([allSecurityRequirements(), allSecurityDefinitions()])
|
||||
.then(([securityRequirements, securitySchemes]) => {
|
||||
// Get just the names of security schemes used in `security`
|
||||
const usedSecurities = securityRequirements
|
||||
.map(node => Object.keys(node.node) || [])
|
||||
.reduce(function(a, b) {
|
||||
// flatten!
|
||||
return a.concat(b)
|
||||
}, [])
|
||||
|
||||
return securitySchemes.reduce((acc, node) => {
|
||||
if(usedSecurities.indexOf(node.key) < 0) {
|
||||
acc.push({
|
||||
message: "Security scheme was defined but never used. To apply security, use the `security` section in operations or on the root level of your API definition.",
|
||||
path: node.path,
|
||||
level: "warning",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export const validate2And3TagObjectsHaveUniqueNames = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allTags()
|
||||
.then(nodes => {
|
||||
const seenNames = []
|
||||
return nodes.reduce((acc, node) => {
|
||||
const tagObj = node.node
|
||||
const { name } = tagObj || {}
|
||||
if(!name || seenNames.indexOf(name) > -1) {
|
||||
acc.push({
|
||||
message: "Tag Objects must have unique `name` field values.",
|
||||
path: node.path,
|
||||
level: "error",
|
||||
})
|
||||
} else {
|
||||
seenNames.push(name)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { SOURCE } from "../actions"
|
||||
import { getRootNode } from "src/plugins/validate-semantic/helpers"
|
||||
const operationKeys = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
|
||||
|
||||
export const validateParameterFormDataCaseTypo = () => system => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
|
||||
if(
|
||||
value.in &&
|
||||
typeof value.in === "string" &&
|
||||
value.in.toLowerCase() === "formdata" &&
|
||||
value.in !== "formData"
|
||||
) {
|
||||
acc.push({
|
||||
message: `Parameter "in: ${value.in}" is invalid, did you mean "in: formData"?`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParameterFormDataForFileTypes = () => system => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
|
||||
if(value.type === "file" && value.in !== "formData") {
|
||||
acc.push({
|
||||
message: `Parameters with "type: file" must have "in: formData"`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParameterFormDataConsumesType = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const pathItemValue = node.node
|
||||
const globalConsumes = getRootNode(node).node.consumes
|
||||
const pathItemParameters = pathItemValue.parameters
|
||||
|
||||
const hasPathItemFormDataParameter = pathItemParameters != null && pathItemParameters.find(parameter => parameter.in === "formData")
|
||||
const hasPathItemFileParameter = pathItemParameters != null && pathItemParameters.find(parameter => parameter.type === "file")
|
||||
|
||||
for (const method of operationKeys) {
|
||||
const operationValue = pathItemValue[method]
|
||||
|
||||
if (operationValue) {
|
||||
const effectiveConsumes = operationValue.consumes || globalConsumes || []
|
||||
const operationParameters = operationValue.parameters || []
|
||||
const hasOperationFormDataParameter = operationParameters.find(parameter => parameter.in === "formData")
|
||||
const hasOperationFileParameter = operationParameters.find(parameter => parameter.type === "file")
|
||||
|
||||
if(hasPathItemFileParameter || hasOperationFileParameter){
|
||||
if (!effectiveConsumes.includes("multipart/form-data")) {
|
||||
acc.push({
|
||||
message: `Operations with parameters of "type: file" must include "multipart/form-data" in their "consumes" property`,
|
||||
path: [...node.path, method],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
} else if (hasPathItemFormDataParameter || hasOperationFormDataParameter) {
|
||||
if (!effectiveConsumes.includes("application/x-www-form-urlencoded") && !effectiveConsumes.includes("multipart/form-data")) {
|
||||
acc.push({
|
||||
message: `Operations with parameters of "in: formData" must include "application/x-www-form-urlencoded" or "multipart/form-data" in their "consumes" property`,
|
||||
path: [...node.path, method],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParameterArraysDontContainBodyAndFormData = () => system => {
|
||||
return system.validateSelectors
|
||||
.allParameterArrays()
|
||||
.then(paramArrays => {
|
||||
return paramArrays.reduce((acc, node) => {
|
||||
const bodyParams = node.node.filter(param => param.in === "body")
|
||||
const formDataParams = node.node.filter(param => param.in === "formData")
|
||||
|
||||
if(bodyParams.length && formDataParams.length) {
|
||||
acc.push({
|
||||
message: `Parameters cannot have both a "in: body" and "in: formData", as "formData" _will_ be the body`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
source: SOURCE
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const operationKeys = ["get", "post", "put", "delete", "options", "head", "patch", "trace"]
|
||||
|
||||
export const PATH_TEMPLATES_REGEX = /\{(.*?)\}/g
|
||||
|
||||
export function checkForDefinition(paramName, pathItem) {
|
||||
const pathItemParameters = pathItem.parameters
|
||||
const operationsInPathItem = (Object.keys(pathItem) || [])
|
||||
.filter(key => operationKeys.indexOf(key) > -1)
|
||||
.map(key => {
|
||||
const obj = pathItem[key]
|
||||
obj.method = key
|
||||
return obj
|
||||
})
|
||||
|
||||
const res = {
|
||||
found: false,
|
||||
inPath: false,
|
||||
inOperation: false,
|
||||
caseMatch: false,
|
||||
paramCase: "",
|
||||
missingFromOperations: []
|
||||
}
|
||||
|
||||
// Look at the path parameters
|
||||
if(Array.isArray(pathItemParameters)) {
|
||||
pathItemParameters.forEach(param => {
|
||||
if(param.name === paramName && param.in === "path") {
|
||||
res.found = true
|
||||
res.inPath = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Next, look at the operations...
|
||||
if(!res.found && operationsInPathItem.length) {
|
||||
operationsInPathItem
|
||||
.forEach(op => {
|
||||
const inThisOperation = (op.parameters || [])
|
||||
.some(param => param.name === paramName && param.in === "path")
|
||||
|
||||
const caseMatch = (op.parameters || [])
|
||||
.find(param => param.name && !(param.name === paramName) && (param.name.toLowerCase() === paramName.toLowerCase()) && param.in === "path")
|
||||
|
||||
if(inThisOperation) {
|
||||
res.found = true
|
||||
res.inOperation = true
|
||||
}
|
||||
|
||||
if(caseMatch) {
|
||||
res.caseMatch = true
|
||||
res.paramCase = caseMatch.name
|
||||
}
|
||||
|
||||
if(!inThisOperation) {
|
||||
res.missingFromOperations.push(op.method)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const COMPONENT_NAME_REGEX = /^[A-Za-z0-9\-\._]+$/
|
||||
|
||||
export const validateOAS3ComponentNames = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allOAS3Components()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(!COMPONENT_NAME_REGEX.test(node.key)) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Component names can only contain the characters A-Z a-z 0-9 - . _",
|
||||
path: node.path
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const validateOAS3GetAndDeleteOpsHaveNoRequestBody = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOperations()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const key = (node.key || "").toLowerCase()
|
||||
const value = node.node
|
||||
|
||||
if((key === "get" || key === "delete") && value.requestBody !== undefined) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `${key.toUpperCase()} operations cannot have a requestBody.`,
|
||||
path: [...node.path, "requestBody"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export const validateOAS3HeaderParameterNames = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(node.node.in === "header") {
|
||||
const name = (node.node.name || "").toLowerCase()
|
||||
if(name === "authorization") {
|
||||
acc.push({
|
||||
level: "warning",
|
||||
message: "Header parameters named \"Authorization\" are ignored. Use the `securitySchemes` and `security` sections instead to define authorization.",
|
||||
path: [...node.path, "name"]
|
||||
})
|
||||
} else if(name === "content-type") {
|
||||
acc.push({
|
||||
level: "warning",
|
||||
message: "Header parameters named \"Content-Type\" are ignored. The values for the \"Content-Type\" header are defined by `requestBody.content.<media-type>`.",
|
||||
path: [...node.path, "name"]
|
||||
})
|
||||
} else if(name === "accept") {
|
||||
acc.push({
|
||||
level: "warning",
|
||||
message: "Header parameters named \"Accept\" are ignored. The values for the \"Accept\" header are defined by `responses.<code>.content.<media-type>`.",
|
||||
path: [...node.path, "name"]
|
||||
})
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
export const validateOAS3RefsForRequestBodiesReferenceRequestBodyPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOAS3RequestBodies()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if (!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const [refPath = ""] = ref.split("#")
|
||||
const pathArr = refPath.split("/") || []
|
||||
|
||||
// Ignore external references
|
||||
if (ref.startsWith("#/")) {
|
||||
// Local cases
|
||||
if (refPath.endsWith("requestBody") && (refPath.startsWith("/paths") || refPath.startsWith("/components"))){
|
||||
return acc
|
||||
}
|
||||
|
||||
// Starting with #/compontents/schemas is not allowed
|
||||
if (ref.startsWith("#/components/schemas")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `requestBody $refs cannot point to '#/components/schemas/…', they must point to '#/components/requestBodies/…'`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
} else
|
||||
if (ref.startsWith("#/components") && !ref.startsWith("#/components/requestBodies/")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `requestBody $refs must point to a position where a requestBody can be legally placed`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
|
||||
// Extensions are valid
|
||||
if (ref.startsWith("#/") && pathArr.some(element => element.startsWith("x-"))){
|
||||
return acc
|
||||
}
|
||||
}
|
||||
return acc
|
||||
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateOAS3RequestBodyRefsReferenceAllowableSchemaPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allOAS3RequestBodySchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if(!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const [, refPath = ""] = ref.split("#")
|
||||
const pathArr = refPath.split("/") || []
|
||||
const parentRefKey = pathArr.slice(-2)[0]
|
||||
const targetRefKey = pathArr.slice(-1)[0]
|
||||
if(
|
||||
targetRefKey !== "schema"
|
||||
&& parentRefKey !== "schemas"
|
||||
&& ref.startsWith("#/")
|
||||
) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `requestBody schema $refs must point to a position where a Schema Object can be legally placed`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateOAS3ParameterRefsReferenceParameterPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if(!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
if (ref.startsWith("#/components/headers")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `OAS3 parameter $refs should point to #/components/parameters/... and not #/components/headers/...`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateOAS3RefsForHeadersReferenceHeadersPositions = () => sys => {
|
||||
return sys.validateSelectors
|
||||
.allHeaders()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const ref = value.$ref
|
||||
|
||||
if(!ref) {
|
||||
return acc
|
||||
}
|
||||
|
||||
if (ref.startsWith("#/components/parameters")) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: `OAS3 header $refs should point to #/components/headers/... and not #/components/parameters/...`,
|
||||
path: [...node.path, "$ref"]
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export const validateParameterBadKeys = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameters()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
if(node.node.required !== true && node.node.in === "path") {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Path parameters must have 'required: true'. You can always create another path/operation without this parameter to get the same behaviour.",
|
||||
path: node.path
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateParametersHasOnlyOneBody = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allParameterArrays()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const parameters = node.node || []
|
||||
let bodyParamSeen = false
|
||||
|
||||
parameters.forEach((param) => {
|
||||
if(param.in === "body" && bodyParamSeen) {
|
||||
acc.push({
|
||||
level: "error",
|
||||
message: "Multiple body parameters are not allowed.",
|
||||
path: node.path
|
||||
})
|
||||
}
|
||||
if(param.in === "body") {
|
||||
bodyParamSeen = true
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
PATH_TEMPLATES_REGEX
|
||||
} from "./helpers"
|
||||
|
||||
export const validatePathParameterDeclarationIsNotEmpty = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const pathTemplates = (node.key.match(PATH_TEMPLATES_REGEX) || [])
|
||||
.map(str => str.replace("{", "").replace("}", ""))
|
||||
|
||||
const emptyPathTemplates = pathTemplates.filter(v => !v.length)
|
||||
|
||||
if(emptyPathTemplates.length) {
|
||||
acc.push({
|
||||
message: `Empty path parameter declarations are not valid`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validatePathParameterKeysAreDifferent = () => system => {
|
||||
return system.validateSelectors
|
||||
.allPathItems()
|
||||
.then(nodes => {
|
||||
const seen = []
|
||||
return nodes.reduce((acc, node) => {
|
||||
const realPath = node.key.replace(PATH_TEMPLATES_REGEX, "~~")
|
||||
if(seen.indexOf(realPath) > -1) {
|
||||
acc.push({
|
||||
message: `Equivalent paths are not allowed.`,
|
||||
path: [...node.path],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
seen.push(realPath)
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
export const validateTypeKeyShouldBeString = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
|
||||
if(schemaObj.type !== undefined && typeof schemaObj.type !== "string") {
|
||||
acc.push({
|
||||
message: `Schema "type" key must be a string`,
|
||||
path: [...node.path, "type"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const validateReadOnlyPropertiesNotRequired = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
if(Array.isArray(schemaObj.required) && typeof schemaObj.properties === "object") {
|
||||
schemaObj.required.forEach((prop, i) => {
|
||||
if(schemaObj.properties[prop] && schemaObj.properties[prop].readOnly) {
|
||||
acc.push({
|
||||
message: `Read only properties cannot be marked as required by a schema.`,
|
||||
path: [...node.path, "required", i.toString()],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
// See https://github.com/swagger-api/swagger-editor/issues/1601
|
||||
export const validateSchemaPatternHasNoZAnchors = () => (system) => {
|
||||
return system.validateSelectors
|
||||
.allSchemas()
|
||||
.then(nodes => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const schemaObj = node.node
|
||||
const { pattern } = schemaObj || {}
|
||||
if(typeof pattern === "string" && pattern.indexOf("\\Z") > -1) {
|
||||
acc.push({
|
||||
message: `"\\Z" anchors are not allowed in regular expression patterns`,
|
||||
path: [...node.path, "pattern"],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export const validateSecurityRequirementReferenceExistingScopes = () => (system) => {
|
||||
const { allSecurityRequirements, allSecurityDefinitions } = system.validateSelectors
|
||||
|
||||
return Promise.all([allSecurityRequirements(), allSecurityDefinitions()])
|
||||
.then(([requirementNodes, definitionNodes]) => {
|
||||
const definedSecuritySchemes = definitionNodes
|
||||
.reduce((p, node) => Object.assign(p, { [node.key]: node.node }), {})
|
||||
|
||||
return requirementNodes.reduce((acc, node) => {
|
||||
const value = node.node
|
||||
const requiredSecurityDefinitions = Object.keys(value) || []
|
||||
|
||||
requiredSecurityDefinitions.forEach(name => {
|
||||
const scopes = value[name]
|
||||
const definition = definedSecuritySchemes[name]
|
||||
if(Array.isArray(scopes) && scopes.length && definition) {
|
||||
scopes.forEach((scope, i) => {
|
||||
if(!definition.scopes || definition.scopes[scope] === undefined) {
|
||||
acc.push({
|
||||
message: `Security scope definition ${scope} could not be resolved`,
|
||||
path: [...node.path, i.toString()],
|
||||
level: "error",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user