remove shitty duration parser
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
c4385abb33
commit
2e8f6a1d14
12
src/api.ts
12
src/api.ts
@ -1,4 +1,3 @@
|
|||||||
import { parse } from "./duration";
|
|
||||||
import { perform } from "./email";
|
import { perform } from "./email";
|
||||||
import type { EmailJob } from "./job";
|
import type { EmailJob } from "./job";
|
||||||
import { ConsoleLogger } from "./logger";
|
import { ConsoleLogger } from "./logger";
|
||||||
@ -11,16 +10,7 @@ export const main = (port: number) => {
|
|||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
if (req.method === "POST" && url.pathname === "/api/email") {
|
if (req.method === "POST" && url.pathname === "/api/email") {
|
||||||
const prevalidatedJob = await req.json();
|
const job: EmailJob = 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 jobInsensitive = structuredClone(job);
|
const jobInsensitive = structuredClone(job);
|
||||||
jobInsensitive.from.username = "****REDACTED****";
|
jobInsensitive.from.username = "****REDACTED****";
|
||||||
|
156
src/duration.ts
156
src/duration.ts
@ -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<string, DurationUnit> = {
|
|
||||||
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<DurationUnit> =>
|
|
||||||
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<T> = (
|
|
||||||
arg: T,
|
|
||||||
) => (builder: DurationBuilder) => DurationBuilder;
|
|
||||||
|
|
||||||
export const withMillis: DurationBuilderField<number> =
|
|
||||||
(millis) => (builder) => ({
|
|
||||||
...builder,
|
|
||||||
millis,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const withSeconds: DurationBuilderField<number> =
|
|
||||||
(seconds) => (builder) => ({
|
|
||||||
...builder,
|
|
||||||
seconds,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const withMinutes: DurationBuilderField<number> =
|
|
||||||
(minutes) => (builder) => ({
|
|
||||||
...builder,
|
|
||||||
minutes,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const withHours: DurationBuilderField<number> =
|
|
||||||
(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<string, Duration> => {
|
|
||||||
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<string, DurationBuilder>(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),
|
|
||||||
);
|
|
||||||
};
|
|
@ -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"'));
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user