You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
159 lines
3.6 KiB
TypeScript
159 lines
3.6 KiB
TypeScript
import { TypeOf, Schema, ZodError } from "https://deno.land/x/zod@v3.17.10/mod.ts";
|
|
|
|
export interface Sig<I extends Schema<any> | undefined, O extends Schema<any> | undefined> {
|
|
_sig: true,
|
|
input: I,
|
|
output: O,
|
|
}
|
|
export const sig = <I extends Schema<any> | undefined, O extends Schema<any> | undefined>(i: I, o: O): Sig<I, O> => ({
|
|
_sig: true,
|
|
input: i,
|
|
output: o,
|
|
});
|
|
|
|
type MakeHandlersObject<T> = {
|
|
[K in keyof T]: MakeHandlers<T[K]>
|
|
};
|
|
|
|
type MakeHandlersSig<T extends Sig<any, any>> =
|
|
T["input"] extends undefined ? (
|
|
T["output"] extends undefined
|
|
? () => Promise<void>
|
|
: () => Promise<TypeOf<T["output"]>>
|
|
) : (
|
|
T["output"] extends undefined
|
|
? (i: TypeOf<T["input"]>) => Promise<void>
|
|
: (i: TypeOf<T["input"]>) => Promise<TypeOf<T["output"]>>
|
|
);
|
|
|
|
export type MakeHandlers<T> =
|
|
T extends Sig<any, any>
|
|
? MakeHandlersSig<T>
|
|
: MakeHandlersObject<T>;
|
|
|
|
type Handler = (req: Request) => Promise<Response>;
|
|
const makeServe_ = <T>(
|
|
api: T,
|
|
handlers: MakeHandlers<T>,
|
|
path: string,
|
|
paths: Map<string, Handler>,
|
|
) => {
|
|
const a = api as any;
|
|
Object.entries(handlers).forEach(([k, v]) => {
|
|
const p = `${path}/${k}`;
|
|
|
|
if (typeof v === "function") {
|
|
paths.set(p, async (req: Request) => {
|
|
if (a[k].input) {
|
|
const body = await req.json();
|
|
const parsed = a[k].input.parse(body);
|
|
|
|
return v(parsed);
|
|
}
|
|
|
|
return v();
|
|
});
|
|
}
|
|
|
|
makeServe_(a[k], v, p, paths);
|
|
});
|
|
}
|
|
|
|
export const makeServe = <T>(
|
|
api: T,
|
|
handlers: MakeHandlers<T>,
|
|
path: string,
|
|
) => {
|
|
const paths = new Map<string, Handler>();
|
|
|
|
makeServe_(api, handlers, path, paths);
|
|
|
|
return async (req: Request) => {
|
|
const path = new URL(req.url).pathname;
|
|
const handler = paths.get(path);
|
|
|
|
if (!handler) {
|
|
return new Response(null, {
|
|
status: 404,
|
|
});
|
|
}
|
|
|
|
const res = await handler(req);
|
|
|
|
return new Response(res && JSON.stringify(res));
|
|
};
|
|
};
|
|
|
|
type MakeClient<T> =
|
|
T extends Sig<any, any>
|
|
? MakeClientSig<T>
|
|
: MakeClientObject<T>;
|
|
|
|
type MakeClientObject<T> = {
|
|
[K in keyof T]: MakeClient<T[K]>
|
|
};
|
|
|
|
type ClientHttpError = Response;
|
|
interface ClientValidationError<T> {
|
|
error: ZodError<T>,
|
|
}
|
|
export type ClientResponse<T> = T | ClientHttpError | ClientValidationError<T>;
|
|
|
|
export type MakeClientSig<T extends Sig<any, any>> =
|
|
T["input"] extends undefined ? (
|
|
T["output"] extends undefined
|
|
? () => Promise<ClientResponse<void>>
|
|
: () => Promise<ClientResponse<TypeOf<T["output"]>>>
|
|
) : (
|
|
T["output"] extends undefined
|
|
? (i: TypeOf<T["input"]>) => Promise<ClientResponse<void>>
|
|
: (i: TypeOf<T["input"]>) => Promise<ClientResponse<TypeOf<T["output"]>>>
|
|
);
|
|
|
|
export const makeClient = <T>(api: T, path: string): MakeClient<T> => {
|
|
return Object.fromEntries(Object.entries(api).map(([k, v]) => {
|
|
const p = `${path}/${k}` as string;
|
|
|
|
if ("_sig" in v) {
|
|
const v_ = v as Sig<Schema<any> | undefined, Schema<any> | undefined>;
|
|
return [k, async (body: any) => {
|
|
const b = body ? JSON.stringify(body) : null;
|
|
const re = await fetch(p, { body: b, method: "POST" });
|
|
|
|
if (!re.ok) {
|
|
return re;
|
|
}
|
|
|
|
if (v_.output) {
|
|
const res = await v_.output.safeParseAsync(await re.json());
|
|
if (!res.success) {
|
|
return {
|
|
error: res.error,
|
|
}
|
|
}
|
|
|
|
return res.data;
|
|
}
|
|
}]
|
|
}
|
|
|
|
return [k, makeClient(v, p)];
|
|
})) as MakeClient<T>;
|
|
};
|
|
|
|
export const isHttpError = (v: any): v is ClientHttpError => {
|
|
return v instanceof Response;
|
|
}
|
|
|
|
export const isValidationError = (v: any): v is ClientValidationError<any> => {
|
|
return (typeof v === "object" && v.error instanceof ZodError);
|
|
}
|
|
|
|
export const assert = <T>(res: ClientResponse<T>): T => {
|
|
if (isHttpError(res) || isValidationError(res)) {
|
|
throw res;
|
|
}
|
|
|
|
return res;
|
|
}
|