add arg parser
This commit is contained in:
parent
ad74ff9995
commit
5dbd168785
97
src/args.ts
97
src/args.ts
|
@ -1,9 +1,98 @@
|
||||||
|
import * as E from "fp-ts/lib/Either";
|
||||||
|
import * as O from "fp-ts/lib/Option";
|
||||||
|
import * as R from "fp-ts/lib/ReadonlyArray";
|
||||||
|
import { pipe } from "fp-ts/lib/function";
|
||||||
|
import { parseTestType, type TestType } from "./canary";
|
||||||
|
|
||||||
export interface Args {
|
export interface Args {
|
||||||
testFile: string;
|
readonly testsFile: string;
|
||||||
|
readonly testName: O.Option<string>;
|
||||||
|
readonly testType: O.Option<TestType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseArgs = (argv: string[]): Args => {
|
interface ArgsBuilder {
|
||||||
const useful = argv.slice(2); // skip bun path and script path
|
readonly testsFile: O.Option<string>;
|
||||||
|
readonly testName: O.Option<string>;
|
||||||
|
readonly testType: O.Option<string>;
|
||||||
|
}
|
||||||
|
|
||||||
const flags = useful.map((x, i) => x.startsWith("--") && i);
|
type ArgsBuilderField = (arg: string) => (builder: ArgsBuilder) => ArgsBuilder;
|
||||||
|
|
||||||
|
const createArgsBuilder = (): ArgsBuilder => ({
|
||||||
|
testsFile: O.none,
|
||||||
|
testName: O.none,
|
||||||
|
testType: O.none,
|
||||||
|
});
|
||||||
|
|
||||||
|
const withTestsFile: ArgsBuilderField = (testsFile) => (builder) => ({
|
||||||
|
...builder,
|
||||||
|
testsFile: O.some(testsFile),
|
||||||
|
});
|
||||||
|
|
||||||
|
const withTestName: ArgsBuilderField = (testName) => (builder) => ({
|
||||||
|
...builder,
|
||||||
|
testName: O.some(testName),
|
||||||
|
});
|
||||||
|
|
||||||
|
const withTestType: ArgsBuilderField = (testType) => (builder) => ({
|
||||||
|
...builder,
|
||||||
|
testType: O.some(testType),
|
||||||
|
});
|
||||||
|
|
||||||
|
const flagBuilders: Record<string, ArgsBuilderField> = {
|
||||||
|
"--tests-file": withTestsFile,
|
||||||
|
"--test-name": withTestName,
|
||||||
|
"--test-type": withTestType,
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildArgs = (builder: ArgsBuilder): E.Either<string, Args> => {
|
||||||
|
if (O.isNone(builder.testsFile)) {
|
||||||
|
return E.left("please specify a test file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const testType = pipe(builder.testType, O.flatMap(parseTestType));
|
||||||
|
if (O.isSome(builder.testType) && !O.isSome(testType)) {
|
||||||
|
return E.left("bad test type " + builder.testType.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right({
|
||||||
|
testsFile: builder.testsFile.value,
|
||||||
|
testName: builder.testName,
|
||||||
|
testType,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isArg = (arg: string) => arg.startsWith("--") && arg in flagBuilders;
|
||||||
|
|
||||||
|
export const parseArgs = (argv: string[]): E.Either<string, Args> => {
|
||||||
|
if (argv.length < 2) return E.left("bad argv");
|
||||||
|
|
||||||
|
const useful = argv.slice(2);
|
||||||
|
const argApplicators = pipe(
|
||||||
|
useful,
|
||||||
|
R.mapWithIndex((i, arg) => (isArg(arg) ? O.some({ arg, i }) : O.none)),
|
||||||
|
R.compact,
|
||||||
|
R.map(({ arg, i }) =>
|
||||||
|
pipe(
|
||||||
|
O.fromNullable(flagBuilders[arg]),
|
||||||
|
O.bindTo("argApplicator"),
|
||||||
|
O.bind("val", () =>
|
||||||
|
pipe(
|
||||||
|
O.fromNullable(useful.at(i + 1)),
|
||||||
|
O.flatMap((argValue) =>
|
||||||
|
isArg(argValue) ? O.none : O.some(argValue),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
O.map(({ val, argApplicator }) => argApplicator(val)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return pipe(
|
||||||
|
argApplicators,
|
||||||
|
R.compact,
|
||||||
|
R.reduce(createArgsBuilder(), (builder, applyArgTo) => applyArgTo(builder)),
|
||||||
|
buildArgs,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./job";
|
export * from "./job";
|
||||||
export * from "./testable";
|
export * from "./test";
|
||||||
|
export * from "./email";
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { TaskEither } from "fp-ts/lib/TaskEither";
|
import * as TE from "fp-ts/lib/TaskEither";
|
||||||
|
import * as O from "fp-ts/lib/Option";
|
||||||
import type { Job } from "./job";
|
import type { Job } from "./job";
|
||||||
import type { D } from "../util";
|
import type { D } from "../util";
|
||||||
|
|
||||||
|
@ -21,4 +22,9 @@ export interface Test {
|
||||||
schedule: Schedule;
|
schedule: Schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Testable<T extends Job> = (job: T) => TaskEither<Error, boolean>;
|
export type Testable<T extends Job> = (job: T) => TE.TaskEither<Error, boolean>;
|
||||||
|
|
||||||
|
export const parseTestType = (testType: string): O.Option<TestType> =>
|
||||||
|
Object.values(TestType).includes(testType as TestType)
|
||||||
|
? O.some(testType as TestType)
|
||||||
|
: O.none;
|
|
@ -1,11 +1,13 @@
|
||||||
import * as IO from "fp-ts/IO";
|
import * as IO from "fp-ts/IO";
|
||||||
import type { Publisher } from "./publisher";
|
import type { Publisher } from "./publisher";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
|
import type { Test } from "./canary";
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
result_publishers: Publisher[];
|
result_publishers: Publisher[];
|
||||||
dns: string[];
|
dns: string[];
|
||||||
timeout: string;
|
http_timeout: string;
|
||||||
|
tests: Test[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const readConfig =
|
export const readConfig =
|
||||||
|
|
18
src/index.ts
18
src/index.ts
|
@ -1,6 +1,16 @@
|
||||||
import * as IO from "fp-ts/IO";
|
import * as TE from "fp-ts/lib/TaskEither";
|
||||||
import { readFileSync } from "fs";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
|
import { toError } from "fp-ts/lib/Either";
|
||||||
const main: IO.IO<void> = ConsoleLogger.log("Hello, world!");
|
import { readConfig } from "./config";
|
||||||
|
import { parseArgs } from "./args";
|
||||||
|
|
||||||
|
const main: TE.TaskEither<Error, void> = pipe(
|
||||||
|
TE.fromEither(parseArgs(Bun.argv)),
|
||||||
|
TE.bindTo("args"),
|
||||||
|
TE.bind("config", ({ args }) => TE.fromIO(readConfig(args.testsFile))),
|
||||||
|
TE.fold(
|
||||||
|
(e) => TE.left(toError(e)),
|
||||||
|
() => TE.right(undefined),
|
||||||
|
),
|
||||||
|
);
|
||||||
main();
|
main();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { flow, pipe } from "fp-ts/function";
|
import { flow, pipe } from "fp-ts/function";
|
||||||
import * as E from "fp-ts/Either";
|
import * as E from "fp-ts/lib/Either";
|
||||||
import * as S from "fp-ts/String";
|
import * as S from "fp-ts/lib/string";
|
||||||
import * as O from "fp-ts/lib/Option";
|
import * as O from "fp-ts/lib/Option";
|
||||||
import * as R from "fp-ts/lib/ReadonlyArray";
|
import * as R from "fp-ts/lib/ReadonlyArray";
|
||||||
|
|
||||||
|
@ -60,30 +60,30 @@ export const createDurationBuilder = (): DurationBuilder => ({
|
||||||
hours: 0,
|
hours: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withMillis =
|
export type DurationBuilderField<T> = (
|
||||||
(millis: number) =>
|
arg: T,
|
||||||
(builder: DurationBuilder): DurationBuilder => ({
|
) => (builder: DurationBuilder) => DurationBuilder;
|
||||||
|
|
||||||
|
export const withMillis: DurationBuilderField<number> =
|
||||||
|
(millis) => (builder) => ({
|
||||||
...builder,
|
...builder,
|
||||||
millis,
|
millis,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withSeconds =
|
export const withSeconds: DurationBuilderField<number> =
|
||||||
(seconds: number) =>
|
(seconds) => (builder) => ({
|
||||||
(builder: DurationBuilder): DurationBuilder => ({
|
|
||||||
...builder,
|
...builder,
|
||||||
seconds,
|
seconds,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withMinutes =
|
export const withMinutes: DurationBuilderField<number> =
|
||||||
(minutes: number) =>
|
(minutes) => (builder) => ({
|
||||||
(builder: DurationBuilder): DurationBuilder => ({
|
|
||||||
...builder,
|
...builder,
|
||||||
minutes,
|
minutes,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withHours =
|
export const withHours: DurationBuilderField<number> =
|
||||||
(hours: number) =>
|
(hours) => (builder) => ({
|
||||||
(builder: DurationBuilder): DurationBuilder => ({
|
|
||||||
...builder,
|
...builder,
|
||||||
hours,
|
hours,
|
||||||
});
|
});
|
||||||
|
@ -99,7 +99,7 @@ export const parse = (duration: string): E.Either<string, Duration> => {
|
||||||
duration,
|
duration,
|
||||||
S.split(" "),
|
S.split(" "),
|
||||||
R.map(S.trim),
|
R.map(S.trim),
|
||||||
R.filter((part) => !S.isEmpty(part))
|
R.filter((part) => !S.isEmpty(part)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const valueUnitPairs = pipe(
|
const valueUnitPairs = pipe(
|
||||||
|
@ -120,9 +120,9 @@ export const parse = (duration: string): E.Either<string, Duration> => {
|
||||||
E.map(
|
E.map(
|
||||||
flow(
|
flow(
|
||||||
R.filter(O.isSome),
|
R.filter(O.isSome),
|
||||||
R.map(({ value }) => value)
|
R.map(({ value }) => value),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return pipe(
|
return pipe(
|
||||||
|
@ -146,10 +146,10 @@ export const parse = (duration: string): E.Either<string, Duration> => {
|
||||||
default:
|
default:
|
||||||
return E.left(`unknown unit: ${unit}`);
|
return E.left(`unknown unit: ${unit}`);
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
E.map(build)
|
E.map(build),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import * as E from "fp-ts/lib/Either";
|
||||||
|
import * as O from "fp-ts/lib/Option";
|
||||||
|
import { parseArgs, type Args } from "../src/args";
|
||||||
|
import { TestType } from "../src/canary";
|
||||||
|
|
||||||
|
test("should return an error if argv has less than 2 arguments", () => {
|
||||||
|
const result = parseArgs([]);
|
||||||
|
expect(E.isLeft(result)).toBe(true);
|
||||||
|
expect(E.left<string, Args>("bad argv")).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return an error if --tests-file is not specified", () => {
|
||||||
|
const result = parseArgs(["node", "script.js", "--test-name", "test"]);
|
||||||
|
expect(E.isLeft(result)).toBe(true);
|
||||||
|
expect(E.left<string, Args>("please specify a test file")).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return an error if test type is invalid", () => {
|
||||||
|
const result = parseArgs([
|
||||||
|
"bun",
|
||||||
|
"script.js",
|
||||||
|
"--tests-file",
|
||||||
|
"testFile",
|
||||||
|
"--test-type",
|
||||||
|
"invalidType",
|
||||||
|
]);
|
||||||
|
expect(E.isLeft(result)).toBe(true);
|
||||||
|
expect(E.left<string, Args>("bad test type invalidType")).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should parse arguments correctly with all options specified", () => {
|
||||||
|
const result = parseArgs([
|
||||||
|
"node",
|
||||||
|
"script.js",
|
||||||
|
"--tests-file",
|
||||||
|
"testFile",
|
||||||
|
"--test-name",
|
||||||
|
"testName",
|
||||||
|
"--test-type",
|
||||||
|
"dns",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const expected: Args = {
|
||||||
|
testsFile: "testFile",
|
||||||
|
testName: O.some("testName"),
|
||||||
|
testType: O.some(TestType.DNS),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(E.isRight(result)).toBe(true);
|
||||||
|
expect(E.right<string, Args>(expected)).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should parse arguments correctly with only testsFile specified", () => {
|
||||||
|
const result = parseArgs(["node", "script.js", "--tests-file", "testFile"]);
|
||||||
|
|
||||||
|
const expected: Args = {
|
||||||
|
testsFile: "testFile",
|
||||||
|
testName: O.none,
|
||||||
|
testType: O.none,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(E.isRight(result)).toBe(true);
|
||||||
|
expect(E.right<string, Args>(expected)).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle missing values for flags gracefully", () => {
|
||||||
|
const result = parseArgs([
|
||||||
|
"node",
|
||||||
|
"script.js",
|
||||||
|
"--tests-file",
|
||||||
|
"testFile",
|
||||||
|
"--test-name",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const expected: Args = {
|
||||||
|
testsFile: "testFile",
|
||||||
|
testName: O.none,
|
||||||
|
testType: O.none,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(E.isRight(result)).toBe(true);
|
||||||
|
expect(E.right<string, Args>(expected)).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not parse args as values", () => {
|
||||||
|
const result = parseArgs([
|
||||||
|
"node",
|
||||||
|
"script.js",
|
||||||
|
"--tests-file",
|
||||||
|
"testFile",
|
||||||
|
"--test-name",
|
||||||
|
"--tests-file",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const expected: Args = {
|
||||||
|
testsFile: "testFile",
|
||||||
|
testName: O.none,
|
||||||
|
testType: O.none,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(E.isRight(result)).toBe(true);
|
||||||
|
expect(E.right<string, Args>(expected)).toEqual(result);
|
||||||
|
});
|
Loading…
Reference in New Issue