Setup spotlight project
This commit is contained in:
162
.stoplight/custom-functions/oasPathParam.js
Normal file
162
.stoplight/custom-functions/oasPathParam.js
Normal file
@@ -0,0 +1,162 @@
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
const pathRegex = /(\{;?\??[a-zA-Z0-9_-]+\*?\})/g;
|
||||
|
||||
const isNamedPathParam = p => {
|
||||
return p.in !== void 0 && p.in === 'path' && p.name !== void 0;
|
||||
};
|
||||
|
||||
const isUnknownNamedPathParam = (p, path, results, seen) => {
|
||||
if (!isNamedPathParam(p)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p.required !== true) {
|
||||
results.push(generateResult(requiredMessage(p.name), path));
|
||||
}
|
||||
|
||||
if (p.name in seen) {
|
||||
results.push(generateResult(uniqueDefinitionMessage(p.name), path));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const ensureAllDefinedPathParamsAreUsedInPath = (path, params, expected, results) => {
|
||||
for (const p of Object.keys(params)) {
|
||||
if (!params[p]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!expected.includes(p)) {
|
||||
const resPath = params[p];
|
||||
results.push(generateResult(`Parameter "${p}" must be used in path "${path}".`, resPath));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ensureAllExpectedParamsInPathAreDefined = (path, params, expected, operationPath, results) => {
|
||||
for (const p of expected) {
|
||||
if (!(p in params)) {
|
||||
results.push(
|
||||
generateResult(`Operation must define parameter "{${p}}" as expected by path "${path}".`, operationPath),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const oasPathParam = targetVal => {
|
||||
/**
|
||||
* This rule verifies:
|
||||
*
|
||||
* 1. for every param referenced in the path string ie /users/{userId}, var must be defined in either
|
||||
* path.parameters, or operation.parameters object
|
||||
* 2. every path.parameters + operation.parameters property must be used in the path string
|
||||
*/
|
||||
|
||||
if (!isObject(targetVal) || !isObject(targetVal.paths)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
// keep track of normalized paths for verifying paths are unique
|
||||
const uniquePaths = {};
|
||||
const validOperationKeys = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];
|
||||
|
||||
for (const path of Object.keys(targetVal.paths)) {
|
||||
const pathValue = targetVal.paths[path];
|
||||
if (!isObject(pathValue)) continue;
|
||||
|
||||
// verify normalized paths are functionally unique (ie `/path/{one}` vs `/path/{two}` are
|
||||
// different but equivalent within the context of OAS)
|
||||
const normalized = path.replace(pathRegex, '%'); // '%' is used here since its invalid in paths
|
||||
if (normalized in uniquePaths) {
|
||||
results.push(
|
||||
generateResult(`Paths "${String(uniquePaths[normalized])}" and "${path}" must not be equivalent.`, [
|
||||
'paths',
|
||||
path,
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
uniquePaths[normalized] = path;
|
||||
}
|
||||
|
||||
// find all templated path parameters
|
||||
const pathElements = [];
|
||||
let match;
|
||||
|
||||
while ((match = pathRegex.exec(path))) {
|
||||
const p = match[0].replace(/[{}?*;]/g, '');
|
||||
if (pathElements.includes(p)) {
|
||||
results.push(generateResult(`Path "${path}" must not use parameter "{${p}}" multiple times.`, ['paths', path]));
|
||||
} else {
|
||||
pathElements.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
// find parameters set within the top-level 'parameters' object
|
||||
const topParams = {};
|
||||
if (Array.isArray(pathValue.parameters)) {
|
||||
for (const [i, value] of pathValue.parameters.entries()) {
|
||||
if (!isObject(value)) continue;
|
||||
|
||||
const fullParameterPath = ['paths', path, 'parameters', i];
|
||||
|
||||
if (isUnknownNamedPathParam(value, fullParameterPath, results, topParams)) {
|
||||
topParams[value.name] = fullParameterPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(targetVal.paths[path])) {
|
||||
// find parameters set within the operation's 'parameters' object
|
||||
for (const op of Object.keys(pathValue)) {
|
||||
const operationValue = pathValue[op];
|
||||
if (!isObject(operationValue)) continue;
|
||||
|
||||
if (op === 'parameters' || !validOperationKeys.includes(op)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const operationParams = {};
|
||||
const { parameters } = operationValue;
|
||||
const operationPath = ['paths', path, op];
|
||||
|
||||
if (Array.isArray(parameters)) {
|
||||
for (const [i, p] of parameters.entries()) {
|
||||
if (!isObject(p)) continue;
|
||||
|
||||
const fullParameterPath = [...operationPath, 'parameters', i];
|
||||
|
||||
if (isUnknownNamedPathParam(p, fullParameterPath, results, operationParams)) {
|
||||
operationParams[p.name] = fullParameterPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const definedParams = { ...topParams, ...operationParams };
|
||||
ensureAllDefinedPathParamsAreUsedInPath(path, definedParams, pathElements, results);
|
||||
ensureAllExpectedParamsInPathAreDefined(path, definedParams, pathElements, operationPath, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
function generateResult(message, path) {
|
||||
return {
|
||||
message,
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
const requiredMessage = name => `Path parameter "${name}" must have "required" property that is set to "true".`;
|
||||
|
||||
const uniqueDefinitionMessage = name => `Path parameter "${name}" must not be defined multiple times.`;
|
||||
|
||||
export default oasPathParam;
|
||||
Reference in New Issue
Block a user