107 lines
2.8 KiB
TypeScript
107 lines
2.8 KiB
TypeScript
import * as Api from "common/Api";
|
|
import * as Cbor from "common/Cbor";
|
|
import * as Client from "common/Client";
|
|
import { Cause, Effect, Either, HashMap, Inspectable, Option, pipe } from "effect";
|
|
|
|
/* NOTE We shouldn't need to extract this to a separate type, but if we don't do
|
|
* it the TypeScript parser in VS Code sort of blows up and the syntax colors
|
|
* turn wrong.
|
|
*/
|
|
type Return<Impl extends Api.ApiBundleImpl<any>> = Effect.Effect<Response, never, Effect.Effect.Context<ReturnType<Impl[keyof Impl]>>>;
|
|
|
|
const catchToResponse = Effect.catchAll((error: unknown) => pipe(
|
|
error,
|
|
Inspectable.toJSON,
|
|
Cbor.encode,
|
|
Effect.map(Client.cborBody),
|
|
Effect.matchEffect({
|
|
onSuccess: ({ body, headers }) => Effect.fail(new Response(body, { status: 500, headers })),
|
|
onFailure: () => Effect.fail(new Response(null, { status: 500 })),
|
|
}),
|
|
));
|
|
|
|
export const implement = <
|
|
const Record extends { readonly [_: string]: Api.ApiAny },
|
|
const Impl extends Api.ApiBundleImpl<Record>,
|
|
>(
|
|
bundle: Api.ApiBundle<Record>,
|
|
impl: Impl,
|
|
): (key: string, request: Request) => Return<Impl> => {
|
|
return (key, requestObject) => {
|
|
/* Force both return types to be `Response`. We can use the error route
|
|
* for it's short-circuit capabilities.
|
|
*/
|
|
const effect: Effect.Effect<Response, Response, any> = Effect.gen(function* () {
|
|
const maybeApi = HashMap.get(bundle.map, key);
|
|
if (Option.isNone(maybeApi)) {
|
|
return RESPONSE_API_NOT_FOUND;
|
|
}
|
|
const { value: api } = maybeApi;
|
|
|
|
const fn = impl[key];
|
|
if (fn === undefined) {
|
|
return RESPONSE_API_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
const request = yield* pipe(
|
|
requestObject,
|
|
Client.decodeBody(api.request),
|
|
catchToResponse,
|
|
);
|
|
|
|
const result = yield* pipe(
|
|
request,
|
|
fn,
|
|
Effect.sandbox,
|
|
Effect.either,
|
|
);
|
|
|
|
const { status, response } = pipe(
|
|
result,
|
|
Either.match({
|
|
onLeft: (cause) => pipe(
|
|
cause,
|
|
Cause.failureOrCause,
|
|
Either.match({
|
|
onLeft: (error) => ({
|
|
status: 400,
|
|
response: pipe(
|
|
error,
|
|
Client.encodeBody(api.error),
|
|
catchToResponse,
|
|
),
|
|
}),
|
|
onRight: (die) => ({
|
|
status: 500,
|
|
response: pipe(
|
|
die,
|
|
Inspectable.toJSON,
|
|
Cbor.encode,
|
|
Effect.map(Client.cborBody),
|
|
catchToResponse,
|
|
),
|
|
}),
|
|
}),
|
|
),
|
|
onRight: (response) => ({
|
|
status: 200,
|
|
response: pipe(
|
|
response,
|
|
Client.encodeBody(api.response),
|
|
catchToResponse,
|
|
),
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const { body, headers } = yield* response;
|
|
return new Response(body, { status, headers });
|
|
});
|
|
|
|
return Effect.catchAll(effect, Effect.succeed);
|
|
};
|
|
};
|
|
|
|
const RESPONSE_API_NOT_FOUND = new Response(null, { status: 404 });
|
|
const RESPONSE_API_NOT_IMPLEMENTED = new Response(null, { status: 501 });
|