effect-api/packages/backend/src/Api.ts

108 lines
2.9 KiB
TypeScript

/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Schema as S } from "@effect/schema";
import * as Api from "@make-api/api/Api";
import * as Query from "@make-api/api/Query";
import * as RequestBody from "@make-api/api/RequestBody";
import * as Route from "@make-api/api/Route";
import { Array as A, Effect, Match, Option as O, Record as R, pipe } from "effect";
const getUsers = pipe(
Api.make("GET", "/users"),
Api.responseBodyJson(200, S.Array(S.Struct({
userId: S.UUID,
username: S.String,
email: S.String,
}))),
Api.responseBodyJson(500, S.Struct({
error: S.String,
stackTrace: S.optional(S.String, { exact: true }),
})),
);
type RouteParams<T extends Route.Route.Any> = { readonly [_: string]: unknown }; // TODO
const matchesRoute = <T extends Route.Route.Any>(route: T, pathComponents: readonly string[]): boolean => {
if (route.length !== pathComponents.length) return false;
return pipe(
A.zip(route, pathComponents),
A.every(([token, pathComponent]) => token._tag !== "Literal" || token.literal === pathComponent),
);
};
const makeRouteParser = <T extends Route.Route.Any>(route: T) => {
const decoder = pipe(
route,
A.filterMap((r) => r._tag === "Param" ? O.some([r.name, r.schema] as const) : O.none()),
R.fromEntries,
S.Struct,
(_) => S.decode(_, { errors: "all" }),
);
return (pathComponents: readonly string[]) => pipe(
pathComponents,
A.filterMap((c, i) => {
const r = route[i];
return r._tag === "Param" ? O.some([r.name, c] as const) : O.none();
}),
R.fromEntries,
decoder,
);
};
const makeQueryParser = <T extends Query.Query.Any>(query: T) => {
const decoder = pipe();
return (searchParams: URLSearchParams) => pipe();
};
const makeBodyParser = <T extends RequestBody.RequestBody.Any>(requestBody: T) => {
const decoder = pipe(
Match.value<RequestBody.RequestBody.Any>(requestBody),
Match.tags({
None: () => {},
Text: () => {},
Json: () => {},
UrlEncoded: () => {},
Multipart: () => {},
File: () => {},
}),
);
return (req: Request) => pipe();
};
const routeParser = makeRouteParser(getUsers.props.route);
const queryParser = makeQueryParser(getUsers.props.query);
const bodyParser = makeBodyParser(getUsers.props.request);
Bun.serve({
fetch: (req) => {
const url = new URL(req.url);
const pathname = url.pathname;
const pathComponents = pathname.replaceAll(/^\/|\/$/g, "").split("/");
const searchParams = url.searchParams;
if (matchesRoute(getUsers.props.route, pathComponents)) {
const paramsEffect = routeParser(pathComponents);
const queryEffect = queryParser(searchParams);
const bodyEffect = bodyParser(req);
const effect = Effect.all({
params: paramsEffect,
query: queryEffect,
body: bodyEffect,
});
}
},
websocket: {
message: (ws, message) => {}
},
});