Skip to content

Commit

Permalink
build(tools): script to check OAS extensions #489
Browse files Browse the repository at this point in the history
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 <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed Aug 3, 2021
1 parent a340765 commit 792b8cf
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions tools/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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...
{
Expand Down
133 changes: 133 additions & 0 deletions tools/custom-checks/check-open-api-json-specs.ts
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;
}
18 changes: 18 additions & 0 deletions tools/custom-checks/has-property.ts
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;
}
5 changes: 5 additions & 0 deletions tools/custom-checks/is-std-lib-record.ts
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;
}
28 changes: 28 additions & 0 deletions tools/custom-checks/run-custom-checks.ts
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);
}

0 comments on commit 792b8cf

Please sign in to comment.