diff --git a/Makefile b/Makefile index 5f317c2e..fa3e0c02 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,7 @@ install: npm install spectral: install - ./node_modules/.bin/spectral lint spec/json/twilio_*.json -Dq - + ./node_modules/.bin/spectral lint spec/json/twilio_*.json -Dq --verbose test: spectral test-docker: diff --git a/functions/operations.js b/functions/operations.js new file mode 100644 index 00000000..ab563220 --- /dev/null +++ b/functions/operations.js @@ -0,0 +1,62 @@ +import { containsPathInstance, isInstancePath, HTTP_OPS } from './utils'; + + +const instanceMap = { + "post": "Update", + "get": "Fetch", + "delete": "Delete" +}; + +const listMap = { + "get" : "List", + "post": "Create" +} + + +const urlOperation = (pathKey, pathObj, isInstance) => { + const results = [] + const opMap = isInstance ? instanceMap : listMap + for (const [key, value] of Object.entries(pathObj)) { + if (!HTTP_OPS.includes(key)) { + continue + } + if (opMap[key] && !value.operationId.startsWith(opMap[key])) { + results.push( + { + message: "Error operationId for path " + pathKey + " has invalid operationId of " + value.operationId, + } + ); + }; + }; + return results +} + +const pathTypeOperation = (pathKey, pathObj) => { + const results = [] + for (const [key, value] of Object.entries(pathObj)) { + if (!HTTP_OPS.includes(key)) { + continue + } + if (!value.operationId.startsWith(listMap[key]) || !value.operationId.startsWith(instanceMap[key])) { + const pathType = pathObj["x-twilio"]["pathType"] ; + results.push({ + message: "Error operationId for path " + pathKey + " of defined pathType " + pathType + " has invalid operationId of " + value.operationId, + }); + } + } + results; +} + +export default (ops) => { + const results = [] + for (const [pathKey, pathObj] of Object.entries(ops)) { + if (containsPathInstance(pathObj) ) { + results.concat(pathTypeOperation(pathKey, pathObj)); + } + else{ + results.concat(urlOperation(pathKey, pathObj, isInstancePath(pathKey))); + } + } + return results; +} + diff --git a/functions/post_operations.js b/functions/post_operations.js new file mode 100644 index 00000000..ade8ae3e --- /dev/null +++ b/functions/post_operations.js @@ -0,0 +1,16 @@ +import { isInstancePath } from './utils'; + +export default (ops) => { + const results = [] + for (const [pathKey, pathObj] of Object.entries(ops)) { + if (isInstancePath(pathKey)) { + if (pathObj["put"]) { + results.push( + { + message: "put operation not supported in instance path, please rename put to post ", + }); + } + } + } + return results +} \ No newline at end of file diff --git a/functions/recordkey.js b/functions/recordkey.js new file mode 100644 index 00000000..efea287c --- /dev/null +++ b/functions/recordkey.js @@ -0,0 +1,34 @@ +import { containsPathInstance, isInstancePath } from './utils'; + +const isRecordKey = (value, pathKey) => { + const results = []; + let flag = false; + for (const [_, prop ] of Object.entries(value["responses"]["200"]["content"]["application/json"]["schema"]["properties"])) { + if (prop["type"] == "array") { + flag = true; + } + } + if (!flag) { + results.push( + { + message: "Missing record key for pathkey " + pathKey, + } + ); + }; + return results; +} + + +export default (ops) => { + const results = [] + for (const [pathKey, pathObj] of Object.entries(ops)) { + if (!containsPathInstance(pathObj) && !isInstancePath(pathKey)) { + for (const [key, value] of Object.entries(pathObj)) { + if (key == "get") { + results.concat(isRecordKey(value, pathKey)); + } + } + }; + } + return results; +}; \ No newline at end of file diff --git a/functions/serverurl.js b/functions/serverurl.js new file mode 100644 index 00000000..aa7e20c3 --- /dev/null +++ b/functions/serverurl.js @@ -0,0 +1,21 @@ +module.exports = (ops) => { + const results = [] + const info = ops.info + if (info.servers) { + return results + } + let flag = false; + for (const [_, pathObj] of Object.entries(ops.paths)) { + if (!pathObj.servers) { + flag = true + } + } + if (flag == true) { + results.push( + { + message: "Either add serverurl globally or add it serverurl for all the paths", + } + ) + } + return results +} \ No newline at end of file diff --git a/functions/utils.js b/functions/utils.js new file mode 100644 index 00000000..39f28f4b --- /dev/null +++ b/functions/utils.js @@ -0,0 +1,18 @@ + + export const containsPathInstance = (pathObj) => { + if (pathObj["x-twilio"] && pathObj["x-twilio"]["pathType"] == "instance") { + return true; + } + return false; +} + +export const isInstancePath = (path) => { + const paths = path.split("/"); + const last = paths[paths.length - 1]; + if (last[last.length - 1] == '}' || last.slice(last.length - 6 , last.length) == '}.json' ) { + return true; + } + return false; +}; + +export const HTTP_OPS = ["get", "post", "delete", "put", "patch"]; \ No newline at end of file diff --git a/spectral.yaml b/spectral.yaml index 1a09a7dc..318d817a 100644 --- a/spectral.yaml +++ b/spectral.yaml @@ -1,7 +1,7 @@ extends: spectral:oas +functions: [recordkey, operations, serverurl, post_operations] rules: no-$ref-siblings: false - component-name-rule: severity: error recommended: true @@ -15,7 +15,7 @@ rules: pascal-case-name-rule: severity: error recommended: true - message: OperationId should be PascalCased. + message: OperationId should be pascalCased. type: style given: "$.paths.[*].operationId" then: @@ -52,3 +52,33 @@ rules: max: 50 min: 4 + list-operation-record-key: + message: "Response object does not have 2xx operation or default set" + given: "$.paths" + severity: error + then: + function: recordkey + + operation-id-convention: + message: "{{error}}" + given: "$.paths" + severity: error + then: + function: operations + + valid-server-url: + message: "{{error}}" + given: "$" + severity: error + then: + function: serverurl + + update-operation: + message: "{{error}}" + given: "$.paths" + severity: error + then: + function: post_operations + + oas3-valid-oas-content-example: off + oas3-valid-oas-parameter-example: off \ No newline at end of file