refactor: cli updates (#2907)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/cli",
|
"name": "@hoppscotch/cli",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||||
"homepage": "https://hoppscotch.io",
|
"homepage": "https://hoppscotch.io",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -5,42 +5,52 @@ import { execAsync, getErrorCode, getTestJsonFilePath } from "../utils";
|
|||||||
describe("Test 'hopp test <file>' command:", () => {
|
describe("Test 'hopp test <file>' command:", () => {
|
||||||
test("No collection file path provided.", async () => {
|
test("No collection file path provided.", async () => {
|
||||||
const cmd = `node ./bin/hopp test`;
|
const cmd = `node ./bin/hopp test`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Collection file not found.", async () => {
|
test("Collection file not found.", async () => {
|
||||||
const cmd = `node ./bin/hopp test notfound.json`;
|
const cmd = `node ./bin/hopp test notfound.json`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Malformed collection file.", async () => {
|
test("Collection file is invalid JSON.", async () => {
|
||||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||||
"malformed-collection.json"
|
"malformed-collection.json"
|
||||||
)}`;
|
)}`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
|
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Malformed collection file.", async () => {
|
||||||
|
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||||
|
"malformed-collection2.json"
|
||||||
|
)}`;
|
||||||
|
const { stderr } = await execAsync(cmd);
|
||||||
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invalid arguement.", async () => {
|
test("Invalid arguement.", async () => {
|
||||||
const cmd = `node ./bin/hopp invalid-arg`;
|
const cmd = `node ./bin/hopp invalid-arg`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Collection file not JSON type.", async () => {
|
test("Collection file not JSON type.", async () => {
|
||||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
|
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||||
});
|
});
|
||||||
@@ -70,24 +80,24 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
|
|||||||
|
|
||||||
test("No env file path provided.", async () => {
|
test("No env file path provided.", async () => {
|
||||||
const cmd = `${VALID_TEST_CMD} --env`;
|
const cmd = `${VALID_TEST_CMD} --env`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ENV file not JSON type.", async () => {
|
test("ENV file not JSON type.", async () => {
|
||||||
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
|
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ENV file not found.", async () => {
|
test("ENV file not found.", async () => {
|
||||||
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
|
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||||
});
|
});
|
||||||
@@ -109,17 +119,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
|||||||
|
|
||||||
test("No value passed to delay flag.", async () => {
|
test("No value passed to delay flag.", async () => {
|
||||||
const cmd = `${VALID_TEST_CMD} --delay`;
|
const cmd = `${VALID_TEST_CMD} --delay`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invalid value passed to delay flag.", async () => {
|
test("Invalid value passed to delay flag.", async () => {
|
||||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||||
const { stdout } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stderr);
|
||||||
|
console.log("invalid value thing", out)
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { HoppCLIError } from "../../../types/errors";
|
|
||||||
import { checkFile } from "../../../utils/checks";
|
|
||||||
|
|
||||||
import "@relmify/jest-fp-ts";
|
|
||||||
|
|
||||||
describe("checkFile", () => {
|
|
||||||
test("File doesn't exists.", () => {
|
|
||||||
return expect(
|
|
||||||
checkFile("./src/samples/this-file-not-exists.json")()
|
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
|
||||||
code: "FILE_NOT_FOUND",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("File not of JSON type.", () => {
|
|
||||||
return expect(
|
|
||||||
checkFile("./src/__tests__/samples/notjson.txt")()
|
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
|
||||||
code: "INVALID_FILE_TYPE",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Existing JSON file.", () => {
|
|
||||||
return expect(
|
|
||||||
checkFile("./src/__tests__/samples/passes.json")()
|
|
||||||
).resolves.toBeRight();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -50,7 +50,7 @@ describe("collectionsRunner", () => {
|
|||||||
|
|
||||||
test("Empty HoppCollection.", () => {
|
test("Empty HoppCollection.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })()
|
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })
|
||||||
).resolves.toStrictEqual([]);
|
).resolves.toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ describe("collectionsRunner", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
envs: SAMPLE_ENVS,
|
envs: SAMPLE_ENVS,
|
||||||
})()
|
})
|
||||||
).resolves.toMatchObject([]);
|
).resolves.toMatchObject([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ describe("collectionsRunner", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
envs: SAMPLE_ENVS,
|
envs: SAMPLE_ENVS,
|
||||||
})()
|
})
|
||||||
).resolves.toMatchObject([
|
).resolves.toMatchObject([
|
||||||
{
|
{
|
||||||
path: "collection/request",
|
path: "collection/request",
|
||||||
@@ -116,7 +116,7 @@ describe("collectionsRunner", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
envs: SAMPLE_ENVS,
|
envs: SAMPLE_ENVS,
|
||||||
})()
|
})
|
||||||
).resolves.toMatchObject([
|
).resolves.toMatchObject([
|
||||||
{
|
{
|
||||||
path: "collection/folder/request",
|
path: "collection/folder/request",
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { HoppCLIError } from "../../../types/errors";
|
import { HoppCLIError } from "../../../types/errors";
|
||||||
import { parseCollectionData } from "../../../utils/mutators";
|
import { parseCollectionData } from "../../../utils/mutators";
|
||||||
|
|
||||||
import "@relmify/jest-fp-ts";
|
|
||||||
|
|
||||||
describe("parseCollectionData", () => {
|
describe("parseCollectionData", () => {
|
||||||
test("Reading non-existing file.", () => {
|
test("Reading non-existing file.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
parseCollectionData("./src/__tests__/samples/notexist.json")()
|
parseCollectionData("./src/__tests__/samples/notexist.json")
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
).rejects.toMatchObject(<HoppCLIError>{
|
||||||
code: "FILE_NOT_FOUND",
|
code: "FILE_NOT_FOUND",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Unparseable JSON contents.", () => {
|
test("Unparseable JSON contents.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
parseCollectionData("./src/__tests__/samples/malformed-collection.json")()
|
parseCollectionData("./src/__tests__/samples/malformed-collection.json")
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
).rejects.toMatchObject(<HoppCLIError>{
|
||||||
code: "MALFORMED_COLLECTION",
|
code: "UNKNOWN_ERROR",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,15 +22,15 @@ describe("parseCollectionData", () => {
|
|||||||
return expect(
|
return expect(
|
||||||
parseCollectionData(
|
parseCollectionData(
|
||||||
"./src/__tests__/samples/malformed-collection2.json"
|
"./src/__tests__/samples/malformed-collection2.json"
|
||||||
)()
|
)
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
).rejects.toMatchObject(<HoppCLIError>{
|
||||||
code: "MALFORMED_COLLECTION",
|
code: "MALFORMED_COLLECTION",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Valid HoppCollection.", () => {
|
test("Valid HoppCollection.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
parseCollectionData("./src/__tests__/samples/passes.json")()
|
parseCollectionData("./src/__tests__/samples/passes.json")
|
||||||
).resolves.toBeRight();
|
).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as TE from "fp-ts/TaskEither";
|
|
||||||
import { pipe, flow } from "fp-ts/function";
|
|
||||||
import {
|
import {
|
||||||
collectionsRunner,
|
collectionsRunner,
|
||||||
collectionsRunnerExit,
|
collectionsRunnerExit,
|
||||||
@@ -10,18 +8,23 @@ import { parseCollectionData } from "../utils/mutators";
|
|||||||
import { parseEnvsData } from "../options/test/env";
|
import { parseEnvsData } from "../options/test/env";
|
||||||
import { TestCmdOptions } from "../types/commands";
|
import { TestCmdOptions } from "../types/commands";
|
||||||
import { parseDelayOption } from "../options/test/delay";
|
import { parseDelayOption } from "../options/test/delay";
|
||||||
|
import { HoppEnvs } from "../types/request";
|
||||||
|
import { isHoppCLIError } from "../utils/checks";
|
||||||
|
|
||||||
export const test = (path: string, options: TestCmdOptions) => async () => {
|
export const test = (path: string, options: TestCmdOptions) => async () => {
|
||||||
await pipe(
|
try {
|
||||||
TE.Do,
|
const delay = options.delay ? parseDelayOption(options.delay) : 0
|
||||||
TE.bind("envs", () => parseEnvsData(options.env)),
|
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
|
||||||
TE.bind("collections", () => parseCollectionData(path)),
|
const collections = await parseCollectionData(path)
|
||||||
TE.bind("delay", () => parseDelayOption(options.delay)),
|
|
||||||
TE.chainTaskK(collectionsRunner),
|
const report = await collectionsRunner({collections, envs, delay})
|
||||||
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
|
const hasSucceeded = collectionsRunnerResult(report)
|
||||||
TE.mapLeft((e) => {
|
collectionsRunnerExit(hasSucceeded)
|
||||||
handleError(e);
|
} catch(e) {
|
||||||
|
if(isHoppCLIError(e)) {
|
||||||
|
handleError(e)
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
})
|
}
|
||||||
)();
|
else throw e
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { log } from "console";
|
|
||||||
import * as S from "fp-ts/string";
|
import * as S from "fp-ts/string";
|
||||||
import { HoppError, HoppErrorCode } from "../types/errors";
|
import { HoppError, HoppErrorCode } from "../types/errors";
|
||||||
import { hasProperty, isSafeCommanderError } from "../utils/checks";
|
import { hasProperty, isSafeCommanderError } from "../utils/checks";
|
||||||
@@ -7,7 +6,7 @@ import { exceptionColors } from "../utils/getters";
|
|||||||
const { BG_FAIL } = exceptionColors;
|
const { BG_FAIL } = exceptionColors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses unknown error data and narrows it to get information realted to
|
* Parses unknown error data and narrows it to get information related to
|
||||||
* error in string format.
|
* error in string format.
|
||||||
* @param e Error data to parse.
|
* @param e Error data to parse.
|
||||||
* @returns Information in string format appropriately parsed, based on error type.
|
* @returns Information in string format appropriately parsed, based on error type.
|
||||||
@@ -81,6 +80,6 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!S.isEmpty(ERROR_MSG)) {
|
if (!S.isEmpty(ERROR_MSG)) {
|
||||||
log(ERROR_CODE, ERROR_MSG);
|
console.error(ERROR_CODE, ERROR_MSG);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
import * as TE from "fp-ts/TaskEither";
|
import { error } from "../../types/errors";
|
||||||
import * as S from "fp-ts/string";
|
|
||||||
import { pipe } from "fp-ts/function";
|
|
||||||
import { error, HoppCLIError } from "../../types/errors";
|
|
||||||
|
|
||||||
export const parseDelayOption = (
|
export function parseDelayOption(delay: string): number {
|
||||||
delay: unknown
|
const maybeInt = Number.parseInt(delay)
|
||||||
): TE.TaskEither<HoppCLIError, number> =>
|
|
||||||
!S.isString(delay)
|
if(!Number.isNaN(maybeInt)) {
|
||||||
? TE.right(0)
|
return maybeInt
|
||||||
: pipe(
|
} else {
|
||||||
delay,
|
throw error({
|
||||||
Number,
|
code: "INVALID_ARGUMENT",
|
||||||
TE.fromPredicate(Number.isSafeInteger, () =>
|
data: "Expected '-d, --delay' value to be number",
|
||||||
error({
|
})
|
||||||
code: "INVALID_ARGUMENT",
|
}
|
||||||
data: "Expected '-d, --delay' value to be number",
|
}
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,64 +1,27 @@
|
|||||||
import fs from "fs/promises";
|
import { error } from "../../types/errors";
|
||||||
import { pipe } from "fp-ts/function";
|
|
||||||
import * as TE from "fp-ts/TaskEither";
|
|
||||||
import * as E from "fp-ts/Either";
|
|
||||||
import * as J from "fp-ts/Json";
|
|
||||||
import * as A from "fp-ts/Array";
|
|
||||||
import * as S from "fp-ts/string";
|
|
||||||
import isArray from "lodash/isArray";
|
|
||||||
import { HoppCLIError, error } from "../../types/errors";
|
|
||||||
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
||||||
import { checkFile } from "../../utils/checks";
|
import { readJsonFile } from "../../utils/mutators";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses env json file for given path and validates the parsed env json object.
|
* Parses env json file for given path and validates the parsed env json object.
|
||||||
* @param path Path of env.json file to be parsed.
|
* @param path Path of env.json file to be parsed.
|
||||||
* @returns For successful parsing we get HoppEnvs object.
|
* @returns For successful parsing we get HoppEnvs object.
|
||||||
*/
|
*/
|
||||||
export const parseEnvsData = (
|
export async function parseEnvsData(path: string) {
|
||||||
path: unknown
|
const contents = await readJsonFile(path)
|
||||||
): TE.TaskEither<HoppCLIError, HoppEnvs> =>
|
|
||||||
!S.isString(path)
|
|
||||||
? TE.right({ global: [], selected: [] })
|
|
||||||
: pipe(
|
|
||||||
// Checking if the env.json file exists or not.
|
|
||||||
checkFile(path),
|
|
||||||
|
|
||||||
// Trying to read given env json file path.
|
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) {
|
||||||
TE.chainW((checkedPath) =>
|
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
|
||||||
TE.tryCatch(
|
}
|
||||||
() => fs.readFile(checkedPath),
|
|
||||||
(reason) =>
|
|
||||||
error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
// Trying to JSON parse the read file data and mapping the entries to HoppEnvPairs.
|
const envPairs: Array<HoppEnvPair> = []
|
||||||
TE.chainEitherKW((data) =>
|
|
||||||
pipe(
|
for( const [key,value] of Object.entries(contents)) {
|
||||||
data.toString(),
|
if(typeof value !== "string") {
|
||||||
J.parse,
|
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
|
||||||
E.map((jsonData) =>
|
}
|
||||||
jsonData && typeof jsonData === "object" && !isArray(jsonData)
|
|
||||||
? pipe(
|
envPairs.push({key, value})
|
||||||
jsonData,
|
}
|
||||||
Object.entries,
|
return <HoppEnvs>{ global: [], selected: envPairs }
|
||||||
A.map(
|
}
|
||||||
([key, value]) =>
|
|
||||||
<HoppEnvPair>{
|
|
||||||
key,
|
|
||||||
value: S.isString(value)
|
|
||||||
? value
|
|
||||||
: JSON.stringify(value),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
E.map((envPairs) => <HoppEnvs>{ global: [], selected: envPairs }),
|
|
||||||
E.mapLeft((e) =>
|
|
||||||
error({ code: "MALFORMED_ENV_FILE", path, data: E.toError(e) })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export type TestCmdOptions = {
|
export type TestCmdOptions = {
|
||||||
env: string;
|
env: string | undefined;
|
||||||
delay: number;
|
delay: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HoppEnvFileExt = "json";
|
export type HOPP_ENV_FILE_EXT = "json";
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import fs from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
import { pipe } from "fp-ts/function";
|
|
||||||
import {
|
import {
|
||||||
HoppCollection,
|
HoppCollection,
|
||||||
HoppRESTRequest,
|
HoppRESTRequest,
|
||||||
isHoppRESTRequest,
|
isHoppRESTRequest,
|
||||||
} from "@hoppscotch/data";
|
} from "@hoppscotch/data";
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import * as S from "fp-ts/string";
|
|
||||||
import * as TE from "fp-ts/TaskEither";
|
|
||||||
import * as E from "fp-ts/Either";
|
|
||||||
import curryRight from "lodash/curryRight";
|
|
||||||
import { CommanderError } from "commander";
|
import { CommanderError } from "commander";
|
||||||
import { error, HoppCLIError, HoppErrnoException } from "../types/errors";
|
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||||
import { HoppCollectionFileExt } from "../types/collections";
|
|
||||||
import { HoppEnvFileExt } from "../types/commands";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether an object has a property with given name.
|
* Determines whether an object has a property with given name.
|
||||||
@@ -71,59 +62,6 @@ export const isRESTCollection = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the file path matches the requried file type with of required extension.
|
|
||||||
* @param path The input file path to check.
|
|
||||||
* @param extension The required extension for input file path.
|
|
||||||
* @returns Absolute path for valid file extension OR HoppCLIError in case of error.
|
|
||||||
*/
|
|
||||||
export const checkFileExt = curryRight(
|
|
||||||
(
|
|
||||||
path: unknown,
|
|
||||||
extension: HoppCollectionFileExt | HoppEnvFileExt
|
|
||||||
): E.Either<HoppCLIError, string> =>
|
|
||||||
pipe(
|
|
||||||
path,
|
|
||||||
E.fromPredicate(S.isString, (_) => error({ code: "NO_FILE_PATH" })),
|
|
||||||
E.chainW(
|
|
||||||
E.fromPredicate(S.endsWith(`.${extension}`), (_) =>
|
|
||||||
error({ code: "INVALID_FILE_TYPE", data: extension })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given file path exists and is of given type.
|
|
||||||
* @param path The input file path to check.
|
|
||||||
* @returns Absolute path for valid file path OR HoppCLIError in case of error.
|
|
||||||
*/
|
|
||||||
export const checkFile = (path: unknown): TE.TaskEither<HoppCLIError, string> =>
|
|
||||||
pipe(
|
|
||||||
path,
|
|
||||||
|
|
||||||
// Checking if path is string.
|
|
||||||
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After checking file path, we map file path to absolute path and check
|
|
||||||
* if file is of given extension type.
|
|
||||||
*/
|
|
||||||
TE.map(join),
|
|
||||||
TE.chainEitherK(checkFileExt("json")),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trying to access given file path.
|
|
||||||
* If successfully accessed, we return the path from predicate step.
|
|
||||||
* Else return HoppCLIError with code FILE_NOT_FOUND.
|
|
||||||
*/
|
|
||||||
TE.chainFirstW((checkedPath) =>
|
|
||||||
TE.tryCatchK(
|
|
||||||
() => fs.access(checkedPath),
|
|
||||||
() => error({ code: "FILE_NOT_FOUND", path: checkedPath })
|
|
||||||
)()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if given error data is of type HoppCLIError, based on existence
|
* Checks if given error data is of type HoppCLIError, based on existence
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as T from "fp-ts/Task";
|
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import { pipe } from "fp-ts/function";
|
import { pipe } from "fp-ts/function";
|
||||||
import { bold } from "chalk";
|
import { bold } from "chalk";
|
||||||
@@ -43,8 +42,8 @@ const { WARN, FAIL } = exceptionColors;
|
|||||||
* @returns List of report for each processed request.
|
* @returns List of report for each processed request.
|
||||||
*/
|
*/
|
||||||
export const collectionsRunner =
|
export const collectionsRunner =
|
||||||
(param: CollectionRunnerParam): T.Task<RequestReport[]> =>
|
async (param: CollectionRunnerParam): Promise<RequestReport[]> =>
|
||||||
async () => {
|
{
|
||||||
const envs: HoppEnvs = param.envs;
|
const envs: HoppEnvs = param.envs;
|
||||||
const delay = param.delay ?? 0;
|
const delay = param.delay ?? 0;
|
||||||
const requestsReport: RequestReport[] = [];
|
const requestsReport: RequestReport[] = [];
|
||||||
@@ -213,10 +212,10 @@ export const collectionsRunnerResult = (
|
|||||||
* Else, exit with code 1.
|
* Else, exit with code 1.
|
||||||
* @param result Boolean defining the collections-runner result.
|
* @param result Boolean defining the collections-runner result.
|
||||||
*/
|
*/
|
||||||
export const collectionsRunnerExit = (result: boolean) => {
|
export const collectionsRunnerExit = (result: boolean): never => {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const EXIT_MSG = FAIL(`\nExited with code 1`);
|
const EXIT_MSG = FAIL(`\nExited with code 1`);
|
||||||
process.stdout.write(EXIT_MSG);
|
process.stderr.write(EXIT_MSG);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import * as E from "fp-ts/Either";
|
|
||||||
import * as TE from "fp-ts/TaskEither";
|
|
||||||
import * as A from "fp-ts/Array";
|
|
||||||
import * as J from "fp-ts/Json";
|
|
||||||
import { pipe } from "fp-ts/function";
|
|
||||||
import { FormDataEntry } from "../types/request";
|
import { FormDataEntry } from "../types/request";
|
||||||
import { error, HoppCLIError } from "../types/errors";
|
import { error } from "../types/errors";
|
||||||
import { isRESTCollection, isHoppErrnoException, checkFile } from "./checks";
|
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,49 +34,44 @@ export const parseErrorMessage = (e: unknown) => {
|
|||||||
return msg.replace(/\n+$|\s{2,}/g, "").trim();
|
return msg.replace(/\n+$|\s{2,}/g, "").trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function readJsonFile(path: string): Promise<unknown> {
|
||||||
|
if(!path.endsWith('.json')) {
|
||||||
|
throw error({ code: "INVALID_FILE_TYPE", data: path })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(path)
|
||||||
|
} catch (e) {
|
||||||
|
throw error({ code: "FILE_NOT_FOUND", path: path })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse((await fs.readFile(path)).toString())
|
||||||
|
} catch(e) {
|
||||||
|
throw error({ code: "UNKNOWN_ERROR", data: e })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses collection json file for given path:context.path, and validates
|
* Parses collection json file for given path:context.path, and validates
|
||||||
* the parsed collectiona array.
|
* the parsed collectiona array.
|
||||||
* @param path Collection json file path.
|
* @param path Collection json file path.
|
||||||
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
|
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
|
||||||
*/
|
*/
|
||||||
export const parseCollectionData = (
|
export async function parseCollectionData(
|
||||||
path: string
|
path: string
|
||||||
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
|
): Promise<HoppCollection<HoppRESTRequest>[]> {
|
||||||
pipe(
|
let contents = await readJsonFile(path)
|
||||||
TE.of(path),
|
|
||||||
|
|
||||||
// Checking if given file path exists or not.
|
const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents]
|
||||||
TE.chain(checkFile),
|
|
||||||
|
|
||||||
// Trying to read give collection json path.
|
if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||||
TE.chainW((checkedPath) =>
|
throw error({
|
||||||
TE.tryCatch(
|
code: "MALFORMED_COLLECTION",
|
||||||
() => fs.readFile(checkedPath),
|
path,
|
||||||
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
data: "Please check the collection data.",
|
||||||
)
|
})
|
||||||
),
|
}
|
||||||
|
|
||||||
// Checking if parsed file data is array.
|
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
|
||||||
TE.chainEitherKW((data) =>
|
};
|
||||||
pipe(
|
|
||||||
data.toString(),
|
|
||||||
J.parse,
|
|
||||||
E.map((jsonData) => (Array.isArray(jsonData) ? jsonData : [jsonData])),
|
|
||||||
E.mapLeft((e) =>
|
|
||||||
error({ code: "MALFORMED_COLLECTION", path, data: E.toError(e) })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
// Validating collections to be HoppRESTCollection.
|
|
||||||
TE.chainW(
|
|
||||||
TE.fromPredicate(A.every(isRESTCollection), () =>
|
|
||||||
error({
|
|
||||||
code: "MALFORMED_COLLECTION",
|
|
||||||
path,
|
|
||||||
data: "Please check the collection data.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user