The rewrite
This commit is contained in:
parent
5c60fbd93d
commit
a67e7efb40
@ -9,5 +9,10 @@ insert_final_newline = true
|
|||||||
tab_width = 4
|
tab_width = 4
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 2
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
# effect-api
|
# effect-api
|
||||||
A library for defining, using and implementing HTTP JSON APIs using Effect ecosystem
|
|
||||||
|
A library for defining, using and implementing HTTP JSON APIs using Effect
|
||||||
|
ecosystem.
|
||||||
|
63
package.json
63
package.json
@ -1,41 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-api",
|
"private": true,
|
||||||
"version": "0.1.0",
|
"type": "module",
|
||||||
"description": "A library for defining, using and implementing HTTP JSON APIs using Effect ecosystem",
|
"author": {
|
||||||
"keywords": [
|
"name": "Szymon Nowakowski",
|
||||||
"api",
|
"email": "renati@renati.me",
|
||||||
"effect",
|
"url": "https://renati.me/"
|
||||||
"fetch",
|
},
|
||||||
"http",
|
"homepage": "https://renati.me/effect-api",
|
||||||
"json"
|
"license": "MPL-2.0",
|
||||||
],
|
"repository": {
|
||||||
"homepage": "https://renati.me/effect-api",
|
"type": "git",
|
||||||
"license": "MPL-2.0",
|
"url": "https://gitea.renati.me/renati/effect-api.git"
|
||||||
"browser": "./dist/oktaeder.js",
|
},
|
||||||
"repository": {
|
"workspaces": [
|
||||||
"type": "git",
|
"./packages/*"
|
||||||
"url": "https://gitea.renati.me/renati/effect-api.git"
|
],
|
||||||
},
|
"devDependencies": {
|
||||||
"scripts": {
|
"typescript": "^5.5.2"
|
||||||
"build": "tsc --build"
|
},
|
||||||
},
|
"dependencies": {
|
||||||
"dependencies": {
|
"@effect/schema": "^0.68.9",
|
||||||
"@effect/schema": "^0.64.3",
|
"effect": "^3.4.2",
|
||||||
"effect": "^2.4.6",
|
"fast-check": "^3.19.0"
|
||||||
"fast-check": "^3.16.0",
|
}
|
||||||
"tslib": "^2.6.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.4.2"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/effect-api.d.ts",
|
|
||||||
"import": "./dist/effect-api.js"
|
|
||||||
},
|
|
||||||
"./Api": {
|
|
||||||
"types": "./dist/Api.d.ts",
|
|
||||||
"import": "./dist/Api.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
4
packages/api/package.json
Normal file
4
packages/api/package.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "@make-api/api",
|
||||||
|
"type": "module"
|
||||||
|
}
|
148
packages/api/src/Api.ts
Normal file
148
packages/api/src/Api.ts
Normal 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
74
packages/api/src/Field.ts
Normal 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,
|
||||||
|
});
|
13
packages/api/src/Method.ts
Normal file
13
packages/api/src/Method.ts
Normal 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"
|
||||||
|
;
|
9
packages/api/src/MimeType.ts
Normal file
9
packages/api/src/MimeType.ts
Normal 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>;
|
23
packages/api/src/Multiplicity.ts
Normal file
23
packages/api/src/Multiplicity.ts
Normal 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
13
packages/api/src/Query.ts
Normal 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<{}>({});
|
102
packages/api/src/RequestBody.ts
Normal file
102
packages/api/src/RequestBody.ts
Normal 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,
|
||||||
|
});
|
90
packages/api/src/ResponseBody.ts
Normal file
90
packages/api/src/ResponseBody.ts
Normal 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
50
packages/api/src/Route.ts
Normal 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);
|
7
packages/backend/package.json
Normal file
7
packages/backend/package.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "@make-api/backend",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
}
|
||||||
|
}
|
107
packages/backend/src/Api.ts
Normal file
107
packages/backend/src/Api.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*!
|
||||||
|
* 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 parseRoute = <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 parseQuery = <T extends Query.Query.Any>(query: T) => {
|
||||||
|
const decoder = pipe();
|
||||||
|
|
||||||
|
return (searchParams: URLSearchParams) => pipe();
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseBody = <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 = parseRoute(getUsers.props.route);
|
||||||
|
const queryParser = parseQuery(getUsers.props.query);
|
||||||
|
const bodyParser = parseBody(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) => {}
|
||||||
|
},
|
||||||
|
});
|
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@ -1,61 +0,0 @@
|
|||||||
lockfileVersion: '6.0'
|
|
||||||
|
|
||||||
settings:
|
|
||||||
autoInstallPeers: true
|
|
||||||
excludeLinksFromLockfile: false
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
'@effect/schema':
|
|
||||||
specifier: ^0.64.3
|
|
||||||
version: 0.64.3(effect@2.4.6)(fast-check@3.16.0)
|
|
||||||
effect:
|
|
||||||
specifier: ^2.4.6
|
|
||||||
version: 2.4.6
|
|
||||||
fast-check:
|
|
||||||
specifier: ^3.16.0
|
|
||||||
version: 3.16.0
|
|
||||||
tslib:
|
|
||||||
specifier: ^2.6.2
|
|
||||||
version: 2.6.2
|
|
||||||
|
|
||||||
devDependencies:
|
|
||||||
typescript:
|
|
||||||
specifier: ^5.4.2
|
|
||||||
version: 5.4.2
|
|
||||||
|
|
||||||
packages:
|
|
||||||
|
|
||||||
/@effect/schema@0.64.3(effect@2.4.6)(fast-check@3.16.0):
|
|
||||||
resolution: {integrity: sha512-Pn/IG+X9GTXLVvDtdjptr3QWxpcH+m7LEgM6loLWk7DP8SPKqWdo3rFaZiILpJFci8QWEk7K23C3IJZOnMfTtA==}
|
|
||||||
peerDependencies:
|
|
||||||
effect: ^2.4.6
|
|
||||||
fast-check: ^3.13.2
|
|
||||||
dependencies:
|
|
||||||
effect: 2.4.6
|
|
||||||
fast-check: 3.16.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/effect@2.4.6:
|
|
||||||
resolution: {integrity: sha512-gaZC+aopWvS67SX8RFT3mirabjpVBk5qshj/Z3IKW35DjzpQ8WhAE7VWwfBBWWMBuQ/B0RsbzKR8YGN2AivIgQ==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/fast-check@3.16.0:
|
|
||||||
resolution: {integrity: sha512-k8GtQHi4pJoRQ1gVDFQno+/FVkowo/ehiz/aCj9O/D7HRWb1sSFzNrw+iPVU8QlWtH+jNwbuN+dDVg3QkS56DQ==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
dependencies:
|
|
||||||
pure-rand: 6.0.4
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/pure-rand@6.0.4:
|
|
||||||
resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tslib@2.6.2:
|
|
||||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/typescript@5.4.2:
|
|
||||||
resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==}
|
|
||||||
engines: {node: '>=14.17'}
|
|
||||||
hasBin: true
|
|
||||||
dev: true
|
|
147
src/Api.ts
147
src/Api.ts
@ -1,147 +0,0 @@
|
|||||||
import * as S from "@effect/schema/Schema";
|
|
||||||
import * as O from "effect/Option";
|
|
||||||
import * as Types from "effect/Types";
|
|
||||||
|
|
||||||
// --- METHOD ------------------------------------------------------------------
|
|
||||||
|
|
||||||
export type Method =
|
|
||||||
| "GET"
|
|
||||||
| "POST"
|
|
||||||
| "PUT"
|
|
||||||
| "PATCH"
|
|
||||||
| "DELETE"
|
|
||||||
;
|
|
||||||
|
|
||||||
// --- ROUTE INPUT -------------------------------------------------------------
|
|
||||||
|
|
||||||
export type RouteInputLiteral<K extends string> = K;
|
|
||||||
|
|
||||||
export type RouteInputSchema<K extends string, T> = readonly [name: K, schema: S.Schema<T, string>];
|
|
||||||
|
|
||||||
export type RouteInputToken =
|
|
||||||
| RouteInputLiteral<string>
|
|
||||||
| RouteInputSchema<string, any>
|
|
||||||
;
|
|
||||||
|
|
||||||
// --- ROUTE -------------------------------------------------------------------
|
|
||||||
|
|
||||||
export enum RouteTokenType {
|
|
||||||
Literal = "literal",
|
|
||||||
Schema = "schema",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteTokenLiteral<K extends string> {
|
|
||||||
readonly type: RouteTokenType.Literal,
|
|
||||||
readonly literal: K,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteTokenSchema<K extends string, T> {
|
|
||||||
readonly type: RouteTokenType.Schema;
|
|
||||||
readonly name: K;
|
|
||||||
readonly schema: S.Schema<T, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RouteToken =
|
|
||||||
| RouteTokenLiteral<string>
|
|
||||||
| RouteTokenSchema<string, any>
|
|
||||||
;
|
|
||||||
|
|
||||||
export function routeTokenLiteral<const K extends string>(literal: K): RouteTokenLiteral<K> {
|
|
||||||
return Object.freeze<RouteTokenLiteral<K>>({
|
|
||||||
type: RouteTokenType.Literal,
|
|
||||||
literal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function routeTokenSchema<const K extends string, const T>(name: K, schema: S.Schema<T, string>): RouteTokenSchema<K, T> {
|
|
||||||
return Object.freeze<RouteTokenSchema<K, T>>({
|
|
||||||
type: RouteTokenType.Schema,
|
|
||||||
name,
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ROUTE FROM INPUT --------------------------------------------------------
|
|
||||||
|
|
||||||
export type RouteFromInput<I extends readonly RouteInputToken[]> = {
|
|
||||||
readonly [K in keyof I]:
|
|
||||||
I[K] extends RouteInputLiteral<infer _K> ? RouteTokenLiteral<_K>
|
|
||||||
: I[K] extends RouteInputSchema<infer _K, infer _T> ? RouteTokenSchema<_K, _T>
|
|
||||||
: never;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ParamFromRouteToken<T extends RouteToken> =
|
|
||||||
T extends RouteTokenSchema<infer K, infer V> ? { readonly [_ in K]: V } : never;
|
|
||||||
|
|
||||||
export type ParamsFromRoute<I extends readonly RouteToken[]> =
|
|
||||||
Types.Simplify<Types.UnionToIntersection<ParamFromRouteToken<I[number]>>>;
|
|
||||||
|
|
||||||
export function makeRoute<const RouteInput extends readonly RouteInputToken[]>(...tokens: RouteInput): RouteFromInput<RouteInput> {
|
|
||||||
const names = new Set<string>();
|
|
||||||
const ret = new Array<RouteToken>(tokens.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < tokens.length; ++i) {
|
|
||||||
const token = tokens[i];
|
|
||||||
if (typeof token === "string") {
|
|
||||||
ret[i] = routeTokenLiteral(token);
|
|
||||||
} else {
|
|
||||||
const [name, schema] = token;
|
|
||||||
if (names.has(name)) {
|
|
||||||
throw new Error(`Duplicate URL parameter name in route: ${name}`);
|
|
||||||
}
|
|
||||||
ret[i] = routeTokenSchema(name, schema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.freeze(ret) as RouteFromInput<RouteInput>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- API ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
export interface ApiBase {
|
|
||||||
readonly requestBody: O.Option<S.Schema<any, any>>;
|
|
||||||
readonly query: O.Option<S.Schema<any, { readonly [_: string]: string }>>;
|
|
||||||
readonly responseBody: O.Option<S.Schema<any, any>>;
|
|
||||||
|
|
||||||
readonly method: Method;
|
|
||||||
readonly route: readonly RouteToken[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RequestType<T extends ApiBase> = S.Schema.Type<O.Option.Value<T["requestBody"]>>;
|
|
||||||
export type RequestEncoded<T extends ApiBase> = S.Schema.Encoded<O.Option.Value<T["requestBody"]>>;
|
|
||||||
export type QueryType<T extends ApiBase> = S.Schema.Type<O.Option.Value<T["query"]>>;
|
|
||||||
export type QueryEncoded<T extends ApiBase> = S.Schema.Encoded<O.Option.Value<T["query"]>>;
|
|
||||||
export type ResponseType<T extends ApiBase> = S.Schema.Type<O.Option.Value<T["responseBody"]>>;
|
|
||||||
export type ResponseEncoded<T extends ApiBase> = S.Schema.Encoded<O.Option.Value<T["responseBody"]>>;
|
|
||||||
export type Params<T extends ApiBase> = ParamsFromRoute<T["route"]>;
|
|
||||||
|
|
||||||
// --- MAKE API ----------------------------------------------------------------
|
|
||||||
|
|
||||||
export interface MakeApiProps {
|
|
||||||
readonly requestBody?: S.Schema<any, any>;
|
|
||||||
readonly query?: S.Schema<any, any>;
|
|
||||||
readonly responseBody?: S.Schema<any, any>;
|
|
||||||
|
|
||||||
readonly method: Method;
|
|
||||||
readonly route: readonly RouteInputToken[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MakeApi<P extends MakeApiProps> {
|
|
||||||
readonly requestBody: P extends { readonly requestBody: S.Schema<infer A, infer I> } ? O.Some<S.Schema<A, I>> : O.None<never>;
|
|
||||||
readonly query: P extends { readonly query: S.Schema<infer A, infer I> } ? O.Some<S.Schema<A, I>> : O.None<never>;
|
|
||||||
readonly responseBody: P extends { readonly responseBody: S.Schema<infer A, infer I> } ? O.Some<S.Schema<A, I>> : O.None<never>;
|
|
||||||
|
|
||||||
readonly method: P["method"];
|
|
||||||
readonly route: RouteFromInput<P["route"]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function make<const P extends MakeApiProps>(props: P): MakeApi<P> {
|
|
||||||
return Object.freeze<any>({
|
|
||||||
requestBody: O.fromNullable(props.requestBody),
|
|
||||||
query: O.fromNullable(props.query),
|
|
||||||
responseBody: O.fromNullable(props.responseBody),
|
|
||||||
|
|
||||||
method: props.method,
|
|
||||||
route: makeRoute(...props.route),
|
|
||||||
} satisfies ApiBase) as MakeApi<P>;
|
|
||||||
}
|
|
102
src/Fetch.ts
102
src/Fetch.ts
@ -1,102 +0,0 @@
|
|||||||
/// <reference lib="dom" />
|
|
||||||
import * as Effect from "effect/Effect";
|
|
||||||
import { pipe } from "effect/Function";
|
|
||||||
import * as Option from "effect/Option";
|
|
||||||
import * as Predicate from "effect/Predicate";
|
|
||||||
import * as Stream from "effect/Stream";
|
|
||||||
|
|
||||||
export const FetchExceptionTypeId: unique symbol = Symbol.for("effect-api/Fetch/errors/FetchException");
|
|
||||||
export const RequestArrayBufferExceptionTypeId: unique symbol = Symbol.for("effect-api/Fetch/errors/RequestArrayBufferException");
|
|
||||||
|
|
||||||
export type FetchExceptionTypeId = typeof FetchExceptionTypeId;
|
|
||||||
export type RequestArrayBufferExceptionTypeId = typeof RequestArrayBufferExceptionTypeId;
|
|
||||||
|
|
||||||
export interface FetchException extends Error {
|
|
||||||
readonly _tag: "FetchException"
|
|
||||||
readonly [FetchExceptionTypeId]: FetchExceptionTypeId;
|
|
||||||
readonly error: TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestArrayBufferException extends Error {
|
|
||||||
readonly _tag: "RequestArrayBufferException"
|
|
||||||
readonly [RequestArrayBufferExceptionTypeId]: RequestArrayBufferExceptionTypeId;
|
|
||||||
readonly error: RangeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FetchExceptionProto = Object.create(Error.prototype, {
|
|
||||||
_tag: { value: "FetchException" },
|
|
||||||
[FetchExceptionTypeId]: { value: FetchExceptionTypeId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const RequestArrayBufferExceptionProto = Object.create(Error.prototype, {
|
|
||||||
_tag: { value: "RequestArrayBufferException" },
|
|
||||||
[RequestArrayBufferExceptionTypeId]: { value: RequestArrayBufferExceptionTypeId },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const FetchException: (error: TypeError) => FetchException = (error) => Object.create(FetchExceptionProto, {
|
|
||||||
error: { value: error, enumerable: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const RequestArrayBufferException: (error: RangeError) => RequestArrayBufferException = (error) => Object.create(RequestArrayBufferExceptionProto, {
|
|
||||||
error: { value: error, enumerable: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const isFetchException: (u: unknown) => u is FetchException = (u): u is FetchException => Predicate.hasProperty(u, FetchExceptionTypeId);
|
|
||||||
export const isRequestArrayBufferException: (u: unknown) => u is RequestArrayBufferException = (u): u is RequestArrayBufferException => Predicate.hasProperty(u, RequestArrayBufferExceptionTypeId);
|
|
||||||
|
|
||||||
const _wrapResponseHandler: ProxyHandler<Response> = Object.freeze<ProxyHandler<Response>>({
|
|
||||||
get: (target, p, receiver) => {
|
|
||||||
switch (p) {
|
|
||||||
case "body":
|
|
||||||
return <E>(onError: (error: unknown) => E): Option.Option<Stream.Stream<never, E, Uint8Array>> => {
|
|
||||||
return pipe(
|
|
||||||
Reflect.get(target, p, receiver),
|
|
||||||
Option.fromNullable,
|
|
||||||
Option.map(Stream.fromReadableStream())
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const _wrapResponse: (response: Response) => _Response = (response) => {
|
|
||||||
};
|
|
||||||
|
|
||||||
const _fetch: (...args: Parameters<typeof fetch>) => Effect.Effect<never, FetchException, _Response> = (input, init) => {
|
|
||||||
return pipe(
|
|
||||||
Effect.tryPromise({
|
|
||||||
try: (signal) => fetch(input, { signal, ...init }),
|
|
||||||
catch: (error) => {
|
|
||||||
if (error instanceof TypeError) {
|
|
||||||
return FetchException(error);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Effect.map(_wrapResponse),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface _Response {
|
|
||||||
body<E>(onError: (error: unknown) => E): Option.Option<Stream.Stream<never, E, Uint8Array>>;
|
|
||||||
readonly bodyUsed: boolean;
|
|
||||||
arrayBuffer(): Effect.Effect<never, RequestArrayBufferException, ArrayBuffer>;
|
|
||||||
blob(): Effect.Effect<never, ___, Blob>;
|
|
||||||
formData(): Effect.Effect<never, ___, FormData>;
|
|
||||||
json(): Effect.Effect<never, ___, unknown>;
|
|
||||||
text(): Effect.Effect<never, ___, string>;
|
|
||||||
readonly headers: Headers;
|
|
||||||
readonly ok: boolean;
|
|
||||||
readonly redirected: boolean;
|
|
||||||
readonly status: number;
|
|
||||||
readonly statusText: string;
|
|
||||||
readonly type: ResponseType;
|
|
||||||
readonly url: string;
|
|
||||||
clone(): _Response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
_fetch as fetch,
|
|
||||||
type _Request as Request
|
|
||||||
};
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
|||||||
export * as Api from "./Api.js";
|
|
||||||
|
|
||||||
// --- EXPERIMENTS -------------------------------------------------------------
|
|
||||||
|
|
||||||
import * as Api from "./Api.js";
|
|
||||||
import * as S from "@effect/schema/Schema";
|
|
||||||
|
|
||||||
const Id = S.struct({ _id: S.ULID });
|
|
||||||
|
|
||||||
const Product = S.struct({
|
|
||||||
name: S.string,
|
|
||||||
price: S.bigint,
|
|
||||||
});
|
|
||||||
|
|
||||||
const WithId = S.extend(Id);
|
|
||||||
|
|
||||||
const LoginRequest = S.struct({
|
|
||||||
username: S.string,
|
|
||||||
password: S.string,
|
|
||||||
});
|
|
||||||
|
|
||||||
const login = Api.make({
|
|
||||||
method: "POST",
|
|
||||||
route: ["login"],
|
|
||||||
requestBody: LoginRequest,
|
|
||||||
});
|
|
||||||
|
|
||||||
const logout = Api.make({
|
|
||||||
method: "POST",
|
|
||||||
route: ["logout"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getProducts = Api.make({
|
|
||||||
method: "GET",
|
|
||||||
route: ["products"],
|
|
||||||
responseBody: S.array(WithId(Product)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const getProduct = Api.make({
|
|
||||||
method: "GET",
|
|
||||||
route: ["products", ["_id", S.ULID]],
|
|
||||||
responseBody: WithId(Product),
|
|
||||||
});
|
|
||||||
|
|
||||||
const postProduct = Api.make({
|
|
||||||
method: "POST",
|
|
||||||
route: ["products"],
|
|
||||||
requestBody: Product, responseBody: Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const putProduct = Api.make({
|
|
||||||
method: "PUT",
|
|
||||||
route: ["products", ["_id", S.ULID]],
|
|
||||||
requestBody: Product,
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchProduct = Api.make({
|
|
||||||
method: "PATCH",
|
|
||||||
route: ["products", ["_id", S.ULID]],
|
|
||||||
requestBody: S.partial(Product),
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteProduct = Api.make({
|
|
||||||
method: "POST",
|
|
||||||
route: ["products", ["_id", S.ULID]],
|
|
||||||
});
|
|
@ -1,56 +1,61 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
|
||||||
"module": "NodeNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Bundler",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
|
||||||
"outDir": "./dist/",
|
"target": "ESNext",
|
||||||
"rootDir": "./src/",
|
"lib": ["ESNext", "DOM"],
|
||||||
"tsBuildInfoFile": "./.tsbuildinfo",
|
|
||||||
|
|
||||||
"target": "ES2022",
|
"sourceMap": true,
|
||||||
"lib": ["ES2022"],
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"removeComments": false,
|
||||||
|
"newLine": "lf",
|
||||||
|
|
||||||
"sourceMap": true,
|
"noErrorTruncation": true,
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"removeComments": false,
|
|
||||||
"newLine": "lf",
|
|
||||||
|
|
||||||
"noErrorTruncation": true,
|
"strict": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"useUnknownInCatchVariables": true,
|
||||||
|
|
||||||
"strict": true,
|
"noUnusedLocals": true,
|
||||||
"alwaysStrict": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitAny": true,
|
"allowUnusedLabels": false,
|
||||||
"noImplicitThis": true,
|
"allowUnreachableCode": false,
|
||||||
"strictNullChecks": true,
|
|
||||||
"strictBindCallApply": true,
|
|
||||||
"strictFunctionTypes": true,
|
|
||||||
"strictPropertyInitialization": true,
|
|
||||||
"useUnknownInCatchVariables": true,
|
|
||||||
|
|
||||||
"noUnusedLocals": true,
|
"noImplicitOverride": true,
|
||||||
"noUnusedParameters": true,
|
"noImplicitReturns": true,
|
||||||
"allowUnusedLabels": false,
|
|
||||||
"allowUnreachableCode": false,
|
|
||||||
|
|
||||||
"noImplicitOverride": true,
|
"noUncheckedIndexedAccess": false,
|
||||||
"noImplicitReturns": true,
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
"noUncheckedIndexedAccess": false,
|
"allowSyntheticDefaultImports": true,
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
"esModuleInterop": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"isolatedModules": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"importHelpers": false,
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
|
|
||||||
"allowSyntheticDefaultImports": true,
|
"skipLibCheck": false,
|
||||||
"esModuleInterop": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"importHelpers": true,
|
|
||||||
|
|
||||||
"skipLibCheck": false,
|
"paths": {
|
||||||
},
|
"@make-api/api": ["./packages/api/src/index.ts"],
|
||||||
"include": ["./src/**/*"],
|
"@make-api/api/*": ["./packages/api/src/*"],
|
||||||
|
"@make-api/backend": ["./packages/backend/src/index.ts"],
|
||||||
|
"@make-api/backend/*": ["./packages/backend/src/*"],
|
||||||
|
"@make-api/frontend": ["./packages/frontend/src/index.ts"],
|
||||||
|
"@make-api/frontend/*": ["./packages/frontend/src/*"],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user