The rewrite
This commit is contained in:
		| @@ -9,5 +9,10 @@ insert_final_newline = true | ||||
| tab_width = 4 | ||||
| trim_trailing_whitespace = true | ||||
|  | ||||
| [*.json] | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
| tab_width = 2 | ||||
|  | ||||
| [*.md] | ||||
| trim_trailing_whitespace = false | ||||
|   | ||||
| @@ -1,2 +1,4 @@ | ||||
| # 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. | ||||
|   | ||||
							
								
								
									
										45
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,41 +1,26 @@ | ||||
| { | ||||
| 	"name": "effect-api", | ||||
| 	"version": "0.1.0", | ||||
| 	"description": "A library for defining, using and implementing HTTP JSON APIs using Effect ecosystem", | ||||
| 	"keywords": [ | ||||
| 		"api", | ||||
| 		"effect", | ||||
| 		"fetch", | ||||
| 		"http", | ||||
| 		"json" | ||||
| 	], | ||||
|   "private": true, | ||||
|   "type": "module", | ||||
|   "author": { | ||||
|     "name": "Szymon Nowakowski", | ||||
|     "email": "renati@renati.me", | ||||
|     "url": "https://renati.me/" | ||||
|   }, | ||||
|   "homepage": "https://renati.me/effect-api", | ||||
|   "license": "MPL-2.0", | ||||
| 	"browser": "./dist/oktaeder.js", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "https://gitea.renati.me/renati/effect-api.git" | ||||
|   }, | ||||
| 	"scripts": { | ||||
| 		"build": "tsc --build" | ||||
|   "workspaces": [ | ||||
|     "./packages/*" | ||||
|   ], | ||||
|   "devDependencies": { | ||||
|     "typescript": "^5.5.2" | ||||
|   }, | ||||
|   "dependencies": { | ||||
| 		"@effect/schema": "^0.64.3", | ||||
| 		"effect": "^2.4.6", | ||||
| 		"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" | ||||
| 		} | ||||
|     "@effect/schema": "^0.68.9", | ||||
|     "effect": "^3.4.2", | ||||
|     "fast-check": "^3.19.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										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]], | ||||
| }); | ||||
| @@ -3,15 +3,12 @@ | ||||
|     "incremental": true, | ||||
|     "composite": true, | ||||
|  | ||||
| 		"module": "NodeNext", | ||||
| 		"moduleResolution": "NodeNext", | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "Bundler", | ||||
|     "moduleDetection": "force", | ||||
|  | ||||
| 		"outDir": "./dist/", | ||||
| 		"rootDir": "./src/", | ||||
| 		"tsBuildInfoFile": "./.tsbuildinfo", | ||||
|  | ||||
| 		"target": "ES2022", | ||||
| 		"lib": ["ES2022"], | ||||
|     "target": "ESNext", | ||||
|     "lib": ["ESNext", "DOM"], | ||||
|  | ||||
|     "sourceMap": true, | ||||
|     "declaration": true, | ||||
| @@ -48,9 +45,17 @@ | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "esModuleInterop": true, | ||||
|     "isolatedModules": true, | ||||
| 		"importHelpers": true, | ||||
|     "importHelpers": false, | ||||
|  | ||||
|     "skipLibCheck": false, | ||||
| 	}, | ||||
| 	"include": ["./src/**/*"], | ||||
|  | ||||
|     "paths": { | ||||
|       "@make-api/api": ["./packages/api/src/index.ts"], | ||||
|       "@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/*"], | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user