diff --git a/src/api.ts b/src/api.ts index 678f40d..6132882 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,3 @@ -import { parse } from "./duration"; import { perform } from "./email"; import type { EmailJob } from "./job"; import { ConsoleLogger } from "./logger"; @@ -11,16 +10,7 @@ export const main = (port: number) => { const url = new URL(req.url); if (req.method === "POST" && url.pathname === "/api/email") { - const prevalidatedJob = await req.json(); - const interval = parse(prevalidatedJob.readRetry.interval); - if (interval._tag === "Left") { - return new Response(interval.left, { status: 400 }); - } - prevalidatedJob.readRetry.interval = interval; - const job: EmailJob = { - ...prevalidatedJob, - readRetry: { ...prevalidatedJob.readRetry, interval }, - }; + const job: EmailJob = await req.json(); const jobInsensitive = structuredClone(job); jobInsensitive.from.username = "****REDACTED****"; diff --git a/src/duration.ts b/src/duration.ts deleted file mode 100644 index ad19921..0000000 --- a/src/duration.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { flow, pipe } from "fp-ts/function"; -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"; - -export type Duration = number; - -export enum DurationUnit { - MILLISECOND, - SECOND, - MINUTE, - HOUR, -} - -const durationUnitMap: Record = { - ms: DurationUnit.MILLISECOND, - milliseconds: DurationUnit.MILLISECOND, - sec: DurationUnit.SECOND, - seconds: DurationUnit.SECOND, - min: DurationUnit.MINUTE, - minutes: DurationUnit.MINUTE, - hr: DurationUnit.HOUR, - hour: DurationUnit.HOUR, - hours: DurationUnit.HOUR, -}; -const getDurationUnit = (key: string): O.Option => - O.fromNullable(durationUnitMap[key.toLowerCase()]); - -export const getMs = (duration: Duration): number => duration; -export const getSeconds = (duration: Duration): number => duration / 1000; -export const getMinutes = (duration: Duration): number => - getSeconds(duration) / 60; -export const getHours = (duration: Duration): number => - getMinutes(duration) / 60; -export const format = (duration: Duration): string => { - const ms = getMs(duration) % 1000; - const seconds = getSeconds(duration) % 60; - const minutes = getMinutes(duration) % 60; - const hours = getHours(duration); - - return ( - [hours, minutes, seconds] - .map((x) => Math.floor(x).toString().padStart(2, "0")) - .join(":") + - "." + - ms.toString().padStart(3, "0") - ); -}; - -export interface DurationBuilder { - readonly millis: number; - readonly seconds: number; - readonly minutes: number; - readonly hours: number; -} -export const createDurationBuilder = (): DurationBuilder => ({ - millis: 0, - seconds: 0, - minutes: 0, - hours: 0, -}); - -export type DurationBuilderField = ( - arg: T, -) => (builder: DurationBuilder) => DurationBuilder; - -export const withMillis: DurationBuilderField = - (millis) => (builder) => ({ - ...builder, - millis, - }); - -export const withSeconds: DurationBuilderField = - (seconds) => (builder) => ({ - ...builder, - seconds, - }); - -export const withMinutes: DurationBuilderField = - (minutes) => (builder) => ({ - ...builder, - minutes, - }); - -export const withHours: DurationBuilderField = - (hours) => (builder) => ({ - ...builder, - hours, - }); - -export const build = (builder: DurationBuilder): Duration => - builder.millis + - builder.seconds * 1000 + - builder.minutes * 60 * 1000 + - builder.hours * 60 * 60 * 1000; - -export const parse = (duration: string): E.Either => { - const parts = pipe( - duration, - S.split(" "), - R.map(S.trim), - R.filter((part) => !S.isEmpty(part)), - ); - - const valueUnitPairs = pipe( - parts, - R.mapWithIndex((i, part) => { - const isUnit = i % 2 !== 0; - if (!isUnit) return E.right(O.none); - - const value = Number(parts[i - 1]); - if (isNaN(value)) return E.left(`bad value: "${parts[i - 1]}"`); - - const unit = getDurationUnit(part); - if (O.isNone(unit)) return E.left(`unknown duration type: ${part}`); - - return E.right(O.some([unit.value, value] as [DurationUnit, number])); - }), - E.sequenceArray, - E.map( - flow( - R.filter(O.isSome), - R.map(({ value }) => value), - ), - ), - ); - - return pipe( - valueUnitPairs, - E.flatMap( - R.reduce( - E.of(createDurationBuilder()), - (builderEither, [unit, value]) => - pipe( - builderEither, - E.chain((builder) => { - switch (unit) { - case DurationUnit.MILLISECOND: - return E.right(withMillis(value)(builder)); - case DurationUnit.SECOND: - return E.right(withSeconds(value)(builder)); - case DurationUnit.MINUTE: - return E.right(withMinutes(value)(builder)); - case DurationUnit.HOUR: - return E.right(withHours(value)(builder)); - default: - return E.left(`unknown unit: ${unit}`); - } - }), - ), - ), - ), - E.map(build), - ); -}; diff --git a/tst/duration.spec.ts b/tst/duration.spec.ts deleted file mode 100644 index bcd50f5..0000000 --- a/tst/duration.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { pipe } from "fp-ts/function"; -import * as E from "fp-ts/Either"; -import { describe, test, expect } from "bun:test"; -import * as D from "../src/duration"; - -describe("Duration Utility", () => { - test("get unit should convert correctly", () => { - expect(D.getMs(1000)).toBe(1000); - expect(D.getSeconds(1000)).toBe(1); - expect(D.getMinutes(60000)).toBe(1); - expect(D.getHours(3600000)).toBe(1); - }); - - test("format should format duration correctly", () => { - expect(D.format(3600000 + 237 + 5 * 60 * 1000)).toBe("01:05:00.237"); - }); -}); - -describe("DurationBuilder", () => { - test("createDurationBuilder should create a builder with zero values", () => { - const builder = D.createDurationBuilder(); - expect(builder.millis).toBe(0); - expect(builder.seconds).toBe(0); - expect(builder.minutes).toBe(0); - expect(builder.hours).toBe(0); - }); - - test("withMillis should set fields correctly and with precedence", () => { - const builder = pipe( - D.createDurationBuilder(), - D.withMillis(0), - D.withSeconds(20), - D.withMinutes(30), - D.withHours(40), - D.withMillis(10), - ); - expect(builder.millis).toBe(10); - expect(builder.seconds).toBe(20); - expect(builder.minutes).toBe(30); - expect(builder.hours).toBe(40); - }); - - test("build should calculate total duration correctly", () => { - const duration = pipe( - D.createDurationBuilder(), - D.withMillis(10), - D.withSeconds(20), - D.withMinutes(30), - D.withHours(40), - D.build, - ); - expect(duration).toBe( - 10 + 20 * 1000 + 30 * 60 * 1000 + 40 * 60 * 60 * 1000, - ); - }); -}); - -describe("parse", () => { - test("should return right for a valid duration", () => { - expect(D.parse("10 seconds 1 hr 30 min")).toEqual( - E.right(1 * 60 * 60 * 1000 + 30 * 60 * 1000 + 10 * 1000), - ); - }); - - test("should operate with order", () => { - expect(D.parse("1 hr 30 min 2 hours")).toEqual( - E.right(2 * 60 * 60 * 1000 + 30 * 60 * 1000), - ); - }); - - test("returns left for unknown duration unit", () => { - expect(D.parse("1 xyz")).toEqual(E.left("unknown duration type: xyz")); - }); - - test("return left for invalid number", () => { - expect(D.parse("abc ms")).toEqual(E.left('bad value: "abc"')); - }); -});