Make API
This commit is contained in:
		
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -20,18 +20,22 @@ | ||||
| 		"build": "tsc --build" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@effect/schema": "^0.51.1", | ||||
| 		"effect": "2.0.0-next.58", | ||||
| 		"fast-check": "^3.14.0", | ||||
| 		"@effect/schema": "^0.64.3", | ||||
| 		"effect": "^2.4.6", | ||||
| 		"fast-check": "^3.16.0", | ||||
| 		"tslib": "^2.6.2" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"typescript": "^5.3.2" | ||||
| 		"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" | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										38
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										38
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -6,41 +6,41 @@ settings: | ||||
|  | ||||
| dependencies: | ||||
|   '@effect/schema': | ||||
|     specifier: ^0.51.1 | ||||
|     version: 0.51.1(effect@2.0.0-next.58)(fast-check@3.14.0) | ||||
|     specifier: ^0.64.3 | ||||
|     version: 0.64.3(effect@2.4.6)(fast-check@3.16.0) | ||||
|   effect: | ||||
|     specifier: 2.0.0-next.58 | ||||
|     version: 2.0.0-next.58 | ||||
|     specifier: ^2.4.6 | ||||
|     version: 2.4.6 | ||||
|   fast-check: | ||||
|     specifier: ^3.14.0 | ||||
|     version: 3.14.0 | ||||
|     specifier: ^3.16.0 | ||||
|     version: 3.16.0 | ||||
|   tslib: | ||||
|     specifier: ^2.6.2 | ||||
|     version: 2.6.2 | ||||
|  | ||||
| devDependencies: | ||||
|   typescript: | ||||
|     specifier: ^5.3.2 | ||||
|     version: 5.3.2 | ||||
|     specifier: ^5.4.2 | ||||
|     version: 5.4.2 | ||||
|  | ||||
| packages: | ||||
|  | ||||
|   /@effect/schema@0.51.1(effect@2.0.0-next.58)(fast-check@3.14.0): | ||||
|     resolution: {integrity: sha512-ZuULzdmAUTtdC/OFaVsEtc7NUCCElYYBE6LJSc9sYG8sDQisA5AQnWFsRk6MZPxzD2EdpTlKgQE0gysn14sttA==} | ||||
|   /@effect/schema@0.64.3(effect@2.4.6)(fast-check@3.16.0): | ||||
|     resolution: {integrity: sha512-Pn/IG+X9GTXLVvDtdjptr3QWxpcH+m7LEgM6loLWk7DP8SPKqWdo3rFaZiILpJFci8QWEk7K23C3IJZOnMfTtA==} | ||||
|     peerDependencies: | ||||
|       effect: 2.0.0-next.58 | ||||
|       effect: ^2.4.6 | ||||
|       fast-check: ^3.13.2 | ||||
|     dependencies: | ||||
|       effect: 2.0.0-next.58 | ||||
|       fast-check: 3.14.0 | ||||
|       effect: 2.4.6 | ||||
|       fast-check: 3.16.0 | ||||
|     dev: false | ||||
|  | ||||
|   /effect@2.0.0-next.58: | ||||
|     resolution: {integrity: sha512-3JtlhJWIZlKe2krJp8ZsKCWCsKMemRVMNOGKtW6bLbg1ZYDi9riuDO44heqEyh7mVBhGsLh/Ut+Hz4olFbfhyg==} | ||||
|   /effect@2.4.6: | ||||
|     resolution: {integrity: sha512-gaZC+aopWvS67SX8RFT3mirabjpVBk5qshj/Z3IKW35DjzpQ8WhAE7VWwfBBWWMBuQ/B0RsbzKR8YGN2AivIgQ==} | ||||
|     dev: false | ||||
|  | ||||
|   /fast-check@3.14.0: | ||||
|     resolution: {integrity: sha512-9Z0zqASzDNjXBox/ileV/fd+4P+V/f3o4shM6QawvcdLFh8yjPG4h5BrHUZ8yzY6amKGDTAmRMyb/JZqe+dCgw==} | ||||
|   /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 | ||||
| @@ -54,8 +54,8 @@ packages: | ||||
|     resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} | ||||
|     dev: false | ||||
|  | ||||
|   /typescript@5.3.2: | ||||
|     resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} | ||||
|   /typescript@5.4.2: | ||||
|     resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} | ||||
|     engines: {node: '>=14.17'} | ||||
|     hasBin: true | ||||
|     dev: true | ||||
|   | ||||
							
								
								
									
										147
									
								
								src/Api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/Api.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/Fetch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| /// <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 | ||||
| }; | ||||
|  | ||||
							
								
								
									
										66
									
								
								src/effect-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/effect-api.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| 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]], | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user