The rewrite
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								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", |  | ||||||
| 		"json" |  | ||||||
| 	], |  | ||||||
|   "homepage": "https://renati.me/effect-api", |   "homepage": "https://renati.me/effect-api", | ||||||
|   "license": "MPL-2.0", |   "license": "MPL-2.0", | ||||||
| 	"browser": "./dist/oktaeder.js", |  | ||||||
|   "repository": { |   "repository": { | ||||||
|     "type": "git", |     "type": "git", | ||||||
|     "url": "https://gitea.renati.me/renati/effect-api.git" |     "url": "https://gitea.renati.me/renati/effect-api.git" | ||||||
|   }, |   }, | ||||||
| 	"scripts": { |   "workspaces": [ | ||||||
| 		"build": "tsc --build" |     "./packages/*" | ||||||
|  |   ], | ||||||
|  |   "devDependencies": { | ||||||
|  |     "typescript": "^5.5.2" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| 		"@effect/schema": "^0.64.3", |     "@effect/schema": "^0.68.9", | ||||||
| 		"effect": "^2.4.6", |     "effect": "^3.4.2", | ||||||
| 		"fast-check": "^3.16.0", |     "fast-check": "^3.19.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]], |  | ||||||
| }); |  | ||||||
| @@ -3,15 +3,12 @@ | |||||||
|     "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", |  | ||||||
| 		"lib": ["ES2022"], |  | ||||||
|  |  | ||||||
|     "sourceMap": true, |     "sourceMap": true, | ||||||
|     "declaration": true, |     "declaration": true, | ||||||
| @@ -48,9 +45,17 @@ | |||||||
|     "allowSyntheticDefaultImports": true, |     "allowSyntheticDefaultImports": true, | ||||||
|     "esModuleInterop": true, |     "esModuleInterop": true, | ||||||
|     "isolatedModules": true, |     "isolatedModules": true, | ||||||
| 		"importHelpers": true, |     "importHelpers": false, | ||||||
|  |  | ||||||
|     "skipLibCheck": 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