@b3-business/cherry 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/dist/index.d.ts +12 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/src/cherry_client.ts +172 -0
- package/src/errors.ts +86 -0
- package/src/index.ts +32 -0
- package/src/path.ts +70 -0
- package/src/route.ts +63 -0
- package/src/types.ts +107 -0
package/README.md
CHANGED
|
@@ -10,12 +10,9 @@ A tree-shakeable, minimal API client factory. Import only the routes you need
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
## Latest Changelog - 0.3.
|
|
13
|
+
## Latest Changelog - 0.3.2
|
|
14
14
|
|
|
15
|
-
- Fix:
|
|
16
|
-
- Fix: Allow omitting empty object for calls that don't require params (e.g., `client.listPosts()` instead of `client.listPosts({})`)
|
|
17
|
-
- Fix: Autocomplete for routes now works correctly in IDE
|
|
18
|
-
- Add comprehensive JSDoc comments to types.ts
|
|
15
|
+
- Fix: Ship TypeScript source files in npm package to fix module resolution
|
|
19
16
|
|
|
20
17
|
See [CHANGELOG.md](https://github.com/b3-business/cherry/blob/main/packages/cherry/CHANGELOG.md) for full history.
|
|
21
18
|
|
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ declare function isCherryError(error: unknown): error is CherryError;
|
|
|
54
54
|
declare function cherryErr<T>(error: CherryError): ResultAsync<T, CherryError>;
|
|
55
55
|
//#endregion
|
|
56
56
|
//#region src/types.d.ts
|
|
57
|
+
type AnySchema = BaseSchema<any, any, any>;
|
|
57
58
|
/** HTTP methods supported by Cherry */
|
|
58
59
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
59
60
|
/** Path template result from path() tagged template */
|
|
@@ -62,7 +63,7 @@ type PathTemplate = {
|
|
|
62
63
|
paramNames: string[];
|
|
63
64
|
};
|
|
64
65
|
/** Route definition with separated parameter schemas */
|
|
65
|
-
type CherryRoute<TPathParams extends
|
|
66
|
+
type CherryRoute<TPathParams extends AnySchema | undefined = undefined, TQueryParams extends AnySchema | undefined = undefined, TBodyParams extends AnySchema | undefined = undefined, TResponse extends AnySchema = AnySchema> = {
|
|
66
67
|
method: HttpMethod;
|
|
67
68
|
path: PathTemplate;
|
|
68
69
|
pathParams?: TPathParams;
|
|
@@ -77,17 +78,18 @@ type QueryParamOptions = {
|
|
|
77
78
|
arrayFormat?: "repeat" | "comma" | "brackets" | "json";
|
|
78
79
|
customSerializer?: (params: Record<string, unknown>) => string;
|
|
79
80
|
};
|
|
81
|
+
type AnyCherryRoute = CherryRoute<any, any, any, any>;
|
|
80
82
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
81
|
-
type
|
|
82
|
-
type InferSchemaInput<T> =
|
|
83
|
+
type IsEmptyObject<T> = keyof T extends never ? true : false;
|
|
84
|
+
type InferSchemaInput<T> = T extends AnySchema ? InferInput<T> : {};
|
|
83
85
|
type BuildRouteInputFromProps<TPathParams, TQueryParams, TBodyParams> = InferSchemaInput<TPathParams> & InferSchemaInput<TQueryParams> & InferSchemaInput<TBodyParams>;
|
|
84
86
|
type InferRouteInput<T> = T extends {
|
|
85
87
|
pathParams?: infer TPath;
|
|
86
88
|
queryParams?: infer TQuery;
|
|
87
89
|
bodyParams?: infer TBody;
|
|
88
|
-
} ?
|
|
90
|
+
} ? IsEmptyObject<BuildRouteInputFromProps<TPath, TQuery, TBody>> extends true ? void : Prettify<BuildRouteInputFromProps<TPath, TQuery, TBody>> : never;
|
|
89
91
|
/** Infer response output from a route */
|
|
90
|
-
type InferRouteOutput<T extends
|
|
92
|
+
type InferRouteOutput<T extends AnyCherryRoute> = T["response"] extends AnySchema ? InferOutput<T["response"]> : never;
|
|
91
93
|
/** Cherry result type - always ResultAsync */
|
|
92
94
|
type CherryResult<T> = ResultAsync<T, CherryError>;
|
|
93
95
|
/** Fetcher request shape (extensible for middleware) */
|
|
@@ -99,7 +101,7 @@ type FetchRequest = {
|
|
|
99
101
|
type Fetcher = (req: FetchRequest) => Promise<Response>;
|
|
100
102
|
/** Route tree (supports namespacing via nested objects) */
|
|
101
103
|
type RouteTree = {
|
|
102
|
-
[key: string]:
|
|
104
|
+
[key: string]: AnyCherryRoute | RouteTree;
|
|
103
105
|
};
|
|
104
106
|
/** Client configuration */
|
|
105
107
|
type ClientConfig<TRoutes extends RouteTree | undefined = undefined> = {
|
|
@@ -109,27 +111,17 @@ type ClientConfig<TRoutes extends RouteTree | undefined = undefined> = {
|
|
|
109
111
|
routes?: TRoutes;
|
|
110
112
|
};
|
|
111
113
|
type Client<TRoutes extends RouteTree | undefined = undefined> = {
|
|
112
|
-
call: <T extends
|
|
114
|
+
call: <T extends AnyCherryRoute>(route: T, ...args: InferRouteInput<T> extends void ? [] : [params: InferRouteInput<T>]) => CherryResult<InferRouteOutput<T>>;
|
|
113
115
|
} & (TRoutes extends RouteTree ? RoutesToClient<TRoutes> : {});
|
|
114
116
|
/** Convert a nested route tree into a nested client method tree */
|
|
115
|
-
type RoutesToClient<TRoutes extends RouteTree> = { [K in keyof TRoutes]: TRoutes[K] extends
|
|
117
|
+
type RoutesToClient<TRoutes extends RouteTree> = { [K in keyof TRoutes]: TRoutes[K] extends AnyCherryRoute ? InferRouteInput<TRoutes[K]> extends void ? () => CherryResult<InferRouteOutput<TRoutes[K]>> : (params: InferRouteInput<TRoutes[K]>) => CherryResult<InferRouteOutput<TRoutes[K]>> : TRoutes[K] extends RouteTree ? RoutesToClient<TRoutes[K]> : never };
|
|
116
118
|
//#endregion
|
|
117
119
|
//#region src/cherry_client.d.ts
|
|
118
120
|
declare function serializeQueryParams(params: Record<string, unknown>, options?: QueryParamOptions): string;
|
|
119
121
|
declare function createCherryClient<TRoutes extends RouteTree | undefined = undefined>(config: ClientConfig<TRoutes>): Client<TRoutes>;
|
|
120
122
|
//#endregion
|
|
121
123
|
//#region src/route.d.ts
|
|
122
|
-
|
|
123
|
-
method: HttpMethod;
|
|
124
|
-
path: PathTemplate;
|
|
125
|
-
pathParams?: TPathParams;
|
|
126
|
-
queryParams?: TQueryParams;
|
|
127
|
-
bodyParams?: TBodyParams;
|
|
128
|
-
response: TResponse;
|
|
129
|
-
queryParamOptions?: QueryParamOptions;
|
|
130
|
-
description?: string;
|
|
131
|
-
};
|
|
132
|
-
declare function route<TPathParams extends v.BaseSchema<any, any, any> | undefined = undefined, TQueryParams extends v.BaseSchema<any, any, any> | undefined = undefined, TBodyParams extends v.BaseSchema<any, any, any> | undefined = undefined, TResponse extends v.BaseSchema<any, any, any> = v.BaseSchema<any, any, any>>(config: RouteConfig<TPathParams, TQueryParams, TBodyParams, TResponse>): CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse>;
|
|
124
|
+
declare function route<TPathParams extends v.BaseSchema<any, any, any> | undefined = undefined, TQueryParams extends v.BaseSchema<any, any, any> | undefined = undefined, TBodyParams extends v.BaseSchema<any, any, any> | undefined = undefined, TResponse extends v.BaseSchema<any, any, any> = v.BaseSchema<any, any, any>>(config: CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse>): CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse>;
|
|
133
125
|
//#endregion
|
|
134
126
|
//#region src/path.d.ts
|
|
135
127
|
/** Branded type for path parameter markers */
|
|
@@ -172,5 +164,5 @@ declare function optional<T extends string>(name: T): OptionalParam<T>;
|
|
|
172
164
|
*/
|
|
173
165
|
declare function path(strings: TemplateStringsArray, ...params: AnyPathParam[]): PathTemplate;
|
|
174
166
|
//#endregion
|
|
175
|
-
export { type AnyPathParam, CherryError, type CherryResult, type CherryRoute, type Client, type ClientConfig, type FetchRequest, type Fetcher, HttpError, type HttpMethod, type InferRouteInput, type InferRouteOutput, NetworkError, type OptionalParam, type PathParam, type PathTemplate, type QueryParamOptions, type
|
|
167
|
+
export { type AnyPathParam, CherryError, type CherryResult, type CherryRoute, type Client, type ClientConfig, type FetchRequest, type Fetcher, HttpError, type HttpMethod, type InferRouteInput, type InferRouteOutput, NetworkError, type OptionalParam, type PathParam, type PathTemplate, type QueryParamOptions, type RouteTree, type RoutesToClient, SerializationError, UnknownCherryError, ValidationError, cherryErr, createCherryClient, isCherryError, optional, param, path, route, serializeQueryParams };
|
|
176
168
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/cherry_client.ts","../src/route.ts","../src/path.ts"],"sourcesContent":[],"mappings":";;;;;;uBAGsB,WAAA,SAAoB,KAAA;;;EAApB,WAAA,CAAA,OAAY,EAAA,MAAQ,EAAA,OA2B7B,CA3BkC,EAAA;IAWlC,KAAA,CAAA,EAAU,OAAA;EAgBV,CAAA;AAcb;AAUA;AAca,cAtDA,SAAA,SAAkB,WAAA,CAsDoB;EAUnC,SAAA,MAAA,EAAa,MAAA;EAKb,SAAA,UAAS,EAAA,MAAA;EAAW,SAAA,IAAA,CAAA,EAAA,OAAA,GAAA,SAAA;EAA0B,SAAA,IAAA,GAAA,WAAA;EAAG,SAAA,SAAA,EAAA,OAAA;EAAf,WAAA,CAAA,MAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,IAAA,CAAA,EAAA,OAAA,GAAA,SAAA,EAAA,KAAA,CAAA,EAAA,OAAA;;;cArDrC,eAAA,SAAwB,WAAA;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/cherry_client.ts","../src/route.ts","../src/path.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["route","result"],"sources":["../src/errors.ts","../src/cherry_client.ts","../src/route.ts","../src/path.ts"],"sourcesContent":["import { errAsync, ResultAsync } from \"neverthrow\";\n\n/** Base error class for all Cherry errors */\nexport abstract class CherryError extends Error {\n abstract readonly type: string;\n abstract readonly retryable: boolean;\n\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = this.constructor.name;\n }\n}\n\n/** HTTP response errors (4xx, 5xx) */\nexport class HttpError extends CherryError {\n readonly type = \"HttpError\";\n readonly retryable: boolean;\n\n constructor(\n public readonly status: number,\n public readonly statusText: string,\n public readonly body?: unknown,\n cause?: unknown,\n ) {\n super(`HTTP ${status}: ${statusText}`, { cause });\n this.retryable = status >= 500 || status === 429;\n }\n}\n\n/** Valibot validation errors */\nexport class ValidationError extends CherryError {\n readonly type = \"ValidationError\";\n readonly retryable = false;\n\n constructor(\n public readonly target: \"request\" | \"response\",\n public readonly issues: unknown[],\n cause?: unknown,\n ) {\n super(`Validation failed for ${target}`, { cause });\n }\n}\n\n/** Network/fetch errors */\nexport class NetworkError extends CherryError {\n readonly type = \"NetworkError\";\n readonly retryable = true;\n\n constructor(cause?: unknown) {\n super(`Network error`, { cause });\n }\n}\n\n/** Serialization errors (e.g., circular references, BigInt in JSON) */\nexport class SerializationError extends CherryError {\n readonly type = \"SerializationError\";\n readonly retryable = false;\n\n constructor(\n public readonly target: \"query\" | \"body\",\n public readonly key: string,\n cause?: unknown,\n ) {\n super(`Failed to serialize ${target} parameter \"${key}\"`, { cause });\n }\n}\n\n/** Catch-all for unexpected errors */\nexport class UnknownCherryError extends CherryError {\n readonly type = \"UnknownCherryError\";\n readonly retryable = false;\n\n constructor(cause?: unknown) {\n super(`Unknown error`, { cause });\n }\n}\n\n/** Type guard for CherryError */\nexport function isCherryError(error: unknown): error is CherryError {\n return error instanceof CherryError;\n}\n\n/** Helper to create error ResultAsync */\nexport function cherryErr<T>(error: CherryError): ResultAsync<T, CherryError> {\n return errAsync(error);\n}\n","import { ResultAsync } from \"neverthrow\";\nimport * as v from \"valibot\";\nimport type {\n CherryRoute,\n CherryResult,\n InferRouteInput,\n InferRouteOutput,\n Fetcher,\n FetchRequest,\n ClientConfig,\n Client,\n RouteTree,\n RoutesToClient,\n QueryParamOptions,\n} from \"./types\";\nimport { HttpError, ValidationError, NetworkError, SerializationError, UnknownCherryError } from \"./errors\";\n\nconst defaultFetcher: Fetcher = (req) => fetch(req.url, req.init);\n\nexport function serializeQueryParams(\n params: Record<string, unknown>,\n options?: QueryParamOptions,\n): string {\n if (options?.customSerializer) {\n return options.customSerializer(params);\n }\n\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n switch (options?.arrayFormat ?? \"repeat\") {\n case \"repeat\":\n for (const item of value) {\n searchParams.append(key, String(item));\n }\n break;\n case \"comma\":\n searchParams.set(key, value.join(\",\"));\n break;\n case \"brackets\":\n for (const item of value) {\n searchParams.append(`${key}[]`, String(item));\n }\n break;\n case \"json\":\n try {\n searchParams.set(key, JSON.stringify(value));\n } catch (error) {\n throw new SerializationError(\"query\", key, error);\n }\n break;\n }\n } else {\n searchParams.set(key, String(value));\n }\n }\n\n return searchParams.toString();\n}\n\nexport function createCherryClient<TRoutes extends RouteTree | undefined = undefined>(\n config: ClientConfig<TRoutes>,\n): Client<TRoutes> {\n const fetcher = config.fetcher ?? defaultFetcher;\n\n function call<T extends CherryRoute<any, any, any, any>>(\n route: T,\n params: InferRouteInput<T>,\n ): CherryResult<InferRouteOutput<T>> {\n return ResultAsync.fromPromise(executeRoute(route, params), (error) => {\n if (error instanceof HttpError) return error;\n if (error instanceof ValidationError) return error;\n if (error instanceof NetworkError) return error;\n if (error instanceof SerializationError) return error;\n return new UnknownCherryError(error);\n });\n }\n\n async function executeRoute<T extends CherryRoute<any, any, any, any>>(\n route: T,\n params: InferRouteInput<T>,\n ): Promise<InferRouteOutput<T>> {\n let pathParams: Record<string, unknown> = {};\n if (route.pathParams) {\n const result = v.safeParse(route.pathParams, params);\n if (!result.success) throw new ValidationError(\"request\", result.issues);\n pathParams = result.output;\n }\n\n let queryParams: Record<string, unknown> = {};\n if (route.queryParams) {\n const result = v.safeParse(route.queryParams, params);\n if (!result.success) throw new ValidationError(\"request\", result.issues);\n queryParams = result.output;\n }\n\n let bodyParams: Record<string, unknown> | undefined;\n if (route.bodyParams) {\n const result = v.safeParse(route.bodyParams, params);\n if (!result.success) throw new ValidationError(\"request\", result.issues);\n bodyParams = result.output;\n }\n\n let url = route.path.template;\n for (const [key, value] of Object.entries(pathParams)) {\n url = url.replace(`:${key}`, encodeURIComponent(String(value)));\n }\n\n const fullUrl = new URL(url, config.baseUrl);\n\n if (Object.keys(queryParams).length > 0) {\n fullUrl.search = serializeQueryParams(queryParams, route.queryParamOptions);\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(await config.headers?.()),\n };\n\n const init: RequestInit = {\n method: route.method,\n headers,\n };\n\n if (bodyParams && route.method !== \"GET\") {\n init.body = JSON.stringify(bodyParams);\n }\n\n const req: FetchRequest = {\n url: fullUrl.toString(),\n init,\n };\n\n let response: Response;\n try {\n response = await fetcher(req);\n } catch (error) {\n throw new NetworkError(error);\n }\n\n if (!response.ok) {\n const body = await response.text().catch(() => undefined);\n throw new HttpError(response.status, response.statusText, body);\n }\n\n const json = await response.json();\n const result = v.safeParse(route.response, json);\n if (!result.success) throw new ValidationError(\"response\", result.issues);\n\n return result.output;\n }\n\n function forgeRouteMethods<T extends RouteTree>(routes: T): RoutesToClient<T> {\n const out: any = {};\n\n for (const [key, value] of Object.entries(routes)) {\n if (value && typeof value === \"object\" && \"method\" in value && \"path\" in value) {\n out[key] = (params: any) => call(value as any, params);\n } else if (value && typeof value === \"object\") {\n out[key] = forgeRouteMethods(value as RouteTree);\n }\n }\n\n return out;\n }\n\n const routes = config.routes ? forgeRouteMethods(config.routes) : {};\n return { call, ...routes } as Client<TRoutes>;\n}\n","import * as v from \"valibot\";\nimport type {\n CherryRoute,\n HttpMethod,\n PathTemplate,\n QueryParamOptions,\n} from \"./types\";\n\nconst HttpMethodSchema = v.picklist([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\nexport type RouteConfig<\n TPathParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TQueryParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TBodyParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TResponse extends v.BaseSchema<any, any, any> = v.BaseSchema<any, any, any>,\n> = {\n method: HttpMethod;\n path: PathTemplate;\n pathParams?: TPathParams;\n queryParams?: TQueryParams;\n bodyParams?: TBodyParams;\n response: TResponse;\n queryParamOptions?: QueryParamOptions;\n description?: string;\n};\n\nexport function route<\n TPathParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TQueryParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TBodyParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TResponse extends v.BaseSchema<any, any, any> = v.BaseSchema<any, any, any>,\n>(\n config: RouteConfig<TPathParams, TQueryParams, TBodyParams, TResponse>,\n): CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse> {\n // validate HTTP method\n v.parse(HttpMethodSchema, config.method);\n\n // validate availability and content of pathParams schema, if needed\n if (config.path.paramNames.length > 0) {\n if (!config.pathParams) {\n throw new Error(\n `Route has path params [${config.path.paramNames.join(\", \")}] but no pathParams schema`,\n );\n }\n\n const schemaKeys = getSchemaKeys(config.pathParams);\n\n for (const paramName of config.path.paramNames) {\n if (!schemaKeys.includes(paramName)) {\n throw new Error(\n `Path param \":${paramName}\" not found in pathParams schema. ` +\n `Available: [${schemaKeys.join(\", \")}]`,\n );\n }\n }\n\n for (const schemaKey of schemaKeys) {\n if (!config.path.paramNames.includes(schemaKey)) {\n throw new Error(\n `pathParams schema key \"${schemaKey}\" not present in path template. ` +\n `Template params: [${config.path.paramNames.join(\", \")}]`,\n );\n }\n }\n }\n\n return config as CherryRoute<\n TPathParams,\n TQueryParams,\n TBodyParams,\n TResponse\n >;\n}\n\nfunction getSchemaKeys(schema: v.BaseSchema<any, any, any>): string[] {\n if (\n \"entries\" in schema &&\n typeof (schema as any).entries === \"object\" &&\n (schema as any).entries !== null\n ) {\n return Object.keys((schema as any).entries);\n }\n return [];\n}\n","// path.ts - Tagged template functions for type-safe path building\nimport type { PathTemplate } from \"./types\";\n\n/** Branded type for path parameter markers */\ndeclare const PathParamBrand: unique symbol;\nexport type PathParam<T extends string = string> = string & {\n readonly [PathParamBrand]: T;\n};\n\n/** Branded type for optional path parameter markers */\ndeclare const OptionalParamBrand: unique symbol;\nexport type OptionalParam<T extends string = string> = string & {\n readonly [OptionalParamBrand]: T;\n};\n\n/** Union type for any path param marker */\nexport type AnyPathParam = PathParam<string> | OptionalParam<string>;\n\n/** Create a path parameter marker */\nexport function param<T extends string>(name: T): PathParam<T> {\n return `:${name}` as PathParam<T>;\n}\n\n/** Create an optional path parameter marker */\nexport function optional<T extends string>(name: T): OptionalParam<T> {\n return `(:${name})` as OptionalParam<T>;\n}\n\n/**\n * Tagged template for building path templates.\n *\n * @example\n * ```ts\n * // Simple path with one param\n * const userPath = path`/users/${param(\"id\")}`;\n * // { template: \"/users/:id\", paramNames: [\"id\"] }\n *\n * // Multiple params\n * const postPath = path`/users/${param(\"userId\")}/posts/${param(\"postId\")}`;\n * // { template: \"/users/:userId/posts/:postId\", paramNames: [\"userId\", \"postId\"] }\n *\n * // Optional params\n * const versionedPath = path`/api${optional(\"version\")}/users`;\n * // { template: \"/api(:version)/users\", paramNames: [\"version\"] }\n *\n * // No params\n * const staticPath = path`/health`;\n * // { template: \"/health\", paramNames: [] }\n * ```\n */\nexport function path(\n strings: TemplateStringsArray,\n ...params: AnyPathParam[]\n): PathTemplate {\n const paramNames: string[] = [];\n let template = strings[0];\n\n for (let i = 0; i < params.length; i++) {\n const p = params[i];\n template += p + strings[i + 1];\n\n // Extract param name from `:name` or `(:name)`\n const match = p.match(/^\\(?:(\\w+)\\)?$/);\n if (match) {\n paramNames.push(match[1]);\n }\n }\n\n return { template, paramNames };\n}\n"],"mappings":";;;;;AAGA,IAAsB,cAAtB,cAA0C,MAAM;CAI9C,YAAY,SAAiB,SAA+B;AAC1D,QAAM,SAAS,QAAQ;AACvB,OAAK,OAAO,KAAK,YAAY;;;;AAKjC,IAAa,YAAb,cAA+B,YAAY;CACzC,AAAS,OAAO;CAChB,AAAS;CAET,YACE,AAAgB,QAChB,AAAgB,YAChB,AAAgB,MAChB,OACA;AACA,QAAM,QAAQ,OAAO,IAAI,cAAc,EAAE,OAAO,CAAC;EALjC;EACA;EACA;AAIhB,OAAK,YAAY,UAAU,OAAO,WAAW;;;;AAKjD,IAAa,kBAAb,cAAqC,YAAY;CAC/C,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YACE,AAAgB,QAChB,AAAgB,QAChB,OACA;AACA,QAAM,yBAAyB,UAAU,EAAE,OAAO,CAAC;EAJnC;EACA;;;;AAQpB,IAAa,eAAb,cAAkC,YAAY;CAC5C,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YAAY,OAAiB;AAC3B,QAAM,iBAAiB,EAAE,OAAO,CAAC;;;;AAKrC,IAAa,qBAAb,cAAwC,YAAY;CAClD,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YACE,AAAgB,QAChB,AAAgB,KAChB,OACA;AACA,QAAM,uBAAuB,OAAO,cAAc,IAAI,IAAI,EAAE,OAAO,CAAC;EAJpD;EACA;;;;AAQpB,IAAa,qBAAb,cAAwC,YAAY;CAClD,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YAAY,OAAiB;AAC3B,QAAM,iBAAiB,EAAE,OAAO,CAAC;;;;AAKrC,SAAgB,cAAc,OAAsC;AAClE,QAAO,iBAAiB;;;AAI1B,SAAgB,UAAa,OAAiD;AAC5E,QAAO,SAAS,MAAM;;;;;ACnExB,MAAM,kBAA2B,QAAQ,MAAM,IAAI,KAAK,IAAI,KAAK;AAEjE,SAAgB,qBACd,QACA,SACQ;AACR,KAAI,SAAS,iBACX,QAAO,QAAQ,iBAAiB,OAAO;CAGzC,MAAM,eAAe,IAAI,iBAAiB;AAE1C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,EAAE;AACjD,MAAI,UAAU,UAAa,UAAU,KAAM;AAE3C,MAAI,MAAM,QAAQ,MAAM,CACtB,SAAQ,SAAS,eAAe,UAAhC;GACE,KAAK;AACH,SAAK,MAAM,QAAQ,MACjB,cAAa,OAAO,KAAK,OAAO,KAAK,CAAC;AAExC;GACF,KAAK;AACH,iBAAa,IAAI,KAAK,MAAM,KAAK,IAAI,CAAC;AACtC;GACF,KAAK;AACH,SAAK,MAAM,QAAQ,MACjB,cAAa,OAAO,GAAG,IAAI,KAAK,OAAO,KAAK,CAAC;AAE/C;GACF,KAAK;AACH,QAAI;AACF,kBAAa,IAAI,KAAK,KAAK,UAAU,MAAM,CAAC;aACrC,OAAO;AACd,WAAM,IAAI,mBAAmB,SAAS,KAAK,MAAM;;AAEnD;;MAGJ,cAAa,IAAI,KAAK,OAAO,MAAM,CAAC;;AAIxC,QAAO,aAAa,UAAU;;AAGhC,SAAgB,mBACd,QACiB;CACjB,MAAM,UAAU,OAAO,WAAW;CAElC,SAAS,KACP,SACA,QACmC;AACnC,SAAO,YAAY,YAAY,aAAaA,SAAO,OAAO,GAAG,UAAU;AACrE,OAAI,iBAAiB,UAAW,QAAO;AACvC,OAAI,iBAAiB,gBAAiB,QAAO;AAC7C,OAAI,iBAAiB,aAAc,QAAO;AAC1C,OAAI,iBAAiB,mBAAoB,QAAO;AAChD,UAAO,IAAI,mBAAmB,MAAM;IACpC;;CAGJ,eAAe,aACb,SACA,QAC8B;EAC9B,IAAI,aAAsC,EAAE;AAC5C,MAAIA,QAAM,YAAY;GACpB,MAAMC,WAAS,EAAE,UAAUD,QAAM,YAAY,OAAO;AACpD,OAAI,CAACC,SAAO,QAAS,OAAM,IAAI,gBAAgB,WAAWA,SAAO,OAAO;AACxE,gBAAaA,SAAO;;EAGtB,IAAI,cAAuC,EAAE;AAC7C,MAAID,QAAM,aAAa;GACrB,MAAMC,WAAS,EAAE,UAAUD,QAAM,aAAa,OAAO;AACrD,OAAI,CAACC,SAAO,QAAS,OAAM,IAAI,gBAAgB,WAAWA,SAAO,OAAO;AACxE,iBAAcA,SAAO;;EAGvB,IAAI;AACJ,MAAID,QAAM,YAAY;GACpB,MAAMC,WAAS,EAAE,UAAUD,QAAM,YAAY,OAAO;AACpD,OAAI,CAACC,SAAO,QAAS,OAAM,IAAI,gBAAgB,WAAWA,SAAO,OAAO;AACxE,gBAAaA,SAAO;;EAGtB,IAAI,MAAMD,QAAM,KAAK;AACrB,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,CACnD,OAAM,IAAI,QAAQ,IAAI,OAAO,mBAAmB,OAAO,MAAM,CAAC,CAAC;EAGjE,MAAM,UAAU,IAAI,IAAI,KAAK,OAAO,QAAQ;AAE5C,MAAI,OAAO,KAAK,YAAY,CAAC,SAAS,EACpC,SAAQ,SAAS,qBAAqB,aAAaA,QAAM,kBAAkB;EAG7E,MAAM,UAAkC;GACtC,gBAAgB;GAChB,GAAI,MAAM,OAAO,WAAW;GAC7B;EAED,MAAM,OAAoB;GACxB,QAAQA,QAAM;GACd;GACD;AAED,MAAI,cAAcA,QAAM,WAAW,MACjC,MAAK,OAAO,KAAK,UAAU,WAAW;EAGxC,MAAM,MAAoB;GACxB,KAAK,QAAQ,UAAU;GACvB;GACD;EAED,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,QAAQ,IAAI;WACtB,OAAO;AACd,SAAM,IAAI,aAAa,MAAM;;AAG/B,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY,OAAU;AACzD,SAAM,IAAI,UAAU,SAAS,QAAQ,SAAS,YAAY,KAAK;;EAGjE,MAAM,OAAO,MAAM,SAAS,MAAM;EAClC,MAAM,SAAS,EAAE,UAAUA,QAAM,UAAU,KAAK;AAChD,MAAI,CAAC,OAAO,QAAS,OAAM,IAAI,gBAAgB,YAAY,OAAO,OAAO;AAEzE,SAAO,OAAO;;CAGhB,SAAS,kBAAuC,QAA8B;EAC5E,MAAM,MAAW,EAAE;AAEnB,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,KAAI,SAAS,OAAO,UAAU,YAAY,YAAY,SAAS,UAAU,MACvE,KAAI,QAAQ,WAAgB,KAAK,OAAc,OAAO;WAC7C,SAAS,OAAO,UAAU,SACnC,KAAI,OAAO,kBAAkB,MAAmB;AAIpD,SAAO;;AAIT,QAAO;EAAE;EAAM,GADA,OAAO,SAAS,kBAAkB,OAAO,OAAO,GAAG,EAAE;EAC1C;;;;;AClK5B,MAAM,mBAAmB,EAAE,SAAS;CAAC;CAAO;CAAQ;CAAO;CAAS;CAAS,CAAC;AAkB9E,SAAgB,MAMd,QACgE;AAEhE,GAAE,MAAM,kBAAkB,OAAO,OAAO;AAGxC,KAAI,OAAO,KAAK,WAAW,SAAS,GAAG;AACrC,MAAI,CAAC,OAAO,WACV,OAAM,IAAI,MACR,0BAA0B,OAAO,KAAK,WAAW,KAAK,KAAK,CAAC,4BAC7D;EAGH,MAAM,aAAa,cAAc,OAAO,WAAW;AAEnD,OAAK,MAAM,aAAa,OAAO,KAAK,WAClC,KAAI,CAAC,WAAW,SAAS,UAAU,CACjC,OAAM,IAAI,MACR,gBAAgB,UAAU,gDACT,WAAW,KAAK,KAAK,CAAC,GACxC;AAIL,OAAK,MAAM,aAAa,WACtB,KAAI,CAAC,OAAO,KAAK,WAAW,SAAS,UAAU,CAC7C,OAAM,IAAI,MACR,0BAA0B,UAAU,oDACb,OAAO,KAAK,WAAW,KAAK,KAAK,CAAC,GAC1D;;AAKP,QAAO;;AAQT,SAAS,cAAc,QAA+C;AACpE,KACE,aAAa,UACb,OAAQ,OAAe,YAAY,YAClC,OAAe,YAAY,KAE5B,QAAO,OAAO,KAAM,OAAe,QAAQ;AAE7C,QAAO,EAAE;;;;;;AC/DX,SAAgB,MAAwB,MAAuB;AAC7D,QAAO,IAAI;;;AAIb,SAAgB,SAA2B,MAA2B;AACpE,QAAO,KAAK,KAAK;;;;;;;;;;;;;;;;;;;;;;;;AAyBnB,SAAgB,KACd,SACA,GAAG,QACW;CACd,MAAM,aAAuB,EAAE;CAC/B,IAAI,WAAW,QAAQ;AAEvB,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACtC,MAAM,IAAI,OAAO;AACjB,cAAY,IAAI,QAAQ,IAAI;EAG5B,MAAM,QAAQ,EAAE,MAAM,iBAAiB;AACvC,MAAI,MACF,YAAW,KAAK,MAAM,GAAG;;AAI7B,QAAO;EAAE;EAAU;EAAY"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["route","result"],"sources":["../src/errors.ts","../src/cherry_client.ts","../src/route.ts","../src/path.ts"],"sourcesContent":["import { errAsync, ResultAsync } from \"neverthrow\";\n\n/** Base error class for all Cherry errors */\nexport abstract class CherryError extends Error {\n abstract readonly type: string;\n abstract readonly retryable: boolean;\n\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = this.constructor.name;\n }\n}\n\n/** HTTP response errors (4xx, 5xx) */\nexport class HttpError extends CherryError {\n readonly type = \"HttpError\";\n readonly retryable: boolean;\n\n constructor(\n public readonly status: number,\n public readonly statusText: string,\n public readonly body?: unknown,\n cause?: unknown,\n ) {\n super(`HTTP ${status}: ${statusText}`, { cause });\n this.retryable = status >= 500 || status === 429;\n }\n}\n\n/** Valibot validation errors */\nexport class ValidationError extends CherryError {\n readonly type = \"ValidationError\";\n readonly retryable = false;\n\n constructor(\n public readonly target: \"request\" | \"response\",\n public readonly issues: unknown[],\n cause?: unknown,\n ) {\n super(`Validation failed for ${target}`, { cause });\n }\n}\n\n/** Network/fetch errors */\nexport class NetworkError extends CherryError {\n readonly type = \"NetworkError\";\n readonly retryable = true;\n\n constructor(cause?: unknown) {\n super(`Network error`, { cause });\n }\n}\n\n/** Serialization errors (e.g., circular references, BigInt in JSON) */\nexport class SerializationError extends CherryError {\n readonly type = \"SerializationError\";\n readonly retryable = false;\n\n constructor(\n public readonly target: \"query\" | \"body\",\n public readonly key: string,\n cause?: unknown,\n ) {\n super(`Failed to serialize ${target} parameter \"${key}\"`, { cause });\n }\n}\n\n/** Catch-all for unexpected errors */\nexport class UnknownCherryError extends CherryError {\n readonly type = \"UnknownCherryError\";\n readonly retryable = false;\n\n constructor(cause?: unknown) {\n super(`Unknown error`, { cause });\n }\n}\n\n/** Type guard for CherryError */\nexport function isCherryError(error: unknown): error is CherryError {\n return error instanceof CherryError;\n}\n\n/** Helper to create error ResultAsync */\nexport function cherryErr<T>(error: CherryError): ResultAsync<T, CherryError> {\n return errAsync(error);\n}\n","import { ResultAsync } from \"neverthrow\";\nimport * as v from \"valibot\";\nimport type {\n CherryRoute,\n CherryResult,\n InferRouteInput,\n InferRouteOutput,\n Fetcher,\n FetchRequest,\n ClientConfig,\n Client,\n RouteTree,\n RoutesToClient,\n QueryParamOptions,\n} from \"./types\";\nimport { HttpError, ValidationError, NetworkError, SerializationError, UnknownCherryError } from \"./errors\";\n\nconst defaultFetcher: Fetcher = (req) => fetch(req.url, req.init);\n\nexport function serializeQueryParams(\n params: Record<string, unknown>,\n options?: QueryParamOptions,\n): string {\n if (options?.customSerializer) {\n return options.customSerializer(params);\n }\n\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n switch (options?.arrayFormat ?? \"repeat\") {\n case \"repeat\":\n for (const item of value) {\n searchParams.append(key, String(item));\n }\n break;\n case \"comma\":\n searchParams.set(key, value.join(\",\"));\n break;\n case \"brackets\":\n for (const item of value) {\n searchParams.append(`${key}[]`, String(item));\n }\n break;\n case \"json\":\n try {\n searchParams.set(key, JSON.stringify(value));\n } catch (error) {\n throw new SerializationError(\"query\", key, error);\n }\n break;\n }\n } else {\n searchParams.set(key, String(value));\n }\n }\n\n return searchParams.toString();\n}\n\nexport function createCherryClient<TRoutes extends RouteTree | undefined = undefined>(\n config: ClientConfig<TRoutes>,\n): Client<TRoutes> {\n const fetcher = config.fetcher ?? defaultFetcher;\n\n function call<T extends CherryRoute<any, any, any, any>>(\n route: T,\n params: InferRouteInput<T>,\n ): CherryResult<InferRouteOutput<T>> {\n return ResultAsync.fromPromise(executeRoute(route, params), (error) => {\n if (error instanceof HttpError) return error;\n if (error instanceof ValidationError) return error;\n if (error instanceof NetworkError) return error;\n if (error instanceof SerializationError) return error;\n return new UnknownCherryError(error);\n });\n }\n\n async function executeRoute<T extends CherryRoute<any, any, any, any>>(\n route: T,\n params: InferRouteInput<T>,\n ): Promise<InferRouteOutput<T>> {\n let pathParams: Record<string, unknown> = {};\n if (route.pathParams) {\n const result = v.safeParse(route.pathParams, params);\n if (!result.success) throw new ValidationError(\"request\", result.issues);\n pathParams = result.output;\n }\n\n let queryParams: Record<string, unknown> = {};\n if (route.queryParams) {\n const result = v.safeParse(route.queryParams, params);\n if (!result.success) throw new ValidationError(\"request\", result.issues);\n queryParams = result.output;\n }\n\n let bodyParams: Record<string, unknown> | undefined;\n if (route.bodyParams) {\n const result = v.safeParse(route.bodyParams, params);\n if (!result.success) throw new ValidationError(\"request\", result.issues);\n bodyParams = result.output;\n }\n\n let url = route.path.template;\n for (const [key, value] of Object.entries(pathParams)) {\n url = url.replace(`:${key}`, encodeURIComponent(String(value)));\n }\n\n const fullUrl = new URL(url, config.baseUrl);\n\n if (Object.keys(queryParams).length > 0) {\n fullUrl.search = serializeQueryParams(queryParams, route.queryParamOptions);\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(await config.headers?.()),\n };\n\n const init: RequestInit = {\n method: route.method,\n headers,\n };\n\n if (bodyParams && route.method !== \"GET\") {\n init.body = JSON.stringify(bodyParams);\n }\n\n const req: FetchRequest = {\n url: fullUrl.toString(),\n init,\n };\n\n let response: Response;\n try {\n response = await fetcher(req);\n } catch (error) {\n throw new NetworkError(error);\n }\n\n if (!response.ok) {\n const body = await response.text().catch(() => undefined);\n throw new HttpError(response.status, response.statusText, body);\n }\n\n const json = await response.json();\n const result = v.safeParse(route.response, json);\n if (!result.success) throw new ValidationError(\"response\", result.issues);\n\n return result.output;\n }\n\n function forgeRouteMethods<T extends RouteTree>(routes: T): RoutesToClient<T> {\n const out: any = {};\n\n for (const [key, value] of Object.entries(routes)) {\n if (value && typeof value === \"object\" && \"method\" in value && \"path\" in value) {\n out[key] = (params: any) => call(value as any, params);\n } else if (value && typeof value === \"object\") {\n out[key] = forgeRouteMethods(value as RouteTree);\n }\n }\n\n return out;\n }\n\n const routes = config.routes ? forgeRouteMethods(config.routes) : {};\n return { call, ...routes } as Client<TRoutes>;\n}\n","import * as v from \"valibot\";\nimport type { CherryRoute } from \"./types\";\n\nconst HttpMethodSchema = v.picklist([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\nexport function route<\n TPathParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TQueryParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TBodyParams extends v.BaseSchema<any, any, any> | undefined = undefined,\n TResponse extends v.BaseSchema<any, any, any> = v.BaseSchema<any, any, any>,\n>(\n config: CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse>,\n): CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse> {\n // validate HTTP method\n v.parse(HttpMethodSchema, config.method);\n\n // validate availability and content of pathParams schema, if needed\n if (config.path.paramNames.length > 0) {\n if (!config.pathParams) {\n throw new Error(\n `Route has path params [${config.path.paramNames.join(\", \")}] but no pathParams schema`,\n );\n }\n\n const schemaKeys = getSchemaKeys(config.pathParams);\n\n for (const paramName of config.path.paramNames) {\n if (!schemaKeys.includes(paramName)) {\n throw new Error(\n `Path param \":${paramName}\" not found in pathParams schema. ` +\n `Available: [${schemaKeys.join(\", \")}]`,\n );\n }\n }\n\n for (const schemaKey of schemaKeys) {\n if (!config.path.paramNames.includes(schemaKey)) {\n throw new Error(\n `pathParams schema key \"${schemaKey}\" not present in path template. ` +\n `Template params: [${config.path.paramNames.join(\", \")}]`,\n );\n }\n }\n }\n\n return config as CherryRoute<\n TPathParams,\n TQueryParams,\n TBodyParams,\n TResponse\n >;\n}\n\nfunction getSchemaKeys(schema: v.BaseSchema<any, any, any>): string[] {\n if (\n \"entries\" in schema &&\n typeof (schema as any).entries === \"object\" &&\n (schema as any).entries !== null\n ) {\n return Object.keys((schema as any).entries);\n }\n return [];\n}\n","// path.ts - Tagged template functions for type-safe path building\nimport type { PathTemplate } from \"./types\";\n\n/** Branded type for path parameter markers */\ndeclare const PathParamBrand: unique symbol;\nexport type PathParam<T extends string = string> = string & {\n readonly [PathParamBrand]: T;\n};\n\n/** Branded type for optional path parameter markers */\ndeclare const OptionalParamBrand: unique symbol;\nexport type OptionalParam<T extends string = string> = string & {\n readonly [OptionalParamBrand]: T;\n};\n\n/** Union type for any path param marker */\nexport type AnyPathParam = PathParam<string> | OptionalParam<string>;\n\n/** Create a path parameter marker */\nexport function param<T extends string>(name: T): PathParam<T> {\n return `:${name}` as PathParam<T>;\n}\n\n/** Create an optional path parameter marker */\nexport function optional<T extends string>(name: T): OptionalParam<T> {\n return `(:${name})` as OptionalParam<T>;\n}\n\n/**\n * Tagged template for building path templates.\n *\n * @example\n * ```ts\n * // Simple path with one param\n * const userPath = path`/users/${param(\"id\")}`;\n * // { template: \"/users/:id\", paramNames: [\"id\"] }\n *\n * // Multiple params\n * const postPath = path`/users/${param(\"userId\")}/posts/${param(\"postId\")}`;\n * // { template: \"/users/:userId/posts/:postId\", paramNames: [\"userId\", \"postId\"] }\n *\n * // Optional params\n * const versionedPath = path`/api${optional(\"version\")}/users`;\n * // { template: \"/api(:version)/users\", paramNames: [\"version\"] }\n *\n * // No params\n * const staticPath = path`/health`;\n * // { template: \"/health\", paramNames: [] }\n * ```\n */\nexport function path(\n strings: TemplateStringsArray,\n ...params: AnyPathParam[]\n): PathTemplate {\n const paramNames: string[] = [];\n let template = strings[0];\n\n for (let i = 0; i < params.length; i++) {\n const p = params[i];\n template += p + strings[i + 1];\n\n // Extract param name from `:name` or `(:name)`\n const match = p.match(/^\\(?:(\\w+)\\)?$/);\n if (match) {\n paramNames.push(match[1]);\n }\n }\n\n return { template, paramNames };\n}\n"],"mappings":";;;;;AAGA,IAAsB,cAAtB,cAA0C,MAAM;CAI9C,YAAY,SAAiB,SAA+B;AAC1D,QAAM,SAAS,QAAQ;AACvB,OAAK,OAAO,KAAK,YAAY;;;;AAKjC,IAAa,YAAb,cAA+B,YAAY;CACzC,AAAS,OAAO;CAChB,AAAS;CAET,YACE,AAAgB,QAChB,AAAgB,YAChB,AAAgB,MAChB,OACA;AACA,QAAM,QAAQ,OAAO,IAAI,cAAc,EAAE,OAAO,CAAC;EALjC;EACA;EACA;AAIhB,OAAK,YAAY,UAAU,OAAO,WAAW;;;;AAKjD,IAAa,kBAAb,cAAqC,YAAY;CAC/C,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YACE,AAAgB,QAChB,AAAgB,QAChB,OACA;AACA,QAAM,yBAAyB,UAAU,EAAE,OAAO,CAAC;EAJnC;EACA;;;;AAQpB,IAAa,eAAb,cAAkC,YAAY;CAC5C,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YAAY,OAAiB;AAC3B,QAAM,iBAAiB,EAAE,OAAO,CAAC;;;;AAKrC,IAAa,qBAAb,cAAwC,YAAY;CAClD,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YACE,AAAgB,QAChB,AAAgB,KAChB,OACA;AACA,QAAM,uBAAuB,OAAO,cAAc,IAAI,IAAI,EAAE,OAAO,CAAC;EAJpD;EACA;;;;AAQpB,IAAa,qBAAb,cAAwC,YAAY;CAClD,AAAS,OAAO;CAChB,AAAS,YAAY;CAErB,YAAY,OAAiB;AAC3B,QAAM,iBAAiB,EAAE,OAAO,CAAC;;;;AAKrC,SAAgB,cAAc,OAAsC;AAClE,QAAO,iBAAiB;;;AAI1B,SAAgB,UAAa,OAAiD;AAC5E,QAAO,SAAS,MAAM;;;;;ACnExB,MAAM,kBAA2B,QAAQ,MAAM,IAAI,KAAK,IAAI,KAAK;AAEjE,SAAgB,qBACd,QACA,SACQ;AACR,KAAI,SAAS,iBACX,QAAO,QAAQ,iBAAiB,OAAO;CAGzC,MAAM,eAAe,IAAI,iBAAiB;AAE1C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,EAAE;AACjD,MAAI,UAAU,UAAa,UAAU,KAAM;AAE3C,MAAI,MAAM,QAAQ,MAAM,CACtB,SAAQ,SAAS,eAAe,UAAhC;GACE,KAAK;AACH,SAAK,MAAM,QAAQ,MACjB,cAAa,OAAO,KAAK,OAAO,KAAK,CAAC;AAExC;GACF,KAAK;AACH,iBAAa,IAAI,KAAK,MAAM,KAAK,IAAI,CAAC;AACtC;GACF,KAAK;AACH,SAAK,MAAM,QAAQ,MACjB,cAAa,OAAO,GAAG,IAAI,KAAK,OAAO,KAAK,CAAC;AAE/C;GACF,KAAK;AACH,QAAI;AACF,kBAAa,IAAI,KAAK,KAAK,UAAU,MAAM,CAAC;aACrC,OAAO;AACd,WAAM,IAAI,mBAAmB,SAAS,KAAK,MAAM;;AAEnD;;MAGJ,cAAa,IAAI,KAAK,OAAO,MAAM,CAAC;;AAIxC,QAAO,aAAa,UAAU;;AAGhC,SAAgB,mBACd,QACiB;CACjB,MAAM,UAAU,OAAO,WAAW;CAElC,SAAS,KACP,SACA,QACmC;AACnC,SAAO,YAAY,YAAY,aAAaA,SAAO,OAAO,GAAG,UAAU;AACrE,OAAI,iBAAiB,UAAW,QAAO;AACvC,OAAI,iBAAiB,gBAAiB,QAAO;AAC7C,OAAI,iBAAiB,aAAc,QAAO;AAC1C,OAAI,iBAAiB,mBAAoB,QAAO;AAChD,UAAO,IAAI,mBAAmB,MAAM;IACpC;;CAGJ,eAAe,aACb,SACA,QAC8B;EAC9B,IAAI,aAAsC,EAAE;AAC5C,MAAIA,QAAM,YAAY;GACpB,MAAMC,WAAS,EAAE,UAAUD,QAAM,YAAY,OAAO;AACpD,OAAI,CAACC,SAAO,QAAS,OAAM,IAAI,gBAAgB,WAAWA,SAAO,OAAO;AACxE,gBAAaA,SAAO;;EAGtB,IAAI,cAAuC,EAAE;AAC7C,MAAID,QAAM,aAAa;GACrB,MAAMC,WAAS,EAAE,UAAUD,QAAM,aAAa,OAAO;AACrD,OAAI,CAACC,SAAO,QAAS,OAAM,IAAI,gBAAgB,WAAWA,SAAO,OAAO;AACxE,iBAAcA,SAAO;;EAGvB,IAAI;AACJ,MAAID,QAAM,YAAY;GACpB,MAAMC,WAAS,EAAE,UAAUD,QAAM,YAAY,OAAO;AACpD,OAAI,CAACC,SAAO,QAAS,OAAM,IAAI,gBAAgB,WAAWA,SAAO,OAAO;AACxE,gBAAaA,SAAO;;EAGtB,IAAI,MAAMD,QAAM,KAAK;AACrB,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,CACnD,OAAM,IAAI,QAAQ,IAAI,OAAO,mBAAmB,OAAO,MAAM,CAAC,CAAC;EAGjE,MAAM,UAAU,IAAI,IAAI,KAAK,OAAO,QAAQ;AAE5C,MAAI,OAAO,KAAK,YAAY,CAAC,SAAS,EACpC,SAAQ,SAAS,qBAAqB,aAAaA,QAAM,kBAAkB;EAG7E,MAAM,UAAkC;GACtC,gBAAgB;GAChB,GAAI,MAAM,OAAO,WAAW;GAC7B;EAED,MAAM,OAAoB;GACxB,QAAQA,QAAM;GACd;GACD;AAED,MAAI,cAAcA,QAAM,WAAW,MACjC,MAAK,OAAO,KAAK,UAAU,WAAW;EAGxC,MAAM,MAAoB;GACxB,KAAK,QAAQ,UAAU;GACvB;GACD;EAED,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,QAAQ,IAAI;WACtB,OAAO;AACd,SAAM,IAAI,aAAa,MAAM;;AAG/B,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY,OAAU;AACzD,SAAM,IAAI,UAAU,SAAS,QAAQ,SAAS,YAAY,KAAK;;EAGjE,MAAM,OAAO,MAAM,SAAS,MAAM;EAClC,MAAM,SAAS,EAAE,UAAUA,QAAM,UAAU,KAAK;AAChD,MAAI,CAAC,OAAO,QAAS,OAAM,IAAI,gBAAgB,YAAY,OAAO,OAAO;AAEzE,SAAO,OAAO;;CAGhB,SAAS,kBAAuC,QAA8B;EAC5E,MAAM,MAAW,EAAE;AAEnB,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,KAAI,SAAS,OAAO,UAAU,YAAY,YAAY,SAAS,UAAU,MACvE,KAAI,QAAQ,WAAgB,KAAK,OAAc,OAAO;WAC7C,SAAS,OAAO,UAAU,SACnC,KAAI,OAAO,kBAAkB,MAAmB;AAIpD,SAAO;;AAIT,QAAO;EAAE;EAAM,GADA,OAAO,SAAS,kBAAkB,OAAO,OAAO,GAAG,EAAE;EAC1C;;;;;ACvK5B,MAAM,mBAAmB,EAAE,SAAS;CAAC;CAAO;CAAQ;CAAO;CAAS;CAAS,CAAC;AAE9E,SAAgB,MAMd,QACgE;AAEhE,GAAE,MAAM,kBAAkB,OAAO,OAAO;AAGxC,KAAI,OAAO,KAAK,WAAW,SAAS,GAAG;AACrC,MAAI,CAAC,OAAO,WACV,OAAM,IAAI,MACR,0BAA0B,OAAO,KAAK,WAAW,KAAK,KAAK,CAAC,4BAC7D;EAGH,MAAM,aAAa,cAAc,OAAO,WAAW;AAEnD,OAAK,MAAM,aAAa,OAAO,KAAK,WAClC,KAAI,CAAC,WAAW,SAAS,UAAU,CACjC,OAAM,IAAI,MACR,gBAAgB,UAAU,gDACT,WAAW,KAAK,KAAK,CAAC,GACxC;AAIL,OAAK,MAAM,aAAa,WACtB,KAAI,CAAC,OAAO,KAAK,WAAW,SAAS,UAAU,CAC7C,OAAM,IAAI,MACR,0BAA0B,UAAU,oDACb,OAAO,KAAK,WAAW,KAAK,KAAK,CAAC,GAC1D;;AAKP,QAAO;;AAQT,SAAS,cAAc,QAA+C;AACpE,KACE,aAAa,UACb,OAAQ,OAAe,YAAY,YAClC,OAAe,YAAY,KAE5B,QAAO,OAAO,KAAM,OAAe,QAAQ;AAE7C,QAAO,EAAE;;;;;;AC1CX,SAAgB,MAAwB,MAAuB;AAC7D,QAAO,IAAI;;;AAIb,SAAgB,SAA2B,MAA2B;AACpE,QAAO,KAAK,KAAK;;;;;;;;;;;;;;;;;;;;;;;;AAyBnB,SAAgB,KACd,SACA,GAAG,QACW;CACd,MAAM,aAAuB,EAAE;CAC/B,IAAI,WAAW,QAAQ;AAEvB,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACtC,MAAM,IAAI,OAAO;AACjB,cAAY,IAAI,QAAQ,IAAI;EAG5B,MAAM,QAAQ,EAAE,MAAM,iBAAiB;AACvC,MAAI,MACF,YAAW,KAAK,MAAM,GAAG;;AAI7B,QAAO;EAAE;EAAU;EAAY"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b3-business/cherry",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A tree-shakeable, minimal API client factory. Import only the routes you need — nothing more.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"build": "tsdown",
|
|
21
|
-
"check": "
|
|
22
|
-
"lint": "oxlint src test",
|
|
22
|
+
"check": "oxlint --type-aware --type-check src test",
|
|
23
23
|
"typecheck": "tsc --noEmit -p .",
|
|
24
24
|
"test": "bun test",
|
|
25
25
|
"prepublishOnly": "npm run check && npm run test && npm run build",
|
|
@@ -34,8 +34,6 @@
|
|
|
34
34
|
"@types/bun": "^1.3.5",
|
|
35
35
|
"expect-type": "^1.3.0",
|
|
36
36
|
"jsr": "^0.13.5",
|
|
37
|
-
"npm-run-all": "^4.1.5",
|
|
38
|
-
"oxlint": "^1.36.0",
|
|
39
37
|
"tsdown": "^0.19.0-beta.2",
|
|
40
38
|
"typescript": "^5.9.3"
|
|
41
39
|
},
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ResultAsync } from "neverthrow";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import type {
|
|
4
|
+
CherryRoute,
|
|
5
|
+
CherryResult,
|
|
6
|
+
InferRouteInput,
|
|
7
|
+
InferRouteOutput,
|
|
8
|
+
Fetcher,
|
|
9
|
+
FetchRequest,
|
|
10
|
+
ClientConfig,
|
|
11
|
+
Client,
|
|
12
|
+
RouteTree,
|
|
13
|
+
RoutesToClient,
|
|
14
|
+
QueryParamOptions,
|
|
15
|
+
} from "./types";
|
|
16
|
+
import { HttpError, ValidationError, NetworkError, SerializationError, UnknownCherryError } from "./errors";
|
|
17
|
+
|
|
18
|
+
const defaultFetcher: Fetcher = (req) => fetch(req.url, req.init);
|
|
19
|
+
|
|
20
|
+
export function serializeQueryParams(
|
|
21
|
+
params: Record<string, unknown>,
|
|
22
|
+
options?: QueryParamOptions,
|
|
23
|
+
): string {
|
|
24
|
+
if (options?.customSerializer) {
|
|
25
|
+
return options.customSerializer(params);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const searchParams = new URLSearchParams();
|
|
29
|
+
|
|
30
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31
|
+
if (value === undefined || value === null) continue;
|
|
32
|
+
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
switch (options?.arrayFormat ?? "repeat") {
|
|
35
|
+
case "repeat":
|
|
36
|
+
for (const item of value) {
|
|
37
|
+
searchParams.append(key, String(item));
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case "comma":
|
|
41
|
+
searchParams.set(key, value.join(","));
|
|
42
|
+
break;
|
|
43
|
+
case "brackets":
|
|
44
|
+
for (const item of value) {
|
|
45
|
+
searchParams.append(`${key}[]`, String(item));
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "json":
|
|
49
|
+
try {
|
|
50
|
+
searchParams.set(key, JSON.stringify(value));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new SerializationError("query", key, error);
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
searchParams.set(key, String(value));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return searchParams.toString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createCherryClient<TRoutes extends RouteTree | undefined = undefined>(
|
|
65
|
+
config: ClientConfig<TRoutes>,
|
|
66
|
+
): Client<TRoutes> {
|
|
67
|
+
const fetcher = config.fetcher ?? defaultFetcher;
|
|
68
|
+
|
|
69
|
+
function call<T extends CherryRoute<any, any, any, any>>(
|
|
70
|
+
route: T,
|
|
71
|
+
params: InferRouteInput<T>,
|
|
72
|
+
): CherryResult<InferRouteOutput<T>> {
|
|
73
|
+
return ResultAsync.fromPromise(executeRoute(route, params), (error) => {
|
|
74
|
+
if (error instanceof HttpError) return error;
|
|
75
|
+
if (error instanceof ValidationError) return error;
|
|
76
|
+
if (error instanceof NetworkError) return error;
|
|
77
|
+
if (error instanceof SerializationError) return error;
|
|
78
|
+
return new UnknownCherryError(error);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function executeRoute<T extends CherryRoute<any, any, any, any>>(
|
|
83
|
+
route: T,
|
|
84
|
+
params: InferRouteInput<T>,
|
|
85
|
+
): Promise<InferRouteOutput<T>> {
|
|
86
|
+
let pathParams: Record<string, unknown> = {};
|
|
87
|
+
if (route.pathParams) {
|
|
88
|
+
const result = v.safeParse(route.pathParams, params);
|
|
89
|
+
if (!result.success) throw new ValidationError("request", result.issues);
|
|
90
|
+
pathParams = result.output;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let queryParams: Record<string, unknown> = {};
|
|
94
|
+
if (route.queryParams) {
|
|
95
|
+
const result = v.safeParse(route.queryParams, params);
|
|
96
|
+
if (!result.success) throw new ValidationError("request", result.issues);
|
|
97
|
+
queryParams = result.output;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let bodyParams: Record<string, unknown> | undefined;
|
|
101
|
+
if (route.bodyParams) {
|
|
102
|
+
const result = v.safeParse(route.bodyParams, params);
|
|
103
|
+
if (!result.success) throw new ValidationError("request", result.issues);
|
|
104
|
+
bodyParams = result.output;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let url = route.path.template;
|
|
108
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
|
109
|
+
url = url.replace(`:${key}`, encodeURIComponent(String(value)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fullUrl = new URL(url, config.baseUrl);
|
|
113
|
+
|
|
114
|
+
if (Object.keys(queryParams).length > 0) {
|
|
115
|
+
fullUrl.search = serializeQueryParams(queryParams, route.queryParamOptions);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const headers: Record<string, string> = {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
...(await config.headers?.()),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const init: RequestInit = {
|
|
124
|
+
method: route.method,
|
|
125
|
+
headers,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (bodyParams && route.method !== "GET") {
|
|
129
|
+
init.body = JSON.stringify(bodyParams);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const req: FetchRequest = {
|
|
133
|
+
url: fullUrl.toString(),
|
|
134
|
+
init,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
let response: Response;
|
|
138
|
+
try {
|
|
139
|
+
response = await fetcher(req);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new NetworkError(error);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const body = await response.text().catch(() => undefined);
|
|
146
|
+
throw new HttpError(response.status, response.statusText, body);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const json = await response.json();
|
|
150
|
+
const result = v.safeParse(route.response, json);
|
|
151
|
+
if (!result.success) throw new ValidationError("response", result.issues);
|
|
152
|
+
|
|
153
|
+
return result.output;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function forgeRouteMethods<T extends RouteTree>(routes: T): RoutesToClient<T> {
|
|
157
|
+
const out: any = {};
|
|
158
|
+
|
|
159
|
+
for (const [key, value] of Object.entries(routes)) {
|
|
160
|
+
if (value && typeof value === "object" && "method" in value && "path" in value) {
|
|
161
|
+
out[key] = (params: any) => call(value as any, params);
|
|
162
|
+
} else if (value && typeof value === "object") {
|
|
163
|
+
out[key] = forgeRouteMethods(value as RouteTree);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const routes = config.routes ? forgeRouteMethods(config.routes) : {};
|
|
171
|
+
return { call, ...routes } as Client<TRoutes>;
|
|
172
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { errAsync, ResultAsync } from "neverthrow";
|
|
2
|
+
|
|
3
|
+
/** Base error class for all Cherry errors */
|
|
4
|
+
export abstract class CherryError extends Error {
|
|
5
|
+
abstract readonly type: string;
|
|
6
|
+
abstract readonly retryable: boolean;
|
|
7
|
+
|
|
8
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.name = this.constructor.name;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** HTTP response errors (4xx, 5xx) */
|
|
15
|
+
export class HttpError extends CherryError {
|
|
16
|
+
readonly type = "HttpError";
|
|
17
|
+
readonly retryable: boolean;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly status: number,
|
|
21
|
+
public readonly statusText: string,
|
|
22
|
+
public readonly body?: unknown,
|
|
23
|
+
cause?: unknown,
|
|
24
|
+
) {
|
|
25
|
+
super(`HTTP ${status}: ${statusText}`, { cause });
|
|
26
|
+
this.retryable = status >= 500 || status === 429;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Valibot validation errors */
|
|
31
|
+
export class ValidationError extends CherryError {
|
|
32
|
+
readonly type = "ValidationError";
|
|
33
|
+
readonly retryable = false;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
public readonly target: "request" | "response",
|
|
37
|
+
public readonly issues: unknown[],
|
|
38
|
+
cause?: unknown,
|
|
39
|
+
) {
|
|
40
|
+
super(`Validation failed for ${target}`, { cause });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Network/fetch errors */
|
|
45
|
+
export class NetworkError extends CherryError {
|
|
46
|
+
readonly type = "NetworkError";
|
|
47
|
+
readonly retryable = true;
|
|
48
|
+
|
|
49
|
+
constructor(cause?: unknown) {
|
|
50
|
+
super(`Network error`, { cause });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Serialization errors (e.g., circular references, BigInt in JSON) */
|
|
55
|
+
export class SerializationError extends CherryError {
|
|
56
|
+
readonly type = "SerializationError";
|
|
57
|
+
readonly retryable = false;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
public readonly target: "query" | "body",
|
|
61
|
+
public readonly key: string,
|
|
62
|
+
cause?: unknown,
|
|
63
|
+
) {
|
|
64
|
+
super(`Failed to serialize ${target} parameter "${key}"`, { cause });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Catch-all for unexpected errors */
|
|
69
|
+
export class UnknownCherryError extends CherryError {
|
|
70
|
+
readonly type = "UnknownCherryError";
|
|
71
|
+
readonly retryable = false;
|
|
72
|
+
|
|
73
|
+
constructor(cause?: unknown) {
|
|
74
|
+
super(`Unknown error`, { cause });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Type guard for CherryError */
|
|
79
|
+
export function isCherryError(error: unknown): error is CherryError {
|
|
80
|
+
return error instanceof CherryError;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Helper to create error ResultAsync */
|
|
84
|
+
export function cherryErr<T>(error: CherryError): ResultAsync<T, CherryError> {
|
|
85
|
+
return errAsync(error);
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { createCherryClient, serializeQueryParams } from "./cherry_client";
|
|
2
|
+
|
|
3
|
+
export { route } from "./route";
|
|
4
|
+
|
|
5
|
+
export { path, param, optional, type PathParam, type OptionalParam, type AnyPathParam } from "./path";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
CherryError,
|
|
9
|
+
HttpError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
SerializationError,
|
|
13
|
+
UnknownCherryError,
|
|
14
|
+
isCherryError,
|
|
15
|
+
cherryErr,
|
|
16
|
+
} from "./errors";
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
HttpMethod,
|
|
20
|
+
PathTemplate,
|
|
21
|
+
CherryRoute,
|
|
22
|
+
QueryParamOptions,
|
|
23
|
+
InferRouteInput,
|
|
24
|
+
InferRouteOutput,
|
|
25
|
+
CherryResult,
|
|
26
|
+
FetchRequest,
|
|
27
|
+
Fetcher,
|
|
28
|
+
RouteTree,
|
|
29
|
+
ClientConfig,
|
|
30
|
+
Client,
|
|
31
|
+
RoutesToClient,
|
|
32
|
+
} from "./types";
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// path.ts - Tagged template functions for type-safe path building
|
|
2
|
+
import type { PathTemplate } from "./types";
|
|
3
|
+
|
|
4
|
+
/** Branded type for path parameter markers */
|
|
5
|
+
declare const PathParamBrand: unique symbol;
|
|
6
|
+
export type PathParam<T extends string = string> = string & {
|
|
7
|
+
readonly [PathParamBrand]: T;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Branded type for optional path parameter markers */
|
|
11
|
+
declare const OptionalParamBrand: unique symbol;
|
|
12
|
+
export type OptionalParam<T extends string = string> = string & {
|
|
13
|
+
readonly [OptionalParamBrand]: T;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Union type for any path param marker */
|
|
17
|
+
export type AnyPathParam = PathParam<string> | OptionalParam<string>;
|
|
18
|
+
|
|
19
|
+
/** Create a path parameter marker */
|
|
20
|
+
export function param<T extends string>(name: T): PathParam<T> {
|
|
21
|
+
return `:${name}` as PathParam<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Create an optional path parameter marker */
|
|
25
|
+
export function optional<T extends string>(name: T): OptionalParam<T> {
|
|
26
|
+
return `(:${name})` as OptionalParam<T>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tagged template for building path templates.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* // Simple path with one param
|
|
35
|
+
* const userPath = path`/users/${param("id")}`;
|
|
36
|
+
* // { template: "/users/:id", paramNames: ["id"] }
|
|
37
|
+
*
|
|
38
|
+
* // Multiple params
|
|
39
|
+
* const postPath = path`/users/${param("userId")}/posts/${param("postId")}`;
|
|
40
|
+
* // { template: "/users/:userId/posts/:postId", paramNames: ["userId", "postId"] }
|
|
41
|
+
*
|
|
42
|
+
* // Optional params
|
|
43
|
+
* const versionedPath = path`/api${optional("version")}/users`;
|
|
44
|
+
* // { template: "/api(:version)/users", paramNames: ["version"] }
|
|
45
|
+
*
|
|
46
|
+
* // No params
|
|
47
|
+
* const staticPath = path`/health`;
|
|
48
|
+
* // { template: "/health", paramNames: [] }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function path(
|
|
52
|
+
strings: TemplateStringsArray,
|
|
53
|
+
...params: AnyPathParam[]
|
|
54
|
+
): PathTemplate {
|
|
55
|
+
const paramNames: string[] = [];
|
|
56
|
+
let template = strings[0];
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < params.length; i++) {
|
|
59
|
+
const p = params[i];
|
|
60
|
+
template += p + strings[i + 1];
|
|
61
|
+
|
|
62
|
+
// Extract param name from `:name` or `(:name)`
|
|
63
|
+
const match = p.match(/^\(?:(\w+)\)?$/);
|
|
64
|
+
if (match) {
|
|
65
|
+
paramNames.push(match[1]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { template, paramNames };
|
|
70
|
+
}
|
package/src/route.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import type { CherryRoute } from "./types";
|
|
3
|
+
|
|
4
|
+
const HttpMethodSchema = v.picklist(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
5
|
+
|
|
6
|
+
export function route<
|
|
7
|
+
TPathParams extends v.BaseSchema<any, any, any> | undefined = undefined,
|
|
8
|
+
TQueryParams extends v.BaseSchema<any, any, any> | undefined = undefined,
|
|
9
|
+
TBodyParams extends v.BaseSchema<any, any, any> | undefined = undefined,
|
|
10
|
+
TResponse extends v.BaseSchema<any, any, any> = v.BaseSchema<any, any, any>,
|
|
11
|
+
>(
|
|
12
|
+
config: CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse>,
|
|
13
|
+
): CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse> {
|
|
14
|
+
// validate HTTP method
|
|
15
|
+
v.parse(HttpMethodSchema, config.method);
|
|
16
|
+
|
|
17
|
+
// validate availability and content of pathParams schema, if needed
|
|
18
|
+
if (config.path.paramNames.length > 0) {
|
|
19
|
+
if (!config.pathParams) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Route has path params [${config.path.paramNames.join(", ")}] but no pathParams schema`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const schemaKeys = getSchemaKeys(config.pathParams);
|
|
26
|
+
|
|
27
|
+
for (const paramName of config.path.paramNames) {
|
|
28
|
+
if (!schemaKeys.includes(paramName)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Path param ":${paramName}" not found in pathParams schema. ` +
|
|
31
|
+
`Available: [${schemaKeys.join(", ")}]`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const schemaKey of schemaKeys) {
|
|
37
|
+
if (!config.path.paramNames.includes(schemaKey)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`pathParams schema key "${schemaKey}" not present in path template. ` +
|
|
40
|
+
`Template params: [${config.path.paramNames.join(", ")}]`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return config as CherryRoute<
|
|
47
|
+
TPathParams,
|
|
48
|
+
TQueryParams,
|
|
49
|
+
TBodyParams,
|
|
50
|
+
TResponse
|
|
51
|
+
>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getSchemaKeys(schema: v.BaseSchema<any, any, any>): string[] {
|
|
55
|
+
if (
|
|
56
|
+
"entries" in schema &&
|
|
57
|
+
typeof (schema as any).entries === "object" &&
|
|
58
|
+
(schema as any).entries !== null
|
|
59
|
+
) {
|
|
60
|
+
return Object.keys((schema as any).entries);
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { BaseSchema, InferInput, InferOutput } from "valibot";
|
|
2
|
+
import type { ResultAsync } from "neverthrow";
|
|
3
|
+
import type { CherryError } from "./errors";
|
|
4
|
+
|
|
5
|
+
type AnySchema = BaseSchema<any, any, any>;
|
|
6
|
+
|
|
7
|
+
/** HTTP methods supported by Cherry */
|
|
8
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
9
|
+
|
|
10
|
+
/** Path template result from path() tagged template */
|
|
11
|
+
export type PathTemplate = {
|
|
12
|
+
template: string; // "/users/:id/posts/:postId"
|
|
13
|
+
paramNames: string[]; // ["id", "postId"]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Route definition with separated parameter schemas */
|
|
17
|
+
export type CherryRoute<
|
|
18
|
+
TPathParams extends AnySchema | undefined = undefined,
|
|
19
|
+
TQueryParams extends AnySchema | undefined = undefined,
|
|
20
|
+
TBodyParams extends AnySchema | undefined = undefined,
|
|
21
|
+
TResponse extends AnySchema = AnySchema,
|
|
22
|
+
> = {
|
|
23
|
+
method: HttpMethod;
|
|
24
|
+
path: PathTemplate;
|
|
25
|
+
pathParams?: TPathParams;
|
|
26
|
+
queryParams?: TQueryParams;
|
|
27
|
+
bodyParams?: TBodyParams;
|
|
28
|
+
response: TResponse;
|
|
29
|
+
queryParamOptions?: QueryParamOptions;
|
|
30
|
+
description?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Options for query parameter serialization */
|
|
34
|
+
export type QueryParamOptions = {
|
|
35
|
+
arrayFormat?: "repeat" | "comma" | "brackets" | "json";
|
|
36
|
+
customSerializer?: (params: Record<string, unknown>) => string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type AnyCherryRoute = CherryRoute<any, any, any, any>;
|
|
40
|
+
|
|
41
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
42
|
+
|
|
43
|
+
type IsEmptyObject<T> = keyof T extends never ? true : false;
|
|
44
|
+
|
|
45
|
+
type InferSchemaInput<T> = T extends AnySchema ? InferInput<T> : {};
|
|
46
|
+
|
|
47
|
+
type BuildRouteInputFromProps<
|
|
48
|
+
TPathParams,
|
|
49
|
+
TQueryParams,
|
|
50
|
+
TBodyParams
|
|
51
|
+
> = InferSchemaInput<TPathParams> &
|
|
52
|
+
InferSchemaInput<TQueryParams> &
|
|
53
|
+
InferSchemaInput<TBodyParams>;
|
|
54
|
+
|
|
55
|
+
export type InferRouteInput<T> =
|
|
56
|
+
T extends { pathParams?: infer TPath; queryParams?: infer TQuery; bodyParams?: infer TBody }
|
|
57
|
+
? IsEmptyObject<BuildRouteInputFromProps<TPath, TQuery, TBody>> extends true
|
|
58
|
+
? void
|
|
59
|
+
: Prettify<BuildRouteInputFromProps<TPath, TQuery, TBody>>
|
|
60
|
+
: never;
|
|
61
|
+
|
|
62
|
+
/** Infer response output from a route */
|
|
63
|
+
export type InferRouteOutput<T extends AnyCherryRoute> =
|
|
64
|
+
T["response"] extends AnySchema ? InferOutput<T["response"]> : never;
|
|
65
|
+
|
|
66
|
+
/** Cherry result type - always ResultAsync */
|
|
67
|
+
export type CherryResult<T> = ResultAsync<T, CherryError>;
|
|
68
|
+
|
|
69
|
+
/** Fetcher request shape (extensible for middleware) */
|
|
70
|
+
export type FetchRequest = {
|
|
71
|
+
url: string;
|
|
72
|
+
init: RequestInit;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Fetcher function signature */
|
|
76
|
+
export type Fetcher = (req: FetchRequest) => Promise<Response>;
|
|
77
|
+
|
|
78
|
+
/** Route tree (supports namespacing via nested objects) */
|
|
79
|
+
export type RouteTree = {
|
|
80
|
+
[key: string]: AnyCherryRoute | RouteTree;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Client configuration */
|
|
84
|
+
export type ClientConfig<TRoutes extends RouteTree | undefined = undefined> = {
|
|
85
|
+
baseUrl: string;
|
|
86
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
87
|
+
fetcher?: Fetcher;
|
|
88
|
+
routes?: TRoutes;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type Client<TRoutes extends RouteTree | undefined = undefined> = {
|
|
92
|
+
call: <T extends AnyCherryRoute>(
|
|
93
|
+
route: T,
|
|
94
|
+
...args: InferRouteInput<T> extends void ? [] : [params: InferRouteInput<T>]
|
|
95
|
+
) => CherryResult<InferRouteOutput<T>>;
|
|
96
|
+
} & (TRoutes extends RouteTree ? RoutesToClient<TRoutes> : {});
|
|
97
|
+
|
|
98
|
+
/** Convert a nested route tree into a nested client method tree */
|
|
99
|
+
export type RoutesToClient<TRoutes extends RouteTree> = {
|
|
100
|
+
[K in keyof TRoutes]: TRoutes[K] extends AnyCherryRoute
|
|
101
|
+
? InferRouteInput<TRoutes[K]> extends void
|
|
102
|
+
? () => CherryResult<InferRouteOutput<TRoutes[K]>>
|
|
103
|
+
: (params: InferRouteInput<TRoutes[K]>) => CherryResult<InferRouteOutput<TRoutes[K]>>
|
|
104
|
+
: TRoutes[K] extends RouteTree
|
|
105
|
+
? RoutesToClient<TRoutes[K]>
|
|
106
|
+
: never;
|
|
107
|
+
};
|