The rewrite

This commit is contained in:
2024-06-25 20:00:42 +02:00
parent 5c60fbd93d
commit a67e7efb40
23 changed files with 721 additions and 461 deletions

View File

@@ -0,0 +1,4 @@
{
"name": "@make-api/api",
"type": "module"
}

148
packages/api/src/Api.ts Normal file
View File

@@ -0,0 +1,148 @@
/*!
* 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 type { Field } from "./Field";
import type { Method } from "./Method";
import type { MimeTypePredicate } from "./MimeType";
import { QueryEmpty, type Query } from "./Query";
import { RequestBodyFile, RequestBodyJson, RequestBodyMultipart, RequestBodyNone, RequestBodyText, RequestBodyUrlEncoded, type RequestBody } from "./RequestBody";
import { ResponseBodyFile, ResponseBodyJson, ResponseBodyText, ResponseEmpty, type ResponseBody } from "./ResponseBody";
import { Route, RouteLiteral, RouteParam } from "./Route";
export interface Api<Props extends Api.PropsAny> {
readonly method: Method;
readonly props: Props;
}
export namespace Api {
export type Any = Api<PropsAny>;
export type Props<T extends Api.Any> = T extends Api<infer Props> ? Props : never;
export type PropsAny = {
readonly route: Route.Any;
readonly request: RequestBody.Any;
readonly response: { readonly [_: number]: ResponseBody.Any };
readonly query: Query.Any;
};
}
export const Api = <const Props extends Api.PropsAny>(method: Method, props: Props): Api<Props> => Object.freeze<Api<Props>>({
method,
props,
});
// --- BUILDERS ----------------------------------------------------------------
export type RouteInputLiteral<Literal extends string> = Literal;
export type RouteInputParam<Name extends string, Schema extends S.Schema<any, string, unknown>> = readonly [Name, Schema];
export namespace RouteInputLiteral {
export type Any = RouteInputLiteral<string>;
}
export namespace RouteInputParam {
export type Any = RouteInputParam<string, S.Schema<any, string, unknown>>;
}
export namespace RouteInput {
export type Any = readonly (
| RouteInputLiteral.Any
| RouteInputParam.Any
)[];
export type Route<R extends RouteInput.Any> = { readonly [K in keyof R]:
R[K] extends RouteInputLiteral<infer Literal> ? RouteLiteral<Literal>
: R[K] extends RouteInputParam<infer Name, infer Schema> ? RouteParam<Name, Schema>
: never;
};
}
export const make = <const R extends RouteInput.Any>(method: Method, ...routeInput: R): Api<{
readonly route: RouteInput.Route<R>,
readonly request: RequestBodyNone,
readonly response: {},
readonly query: {},
}> => Api(method, {
route: Route(...routeInput.map((r) => typeof r === "string" ? RouteLiteral(r) : RouteParam(r[0], r[1])) as RouteInput.Route<R>),
request: RequestBodyNone,
response: ResponseEmpty,
query: QueryEmpty,
});
export const requestBodyText = <const Schema extends S.Schema<any, string, unknown>>(schema: Schema) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyText<Schema>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyText(schema) });
export const requestBodyJson = <const Schema extends S.Schema.Any>(schema: Schema) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyJson<Schema>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyJson(schema) });
export const requestBodyUrlEncoded = <const Fields extends { readonly [_: string]: Field.AnyUrlEncoded }>(fields: Fields) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyUrlEncoded<Fields>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyUrlEncoded(fields) });
export const requestBodyMultipart = <const Fields extends { readonly [_: string]: Field.Any }>(fields: Fields) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyMultipart<Fields>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyMultipart(fields) });
export const requestBodyFile = <const MimeType extends string>(mimeType: MimeTypePredicate<MimeType>) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyFile<MimeType>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyFile(mimeType) });
export const responseBodyText = <const Status extends number, const Schema extends S.Schema<any, string, unknown>>(status: Status, schema: Schema) =>
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: { readonly [K in keyof T["props"]["response"] | Status]: K extends Status ? ResponseBodyText<Schema> : T["props"]["response"][K] },
readonly query: T["props"]["query"],
}> => Api(method, { ...props, response: Object.freeze<any>({ ...response, [status]: ResponseBodyText(schema) }) });
export const responseBodyJson = <const Status extends number, const Schema extends S.Schema.Any>(status: Status, schema: Schema) =>
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: { readonly [K in keyof T["props"]["response"] | Status]: K extends Status ? ResponseBodyJson<Schema> : T["props"]["response"][K] },
readonly query: T["props"]["query"],
}> => Api(method, { ...props, response: Object.freeze<any>({ ...response, [status]: ResponseBodyJson(schema) }) });
export const responseBodyFile = <const Status extends number, const MimeType extends string>(status: Status, mimeType: MimeTypePredicate<MimeType>) =>
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: { readonly [K in keyof T["props"]["response"] | Status]: K extends Status ? ResponseBodyFile<MimeType> : T["props"]["response"][K] },
readonly query: T["props"]["query"],
}> => Api(method, { ...props, response: Object.freeze<any>({ ...response, [status]: ResponseBodyFile(mimeType) }) });
export const query = <const Query extends Query.Any>(query: Query) =>
<const T extends Api.Any>({ method, props: { query: _, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: T["props"]["response"],
readonly query: Query,
}> => Api(method, { ...props, query });

74
packages/api/src/Field.ts Normal file
View File

@@ -0,0 +1,74 @@
/*!
* 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 type { MimeTypePredicate } from "./MimeType";
import type { Multiplicity } from "./Multiplicity";
// --- INTERFACES --------------------------------------------------------------
export interface FieldText<M extends Multiplicity, Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Text";
readonly multiplicity: M;
readonly schema: Schema;
}
export interface FieldJson<M extends Multiplicity, Schema extends S.Schema.Any> {
readonly _tag: "Json";
readonly multiplicity: M;
readonly schema: Schema;
}
export interface FieldFile<M extends Multiplicity, MimeType extends string> {
readonly _tag: "File";
readonly multiplicity: M;
readonly mimeType: MimeTypePredicate<MimeType>;
}
export namespace FieldText {
export type Any = FieldText<Multiplicity, S.Schema<any, string, unknown>>;
}
export namespace FieldJson {
export type Any = FieldJson<Multiplicity, S.Schema.Any>;
}
export namespace FieldFile {
export type Any = FieldFile<Multiplicity, string>;
}
export namespace Field {
export type Any =
| FieldText.Any
| FieldJson.Any
| FieldFile.Any
;
export type AnyUrlEncoded =
| FieldText.Any
| FieldJson.Any
;
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const FieldText = <const M extends Multiplicity, const Schema extends S.Schema<any, string, unknown>>(multiplicity: M, schema: Schema): FieldText<M, Schema> => Object.freeze<FieldText<M, Schema>>({
_tag: "Text",
multiplicity,
schema,
});
export const FieldJson = <const M extends Multiplicity, const Schema extends S.Schema.Any>(multiplicity: M, schema: Schema): FieldJson<M, Schema> => Object.freeze<FieldJson<M, Schema>>({
_tag: "Json",
multiplicity,
schema,
});
export const FieldFile = <const M extends Multiplicity, const MimeType extends string>(multiplicity: M, mimeType: MimeTypePredicate<MimeType>): FieldFile<M, MimeType> => Object.freeze<FieldFile<M, MimeType>>({
_tag: "File",
multiplicity,
mimeType,
});

View File

@@ -0,0 +1,13 @@
/*!
* 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/.
*/
export type Method =
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "PATCH"
;

View File

@@ -0,0 +1,9 @@
/*!
* 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 { Predicate } from "effect"
export type MimeTypePredicate<T extends string> = T | readonly T[] | Predicate.Refinement<string, T>;

View File

@@ -0,0 +1,23 @@
/*!
* 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 type { Option as O } from "effect";
export type Multiplicity =
| "single"
| "optional"
| "multiple"
| "any"
;
export namespace Multiplicity {
export type With<M extends Multiplicity, T> = {
single: T,
optional: O.Option<T>,
multiple: readonly [T, ...T[]],
any: readonly T[],
}[M];
}

13
packages/api/src/Query.ts Normal file
View File

@@ -0,0 +1,13 @@
/*!
* 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 type { Field } from "./Field";
export namespace Query {
export type Any = { readonly [_: string]: Field.AnyUrlEncoded };
}
export const QueryEmpty: {} = Object.freeze<{}>({});

View File

@@ -0,0 +1,102 @@
/*!
* 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 type { Field } from "./Field";
import type { MimeTypePredicate } from "./MimeType";
// --- INTERFACES --------------------------------------------------------------
export interface RequestBodyNone {
readonly _tag: "None";
}
export interface RequestBodyText<Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Text";
readonly schema: Schema;
}
export interface RequestBodyJson<Schema extends S.Schema.Any> {
readonly _tag: "Json";
readonly schema: Schema;
}
export interface RequestBodyUrlEncoded<Fields extends { readonly [_: string]: Field.AnyUrlEncoded }> {
readonly _tag: "UrlEncoded";
readonly fields: Readonly<Fields>;
}
export interface RequestBodyMultipart<Fields extends { readonly [_: string]: Field.Any }> {
readonly _tag: "Multipart";
readonly fields: Readonly<Fields>;
}
export interface RequestBodyFile<MimeType extends string> {
readonly _tag: "File";
readonly mimeType: MimeTypePredicate<MimeType>;
}
export namespace RequestBodyText {
export type Any = RequestBodyText<S.Schema<any, string, unknown>>;
}
export namespace RequestBodyJson {
export type Any = RequestBodyJson<S.Schema.Any>;
}
export namespace RequestBodyUrlEncoded {
export type Any = RequestBodyUrlEncoded<{}>;
}
export namespace RequestBodyMultipart {
export type Any = RequestBodyMultipart<{}>;
}
export namespace RequestBodyFile {
export type Any = RequestBodyFile<string>;
}
export namespace RequestBody {
export type Any =
| RequestBodyNone
| RequestBodyText.Any
| RequestBodyJson.Any
| RequestBodyUrlEncoded.Any
| RequestBodyMultipart.Any
| RequestBodyFile.Any
;
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const RequestBodyNone: RequestBodyNone = Object.freeze<RequestBodyNone>({
_tag: "None",
});
export const RequestBodyText = <const Schema extends S.Schema<any, string, unknown>>(schema: Schema): RequestBodyText<Schema> => Object.freeze<RequestBodyText<Schema>>({
_tag: "Text",
schema,
});
export const RequestBodyJson = <const Schema extends S.Schema.Any>(schema: Schema): RequestBodyJson<Schema> => Object.freeze<RequestBodyJson<Schema>>({
_tag: "Json",
schema,
});
export const RequestBodyUrlEncoded = <const Fields extends { readonly [_: string]: Field.AnyUrlEncoded }>(fields: Fields): RequestBodyUrlEncoded<Fields> => Object.freeze<RequestBodyUrlEncoded<Fields>>({
_tag: "UrlEncoded",
fields,
});
export const RequestBodyMultipart = <const Fields extends { readonly [_: string]: Field.Any }>(fields: Fields): RequestBodyMultipart<Fields> => Object.freeze<RequestBodyMultipart<Fields>>({
_tag: "Multipart",
fields,
});
export const RequestBodyFile = <const MimeType extends string>(mimeType: MimeTypePredicate<MimeType>): RequestBodyFile<MimeType> => Object.freeze<RequestBodyFile<MimeType>>({
_tag: "File",
mimeType,
});

View File

@@ -0,0 +1,90 @@
/*!
* 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 type { MimeTypePredicate } from "./MimeType";
// --- INTERFACES --------------------------------------------------------------
export interface ResponseBodyNone {
readonly _tag: "None";
}
export interface ResponseBodyText<Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Text";
readonly schema: Schema;
}
export interface ResponseBodyJson<Schema extends S.Schema.Any> {
readonly _tag: "Json";
readonly schema: Schema;
}
export interface ResponseBodyFile<MimeType extends string> {
readonly _tag: "File";
readonly mimeType: MimeTypePredicate<MimeType>;
}
export namespace ResponseBodyText {
export type Any = ResponseBodyText<S.Schema<any, string, unknown>>;
}
export namespace ResponseBodyJson {
export type Any = ResponseBodyJson<S.Schema.Any>;
}
export namespace ResponseBodyFile {
export type Any = ResponseBodyFile<string>;
}
export namespace ResponseBody {
export type Any =
| ResponseBodyNone
| ResponseBodyText.Any
| ResponseBodyJson.Any
| ResponseBodyFile.Any
;
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const ResponseBodyNone: ResponseBodyNone = Object.freeze<ResponseBodyNone>({
_tag: "None",
});
export const ResponseBodyText = <const Schema extends S.Schema<any, string, unknown>>(schema: Schema): ResponseBodyText<Schema> => Object.freeze<ResponseBodyText<Schema>>({
_tag: "Text",
schema,
});
export const ResponseBodyJson = <const Schema extends S.Schema.Any>(schema: Schema): ResponseBodyJson<Schema> => Object.freeze<ResponseBodyJson<Schema>>({
_tag: "Json",
schema,
});
export const ResponseBodyFile = <const MimeType extends string>(mimeType: MimeTypePredicate<MimeType>): ResponseBodyFile<MimeType> => Object.freeze<ResponseBodyFile<MimeType>>({
_tag: "File",
mimeType,
});
export const ResponseEmpty: {} = Object.freeze<{}>({});
import type { Option as O } from "effect";
export type Multiplicity =
| "single"
| "optional"
| "multiple"
| "any"
;
export namespace Multiplicity {
export type With<M extends Multiplicity, T> = {
single: T,
optional: O.Option<T>,
multiple: readonly [T, ...T[]],
any: readonly T[],
}[M];
}

50
packages/api/src/Route.ts Normal file
View File

@@ -0,0 +1,50 @@
/*!
* 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";
// --- INTERFACES --------------------------------------------------------------
export interface RouteLiteral<Literal extends string> {
readonly _tag: "Literal";
readonly literal: Literal;
}
export interface RouteParam<Name extends string, Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Param";
readonly name: Name;
readonly schema: Schema;
}
export namespace RouteLiteral {
export type Any = RouteLiteral<string>;
}
export namespace RouteParam {
export type Any = RouteParam<string, S.Schema<any, string, unknown>>;
}
export namespace Route {
export type Any = readonly (
| RouteLiteral.Any
| RouteParam.Any
)[];
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const RouteLiteral = <const Literal extends string>(literal: Literal): RouteLiteral<Literal> => Object.freeze<RouteLiteral<Literal>>({
_tag: "Literal",
literal,
});
export const RouteParam = <const Name extends string, const Schema extends S.Schema<any, string, unknown>>(name: Name, schema: Schema): RouteParam<Name, Schema> => Object.freeze<RouteParam<Name, Schema>>({
_tag: "Param",
name,
schema,
});
export const Route = <const Route extends Route.Any>(...route: Route): Route => Object.freeze<Route>(route);