-
Notifications
You must be signed in to change notification settings - Fork 286
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
Showing
6 changed files
with
187 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 [email protected]", | ||
"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", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T extends PropertyKey>( | ||
anObject: unknown, | ||
propertyKey: T, | ||
): anObject is Record<T, unknown> { | ||
if (typeof anObject !== "object") { | ||
return false; | ||
} | ||
if (!anObject) { | ||
return false; | ||
} | ||
return propertyKey in anObject; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export function isStdLibRecord( | ||
value: unknown, | ||
): value is Record<string, unknown> { | ||
return typeof value === "object" && value !== null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { checkOpenApiJsonSpecs } from "./check-open-api-json-specs"; | ||
|
||
export async function runCustomChecks( | ||
argv: string[], | ||
env: NodeJS.ProcessEnv, | ||
): Promise<void> { | ||
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); | ||
} |