diff --git a/src/args.ts b/src/args.ts index 31fa2ee..d233c18 100644 --- a/src/args.ts +++ b/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 { - testFile: string; + readonly testsFile: string; + readonly testName: O.Option; + readonly testType: O.Option; } -export const parseArgs = (argv: string[]): Args => { - const useful = argv.slice(2); // skip bun path and script path +interface ArgsBuilder { + readonly testsFile: O.Option; + readonly testName: O.Option; + readonly testType: O.Option; +} - 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 = { + "--tests-file": withTestsFile, + "--test-name": withTestName, + "--test-type": withTestType, +}; + +const buildArgs = (builder: ArgsBuilder): E.Either => { + 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 => { + 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, + ); }; diff --git a/src/canary/index.ts b/src/canary/index.ts index 1d42dec..055a256 100644 --- a/src/canary/index.ts +++ b/src/canary/index.ts @@ -1,2 +1,3 @@ export * from "./job"; -export * from "./testable"; +export * from "./test"; +export * from "./email"; diff --git a/src/canary/testable.ts b/src/canary/test.ts similarity index 50% rename from src/canary/testable.ts rename to src/canary/test.ts index d79aa99..c03fb30 100644 --- a/src/canary/testable.ts +++ b/src/canary/test.ts @@ -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 { D } from "../util"; @@ -21,4 +22,9 @@ export interface Test { schedule: Schedule; } -export type Testable = (job: T) => TaskEither; +export type Testable = (job: T) => TE.TaskEither; + +export const parseTestType = (testType: string): O.Option => + Object.values(TestType).includes(testType as TestType) + ? O.some(testType as TestType) + : O.none; diff --git a/src/config.ts b/src/config.ts index 2da569f..d4527d9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,13 @@ import * as IO from "fp-ts/IO"; import type { Publisher } from "./publisher"; import { readFileSync } from "fs"; +import type { Test } from "./canary"; export interface Config { result_publishers: Publisher[]; dns: string[]; - timeout: string; + http_timeout: string; + tests: Test[]; } export const readConfig = diff --git a/src/index.ts b/src/index.ts index 95fd41f..2b3e95e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,16 @@ -import * as IO from "fp-ts/IO"; -import { readFileSync } from "fs"; - -const main: IO.IO = ConsoleLogger.log("Hello, world!"); +import * as TE from "fp-ts/lib/TaskEither"; +import { pipe } from "fp-ts/lib/function"; +import { toError } from "fp-ts/lib/Either"; +import { readConfig } from "./config"; +import { parseArgs } from "./args"; +const main: TE.TaskEither = 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(); diff --git a/src/util/duration.ts b/src/util/duration.ts index 9c10375..3d1a44c 100644 --- a/src/util/duration.ts +++ b/src/util/duration.ts @@ -1,6 +1,6 @@ import { flow, pipe } from "fp-ts/function"; -import * as E from "fp-ts/Either"; -import * as S from "fp-ts/String"; +import * as E from "fp-ts/lib/Either"; +import * as S from "fp-ts/lib/string"; import * as O from "fp-ts/lib/Option"; import * as R from "fp-ts/lib/ReadonlyArray"; @@ -60,30 +60,30 @@ export const createDurationBuilder = (): DurationBuilder => ({ hours: 0, }); -export const withMillis = - (millis: number) => - (builder: DurationBuilder): DurationBuilder => ({ +export type DurationBuilderField = ( + arg: T, +) => (builder: DurationBuilder) => DurationBuilder; + +export const withMillis: DurationBuilderField = + (millis) => (builder) => ({ ...builder, millis, }); -export const withSeconds = - (seconds: number) => - (builder: DurationBuilder): DurationBuilder => ({ +export const withSeconds: DurationBuilderField = + (seconds) => (builder) => ({ ...builder, seconds, }); -export const withMinutes = - (minutes: number) => - (builder: DurationBuilder): DurationBuilder => ({ +export const withMinutes: DurationBuilderField = + (minutes) => (builder) => ({ ...builder, minutes, }); -export const withHours = - (hours: number) => - (builder: DurationBuilder): DurationBuilder => ({ +export const withHours: DurationBuilderField = + (hours) => (builder) => ({ ...builder, hours, }); @@ -99,7 +99,7 @@ export const parse = (duration: string): E.Either => { duration, S.split(" "), R.map(S.trim), - R.filter((part) => !S.isEmpty(part)) + R.filter((part) => !S.isEmpty(part)), ); const valueUnitPairs = pipe( @@ -120,9 +120,9 @@ export const parse = (duration: string): E.Either => { E.map( flow( R.filter(O.isSome), - R.map(({ value }) => value) - ) - ) + R.map(({ value }) => value), + ), + ), ); return pipe( @@ -146,10 +146,10 @@ export const parse = (duration: string): E.Either => { default: return E.left(`unknown unit: ${unit}`); } - }) - ) - ) + }), + ), + ), ), - E.map(build) + E.map(build), ); }; diff --git a/tst/args.spec.ts b/tst/args.spec.ts new file mode 100644 index 0000000..2da0e83 --- /dev/null +++ b/tst/args.spec.ts @@ -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("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("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("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(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(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(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(expected)).toEqual(result); +}); diff --git a/tst/config.spec.ts b/tst/util/scheduler.spec.ts similarity index 100% rename from tst/config.spec.ts rename to tst/util/scheduler.spec.ts