From 792b8cf44a4a14e57e68b3ea932568bc64a4a837 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Thu, 29 Jul 2021 21:51:53 -0700 Subject: [PATCH] build(tools): script to check OAS extensions #489 Ensures that openapi.json files "x-hyperledger-cactus" extensions do not contain common mistakes in them due to typos or just forgetting to specify certain mandatory properties. Fixes #489 Signed-off-by: Peter Somogyvari --- package.json | 1 + tools/ci.sh | 2 + .../check-open-api-json-specs.ts | 133 ++++++++++++++++++ tools/custom-checks/has-property.ts | 18 +++ tools/custom-checks/is-std-lib-record.ts | 5 + tools/custom-checks/run-custom-checks.ts | 28 ++++ 6 files changed, 187 insertions(+) create mode 100644 tools/custom-checks/check-open-api-json-specs.ts create mode 100644 tools/custom-checks/has-property.ts create mode 100644 tools/custom-checks/is-std-lib-record.ts create mode 100644 tools/custom-checks/run-custom-checks.ts diff --git a/package.json b/package.json index 34602acc5b..23bf5eced7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "run-ci": "./tools/ci.sh", "configure": "yarn install --frozen-lockfile --non-interactive --ignore-engines && yarn run build:dev:backend", "install-yarn": "npm install --global yarn@1.19.0", + "custom-checks": "ts-node ./tools/custom-checks/run-custom-checks.ts", "generate-api-server-config": "node ./tools/generate-api-server-config.js", "start:api-server": "node ./packages/cactus-cmd-api-server/dist/lib/main/typescript/cmd/cactus-api.js --config-file=.config.json", "start:example-supply-chain": "cd ./examples/supply-chain-app/ && npm i --no-package-lock && npm run start", diff --git a/tools/ci.sh b/tools/ci.sh index 3ef4379bd4..b6c572a1c9 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -95,6 +95,8 @@ function mainTask() yarn run configure + yarn run custom-checks + # Tests are still flaky (on weak hardware such as the CI env) despite our best # efforts so here comes the mighty hammer of brute force. 3 times the charm... { diff --git a/tools/custom-checks/check-open-api-json-specs.ts b/tools/custom-checks/check-open-api-json-specs.ts new file mode 100644 index 0000000000..d5384adb81 --- /dev/null +++ b/tools/custom-checks/check-open-api-json-specs.ts @@ -0,0 +1,133 @@ +import fs from "fs-extra"; +import path from "path"; +import globby from "globby"; +import { RuntimeError } from "run-time-error"; +import { hasProperty } from "./has-property"; +import { isStdLibRecord } from "./is-std-lib-record"; + +/** + * Verifies that the openapi.json files in the entire project are conformant to + * certain boilerplate requirements and conventions that are designed to reduce + * or completely eliminate certain types of bugs/mistakes that users/developers + * can make (and frequently do without these checks). + * + * @returns An array with the first item being a boolean indicating + * 1) success (`true`) or 2) failure (`false`) + */ +export async function checkOpenApiJsonSpecs( + req: ICheckOpenApiJsonSpecsRequest, +): Promise<[boolean, string[]]> { + const TAG = "[tools/check-open-api-json-specs.ts]"; + const SCRIPT_DIR = __dirname; + const PROJECT_DIR = path.join(SCRIPT_DIR, "../../"); + console.log(`${TAG} SCRIPT_DIR=${SCRIPT_DIR}`); + console.log(`${TAG} PROJECT_DIR=${PROJECT_DIR}`); + + if (!req) { + throw new RuntimeError(`req parameter cannot be falsy.`); + } + if (!req.argv) { + throw new RuntimeError(`req.argv cannot be falsy.`); + } + if (!req.env) { + throw new RuntimeError(`req.env cannot be falsy.`); + } + + const globbyOpts: globby.GlobbyOptions = { + cwd: PROJECT_DIR, + ignore: ["node_modules"], + }; + + const DEFAULT_GLOB = "**/cactus-*/src/main/json/openapi.json"; + + const oasPaths = await globby(DEFAULT_GLOB, globbyOpts); + console.log(`openapi.json paths: (${oasPaths.length}): `, oasPaths); + + const errors: string[] = []; + + const checks = oasPaths.map(async (oasPathRel) => { + const oasPathAbs = path.join(PROJECT_DIR, oasPathRel); + const oas: unknown = await fs.readJSON(oasPathAbs); + if (typeof oas !== "object") { + errors.push(`ERROR: ${oasPathRel} openapi.json cannot be empty.`); + return; + } + if (!oas) { + errors.push(`ERROR: ${oasPathRel} openapi.json cannot be empty.`); + return; + } + if (!isStdLibRecord(oas)) { + return; + } + if (!hasProperty(oas, "paths")) { + return; + } + + const { paths } = oas; + + if (!isStdLibRecord(paths)) { + errors.push(`ERROR: ${oasPathRel} "paths" must be an object`); + return; + } + + Object.entries(paths).forEach(([pathObjKey, pathObjProp]) => { + if (!isStdLibRecord(pathObjProp)) { + errors.push( + `ERROR: ${oasPathRel} "paths"."${pathObjKey}" must be an object`, + ); + return; + } + Object.entries(pathObjProp).forEach(([verbObjKey, verbObjProp]) => { + if (!isStdLibRecord(verbObjProp)) { + errors.push( + `ERROR: ${oasPathRel} "paths"."${pathObjKey}"."${verbObjKey}" must be an object`, + ); + return; + } + const oasExtension = verbObjProp["x-hyperledger-cactus"]; + if (!isStdLibRecord(oasExtension)) { + const errorMessage = `${oasPathRel} is missing "paths"."${pathObjKey}"."${verbObjKey}"."x-hyperledger-cactus" from the path definition of ${pathObjKey}. Please add it. If you do not know how to, search for existing examples in other openapi.json files within the project for the string "x-hyperledger-cactus"`; + errors.push(errorMessage); + return; + } + if (!hasProperty(oasExtension, "http")) { + const errorMessage = `${oasPathRel} is missing "paths"."${pathObjKey}"."${verbObjKey}"."x-hyperledger-cactus"."http" from the path definition of ${pathObjKey}. Please add it. If you do not know how to, search for existing examples in other openapi.json files within the project for the string "x-hyperledger-cactus"`; + errors.push(errorMessage); + return; + } + const { http } = oasExtension; + if (!hasProperty(http, "verbLowerCase")) { + const errorMessage = `${oasPathRel} is missing "paths"."${pathObjKey}"."${verbObjKey}"."x-hyperledger-cactus"."http"."verbLowerCase" from the path definition of ${pathObjKey}. Please add it. If you do not know how to, search for existing examples in other openapi.json files within the project for the string "x-hyperledger-cactus"`; + errors.push(errorMessage); + return; + } + if (!hasProperty(http, "path")) { + const errorMessage = `${oasPathRel} is missing "paths"."${pathObjKey}"."${verbObjKey}"."x-hyperledger-cactus"."http"."path" from the path definition object of ${pathObjKey}. Please add it. If you do not know how to, search for existing examples in other openapi.json files within the project for the string "x-hyperledger-cactus"`; + errors.push(errorMessage); + return; + } + if (http.path !== pathObjKey) { + const errorMessage = `${oasPathRel} HTTP paths at "paths"."${pathObjKey}"."${verbObjKey}"."x-hyperledger-cactus"."http"."path" must match "${pathObjKey}" but it is currently set to "${http.path}"`; + errors.push(errorMessage); + return; + } + if (http.verbLowerCase !== verbObjKey) { + const errorMessage = `${oasPathRel} HTTP verbs at "paths"."${pathObjKey}"."${verbObjKey}"."x-hyperledger-cactus"."http"."verbLowerCase" must match "${verbObjKey}" but it is currently set to "${http.verbLowerCase}"`; + errors.push(errorMessage); + return; + } + }); + }); + }); + + await Promise.all(checks); + + return [errors.length === 0, errors]; +} + +export const E_MISSING_OAS_EXTENSION = `missing "x-hyperledger-cactus" from `; + +export interface ICheckOpenApiJsonSpecsRequest { + readonly argv: string[]; + readonly env: NodeJS.ProcessEnv; +} diff --git a/tools/custom-checks/has-property.ts b/tools/custom-checks/has-property.ts new file mode 100644 index 0000000000..eacd751633 --- /dev/null +++ b/tools/custom-checks/has-property.ts @@ -0,0 +1,18 @@ +/** + * Determines if a given `propertyKey` is present within `anObject`. + * + * @param anObject The object to check the presence of `propertyKey` for. + * @param propertyKey The key whose presence will be checked. + */ +export function hasProperty( + anObject: unknown, + propertyKey: T, +): anObject is Record { + if (typeof anObject !== "object") { + return false; + } + if (!anObject) { + return false; + } + return propertyKey in anObject; +} diff --git a/tools/custom-checks/is-std-lib-record.ts b/tools/custom-checks/is-std-lib-record.ts new file mode 100644 index 0000000000..700987929f --- /dev/null +++ b/tools/custom-checks/is-std-lib-record.ts @@ -0,0 +1,5 @@ +export function isStdLibRecord( + value: unknown, +): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/tools/custom-checks/run-custom-checks.ts b/tools/custom-checks/run-custom-checks.ts new file mode 100644 index 0000000000..04598e146e --- /dev/null +++ b/tools/custom-checks/run-custom-checks.ts @@ -0,0 +1,28 @@ +import { checkOpenApiJsonSpecs } from "./check-open-api-json-specs"; + +export async function runCustomChecks( + argv: string[], + env: NodeJS.ProcessEnv, +): Promise { + const TAG = "[tools/custom-checks/check-source-code.ts]"; + let overallSuccess = true; + let overallErrors: string[] = []; + + { + const [success, errors] = await checkOpenApiJsonSpecs({ argv, env }); + overallErrors = overallErrors.concat(errors); + overallSuccess = overallSuccess && success; + } + + if (!overallSuccess) { + overallErrors.forEach((it) => console.error(it)); + } else { + console.log(`${TAG} All Checks Passed OK.`); + } + const exitCode = overallSuccess ? 0 : 100; + process.exit(exitCode); +} + +if (require.main === module) { + runCustomChecks(process.argv, process.env); +}