@b3-business/cherry 0.1.0

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 ADDED
@@ -0,0 +1,221 @@
1
+ # cherry
2
+
3
+ > Cherry-pick your API routes.
4
+
5
+ A tree-shakeable, minimal API client factory. Import only the routes you need — nothing more.
6
+
7
+ [![npm](https://img.shields.io/npm/v/@b3b/cherry)](https://www.npmjs.com/package/@b3b/cherry)
8
+ [![JSR](https://jsr.io/badges/@b3b/cherry)](https://jsr.io/@b3b/cherry)
9
+
10
+ ---
11
+
12
+ ## What is Cherry?
13
+
14
+ Cherry is a lightweight API client library that separates **route definitions** from the **client runtime**. Routes are plain objects with validation schemas — import only what you use, bundle only what you import.
15
+
16
+ ```ts
17
+ import { createClient } from "@b3b/cherry";
18
+ import { listZones, getZone } from "./routes/cloudflare";
19
+
20
+ const cf = createClient({
21
+ baseUrl: "https://api.cloudflare.com/client/v4",
22
+ headers: () => ({ Authorization: `Bearer ${process.env.CF_TOKEN}` }),
23
+ routes: { listZones, getZone },
24
+ });
25
+
26
+ // Fully typed, fully tree-shakeable
27
+ const zones = await cf.listZones({ account_id: "abc" });
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Why Cherry?
33
+
34
+ ### The Problem
35
+
36
+ Official API clients (e.g., Cloudflare, AWS) bundle **everything**:
37
+
38
+ - Every endpoint, even ones you'll never use
39
+ - Massive web shims for Node.js compatibility
40
+ - Complex class hierarchies that defeat tree-shaking
41
+
42
+ The result? A simple "list DNS records" call pulls in megabytes of unused code, bloating serverless deployments and slowing cold starts.
43
+
44
+ ```
45
+ # Real-world bundle size comparison (hypothetical)
46
+ cloudflare-sdk: 2.4 MB (bundled)
47
+ cherry + 3 routes: 12 KB (bundled)
48
+ ```
49
+
50
+ ### The Solution
51
+
52
+ Cherry inverts the architecture:
53
+
54
+ | Traditional SDK | Cherry |
55
+ |-----------------|--------|
56
+ | Monolithic client class | Minimal client factory (~50 lines) |
57
+ | All endpoints registered | Routes are plain imports |
58
+ | Tree-shaking impossible | Only imported routes are bundled |
59
+ | Runtime schema validation optional | Validation built-in (Valibot) |
60
+
61
+ **Routes are data, not code.** They're plain objects describing endpoints — completely decoupled from the client that executes them.
62
+
63
+ ---
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ # npm
69
+ npm install @b3b/cherry valibot
70
+
71
+ # pnpm
72
+ pnpm add @b3b/cherry valibot
73
+
74
+ # bun
75
+ bun add @b3b/cherry valibot
76
+
77
+ # jsr (Deno)
78
+ deno add jsr:@b3b/cherry
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Quick Start
84
+
85
+ ### 1. Define a Route
86
+
87
+ ```ts
88
+ import * as v from "valibot";
89
+ import { defineRoute } from "@b3b/cherry";
90
+
91
+ export const listZones = defineRoute({
92
+ method: "GET",
93
+ path: "/zones",
94
+ params: v.object({
95
+ account_id: v.string(),
96
+ page: v.optional(v.number()),
97
+ }),
98
+ response: v.object({
99
+ result: v.array(v.object({ id: v.string(), name: v.string() })),
100
+ }),
101
+ });
102
+ ```
103
+
104
+ ### 2. Create a Client
105
+
106
+ ```ts
107
+ import { createClient } from "@b3b/cherry";
108
+ import { listZones, getZone, createDnsRecord } from "./routes/cloudflare";
109
+
110
+ const cf = createClient({
111
+ baseUrl: "https://api.cloudflare.com/client/v4",
112
+ headers: () => ({ Authorization: `Bearer ${process.env.CF_TOKEN}` }),
113
+ routes: { listZones, getZone, createDnsRecord },
114
+ });
115
+ ```
116
+
117
+ ### 3. Call Your API
118
+
119
+ ```ts
120
+ // Named method — discoverable, autocomplete-friendly
121
+ const zones = await cf.listZones({ account_id: "abc" });
122
+
123
+ // Generic call — works with any route, even ones not in `routes`
124
+ const zones = await cf.call(listZones, { account_id: "abc" });
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Features
130
+
131
+ ### Dynamic Path Parameters
132
+
133
+ ```ts
134
+ export const getZone = defineRoute({
135
+ method: "GET",
136
+ path: (p) => `/zones/${p.zone_id}`,
137
+ params: v.object({ zone_id: v.string() }),
138
+ response: v.object({ /* ... */ }),
139
+ });
140
+ ```
141
+
142
+ ### Custom Fetcher
143
+
144
+ Replace the underlying fetch logic for logging, retries, auth refresh, etc.
145
+
146
+ ```ts
147
+ createClient({
148
+ baseUrl: "...",
149
+ fetcher: async (req) => {
150
+ console.log(`→ ${req.init.method} ${req.url}`);
151
+ const res = await fetch(req.url, req.init);
152
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
153
+ return res;
154
+ },
155
+ });
156
+ ```
157
+
158
+ ### Composable Middleware
159
+
160
+ Composition is userland — no magic middleware system:
161
+
162
+ ```ts
163
+ import type { Fetcher } from "@b3b/cherry";
164
+
165
+ const withRetry = (fetcher: Fetcher, attempts = 3): Fetcher =>
166
+ async (req) => {
167
+ for (let i = 0; i < attempts; i++) {
168
+ try { return await fetcher(req); }
169
+ catch (e) { if (i === attempts - 1) throw e; }
170
+ }
171
+ throw new Error("unreachable");
172
+ };
173
+
174
+ const withLogging = (fetcher: Fetcher): Fetcher =>
175
+ async (req) => {
176
+ console.log(`→ ${req.init.method} ${req.url}`);
177
+ return fetcher(req);
178
+ };
179
+
180
+ createClient({
181
+ fetcher: withLogging(withRetry(defaultFetcher)),
182
+ });
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Design Principles
188
+
189
+ 1. **Tree-shakeable by default** — Routes are plain imports, not registered in a global client
190
+ 2. **Minimal runtime** — Client is ~50 lines, no dependencies beyond Valibot
191
+ 3. **User owns composition** — No built-in middleware, just a replaceable fetcher
192
+ 4. **Type-safe end-to-end** — Params validated in, response validated out
193
+ 5. **No magic** — Everything is explicit and inspectable
194
+
195
+ ---
196
+
197
+ ## Generating Routes from OpenAPI
198
+
199
+ Cherry includes a generator that transforms OpenAPI 3.x specs into route definitions:
200
+
201
+ ```bash
202
+ cherry generate --input ./openapi.json --output ./routes/
203
+ ```
204
+
205
+ See [ARCHITECTURE.md](./agent/ARCHITECTURE.md) for generator implementation details.
206
+
207
+ ---
208
+
209
+ ## Stack
210
+
211
+ - **Runtime:** Bun
212
+ - **Validation:** Valibot
213
+ - **Language:** TypeScript (strict)
214
+ - **Formatting:** Prettier (100 char width)
215
+ - **Bundling:** tsdown
216
+
217
+ ---
218
+
219
+ ## License
220
+
221
+ MIT
@@ -0,0 +1,170 @@
1
+ import { ResultAsync } from "neverthrow";
2
+ import * as v from "valibot";
3
+ import { BaseSchema, InferInput, InferOutput } from "valibot";
4
+
5
+ //#region src/errors.d.ts
6
+ /** Base error class for all Cherry errors */
7
+ declare abstract class CherryError extends Error {
8
+ abstract readonly type: string;
9
+ abstract readonly retryable: boolean;
10
+ constructor(message: string, options?: {
11
+ cause?: unknown;
12
+ });
13
+ }
14
+ /** HTTP response errors (4xx, 5xx) */
15
+ declare class HttpError extends CherryError {
16
+ readonly status: number;
17
+ readonly statusText: string;
18
+ readonly body?: unknown | undefined;
19
+ readonly type = "HttpError";
20
+ readonly retryable: boolean;
21
+ constructor(status: number, statusText: string, body?: unknown | undefined, cause?: unknown);
22
+ }
23
+ /** Valibot validation errors */
24
+ declare class ValidationError extends CherryError {
25
+ readonly target: "request" | "response";
26
+ readonly issues: unknown[];
27
+ readonly type = "ValidationError";
28
+ readonly retryable = false;
29
+ constructor(target: "request" | "response", issues: unknown[], cause?: unknown);
30
+ }
31
+ /** Network/fetch errors */
32
+ declare class NetworkError extends CherryError {
33
+ readonly type = "NetworkError";
34
+ readonly retryable = true;
35
+ constructor(cause?: unknown);
36
+ }
37
+ /** Serialization errors (e.g., circular references, BigInt in JSON) */
38
+ declare class SerializationError extends CherryError {
39
+ readonly target: "query" | "body";
40
+ readonly key: string;
41
+ readonly type = "SerializationError";
42
+ readonly retryable = false;
43
+ constructor(target: "query" | "body", key: string, cause?: unknown);
44
+ }
45
+ /** Catch-all for unexpected errors */
46
+ declare class UnknownCherryError extends CherryError {
47
+ readonly type = "UnknownCherryError";
48
+ readonly retryable = false;
49
+ constructor(cause?: unknown);
50
+ }
51
+ /** Type guard for CherryError */
52
+ declare function isCherryError(error: unknown): error is CherryError;
53
+ /** Helper to create error ResultAsync */
54
+ declare function cherryErr<T>(error: CherryError): ResultAsync<T, CherryError>;
55
+ //#endregion
56
+ //#region src/types.d.ts
57
+ /** HTTP methods supported by Cherry */
58
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
59
+ /** Path template result from path() tagged template */
60
+ type PathTemplate = {
61
+ template: string;
62
+ paramNames: string[];
63
+ };
64
+ /** Route definition with separated parameter schemas */
65
+ type CherryRoute<TPathParams extends BaseSchema<any, any, any> | undefined = undefined, TQueryParams extends BaseSchema<any, any, any> | undefined = undefined, TBodyParams extends BaseSchema<any, any, any> | undefined = undefined, TResponse extends BaseSchema<any, any, any> = BaseSchema<any, any, any>> = {
66
+ method: HttpMethod;
67
+ path: PathTemplate;
68
+ pathParams?: TPathParams;
69
+ queryParams?: TQueryParams;
70
+ bodyParams?: TBodyParams;
71
+ response: TResponse;
72
+ queryParamOptions?: QueryParamOptions;
73
+ description?: string;
74
+ };
75
+ /** Options for query parameter serialization */
76
+ type QueryParamOptions = {
77
+ arrayFormat?: "repeat" | "comma" | "brackets" | "json";
78
+ customSerializer?: (params: Record<string, unknown>) => string;
79
+ };
80
+ type Prettify<T> = { [K in keyof T]: T[K] } & {};
81
+ /** Infer combined input params from a route */
82
+ type InferRouteInput<T> = T extends CherryRoute<infer TPath, infer TQuery, infer TBody, any> ? Prettify<(TPath extends BaseSchema<any, any, any> ? InferInput<TPath> : {}) & (TQuery extends BaseSchema<any, any, any> ? InferInput<TQuery> : {}) & (TBody extends BaseSchema<any, any, any> ? InferInput<TBody> : {})> : never;
83
+ /** Infer response output from a route */
84
+ type InferRouteOutput<T extends CherryRoute<any, any, any, any>> = T["response"] extends BaseSchema<any, any, any> ? InferOutput<T["response"]> : never;
85
+ /** Cherry result type - always ResultAsync */
86
+ type CherryResult<T> = ResultAsync<T, CherryError>;
87
+ /** Fetcher request shape (extensible for middleware) */
88
+ type FetchRequest = {
89
+ url: string;
90
+ init: RequestInit;
91
+ };
92
+ /** Fetcher function signature */
93
+ type Fetcher = (req: FetchRequest) => Promise<Response>;
94
+ /** Route tree (supports namespacing via nested objects) */
95
+ type RouteTree = {
96
+ [key: string]: CherryRoute<any, any, any, any> | RouteTree;
97
+ };
98
+ /** Client configuration */
99
+ type ClientConfig<TRoutes extends RouteTree | undefined = undefined> = {
100
+ baseUrl: string;
101
+ headers?: () => Record<string, string> | Promise<Record<string, string>>;
102
+ fetcher?: Fetcher;
103
+ routes?: TRoutes;
104
+ };
105
+ type Client<TRoutes extends RouteTree | undefined = undefined> = {
106
+ call: <T extends CherryRoute<any, any, any, any>>(route: T, params: InferRouteInput<T>) => CherryResult<InferRouteOutput<T>>;
107
+ } & (TRoutes extends RouteTree ? RoutesToClient<TRoutes> : {});
108
+ /** Convert a nested route tree into a nested client method tree */
109
+ type RoutesToClient<TRoutes extends RouteTree> = { [K in keyof TRoutes]: TRoutes[K] extends CherryRoute<any, any, any, any> ? (params: InferRouteInput<TRoutes[K]>) => CherryResult<InferRouteOutput<TRoutes[K]>> : TRoutes[K] extends RouteTree ? RoutesToClient<TRoutes[K]> : never };
110
+ //#endregion
111
+ //#region src/cherry_client.d.ts
112
+ declare function serializeQueryParams(params: Record<string, unknown>, options?: QueryParamOptions): string;
113
+ declare function createCherryClient<TRoutes extends RouteTree | undefined = undefined>(config: ClientConfig<TRoutes>): Client<TRoutes>;
114
+ //#endregion
115
+ //#region src/route.d.ts
116
+ type RouteConfig<TPathParams extends v.BaseSchema<any, any, any> | undefined, TQueryParams extends v.BaseSchema<any, any, any> | undefined, TBodyParams extends v.BaseSchema<any, any, any> | undefined, TResponse extends v.BaseSchema<any, any, any>> = {
117
+ method: HttpMethod;
118
+ path: PathTemplate;
119
+ pathParams?: TPathParams;
120
+ queryParams?: TQueryParams;
121
+ bodyParams?: TBodyParams;
122
+ response: TResponse;
123
+ queryParamOptions?: QueryParamOptions;
124
+ description?: string;
125
+ };
126
+ declare function route<TPathParams extends v.BaseSchema<any, any, any> | undefined, TQueryParams extends v.BaseSchema<any, any, any> | undefined, TBodyParams extends v.BaseSchema<any, any, any> | undefined, TResponse extends v.BaseSchema<any, any, any>>(config: RouteConfig<TPathParams, TQueryParams, TBodyParams, TResponse>): CherryRoute<TPathParams, TQueryParams, TBodyParams, TResponse>;
127
+ //#endregion
128
+ //#region src/path.d.ts
129
+ /** Branded type for path parameter markers */
130
+ declare const PathParamBrand: unique symbol;
131
+ type PathParam<T extends string = string> = string & {
132
+ readonly [PathParamBrand]: T;
133
+ };
134
+ /** Branded type for optional path parameter markers */
135
+ declare const OptionalParamBrand: unique symbol;
136
+ type OptionalParam<T extends string = string> = string & {
137
+ readonly [OptionalParamBrand]: T;
138
+ };
139
+ /** Union type for any path param marker */
140
+ type AnyPathParam = PathParam<string> | OptionalParam<string>;
141
+ /** Create a path parameter marker */
142
+ declare function param<T extends string>(name: T): PathParam<T>;
143
+ /** Create an optional path parameter marker */
144
+ declare function optional<T extends string>(name: T): OptionalParam<T>;
145
+ /**
146
+ * Tagged template for building path templates.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * // Simple path with one param
151
+ * const userPath = path`/users/${param("id")}`;
152
+ * // { template: "/users/:id", paramNames: ["id"] }
153
+ *
154
+ * // Multiple params
155
+ * const postPath = path`/users/${param("userId")}/posts/${param("postId")}`;
156
+ * // { template: "/users/:userId/posts/:postId", paramNames: ["userId", "postId"] }
157
+ *
158
+ * // Optional params
159
+ * const versionedPath = path`/api${optional("version")}/users`;
160
+ * // { template: "/api(:version)/users", paramNames: ["version"] }
161
+ *
162
+ * // No params
163
+ * const staticPath = path`/health`;
164
+ * // { template: "/health", paramNames: [] }
165
+ * ```
166
+ */
167
+ declare function path(strings: TemplateStringsArray, ...params: AnyPathParam[]): PathTemplate;
168
+ //#endregion
169
+ 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 RouteConfig, type RouteTree, type RoutesToClient, SerializationError, UnknownCherryError, ValidationError, cherryErr, createCherryClient, isCherryError, optional, param, path, route, serializeQueryParams };
170
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ import { ResultAsync, errAsync } from "neverthrow";
2
+ import * as v from "valibot";
3
+
4
+ //#region src/errors.ts
5
+ /** Base error class for all Cherry errors */
6
+ var CherryError = class extends Error {
7
+ constructor(message, options) {
8
+ super(message, options);
9
+ this.name = this.constructor.name;
10
+ }
11
+ };
12
+ /** HTTP response errors (4xx, 5xx) */
13
+ var HttpError = class extends CherryError {
14
+ type = "HttpError";
15
+ retryable;
16
+ constructor(status, statusText, body, cause) {
17
+ super(`HTTP ${status}: ${statusText}`, { cause });
18
+ this.status = status;
19
+ this.statusText = statusText;
20
+ this.body = body;
21
+ this.retryable = status >= 500 || status === 429;
22
+ }
23
+ };
24
+ /** Valibot validation errors */
25
+ var ValidationError = class extends CherryError {
26
+ type = "ValidationError";
27
+ retryable = false;
28
+ constructor(target, issues, cause) {
29
+ super(`Validation failed for ${target}`, { cause });
30
+ this.target = target;
31
+ this.issues = issues;
32
+ }
33
+ };
34
+ /** Network/fetch errors */
35
+ var NetworkError = class extends CherryError {
36
+ type = "NetworkError";
37
+ retryable = true;
38
+ constructor(cause) {
39
+ super(`Network error`, { cause });
40
+ }
41
+ };
42
+ /** Serialization errors (e.g., circular references, BigInt in JSON) */
43
+ var SerializationError = class extends CherryError {
44
+ type = "SerializationError";
45
+ retryable = false;
46
+ constructor(target, key, cause) {
47
+ super(`Failed to serialize ${target} parameter "${key}"`, { cause });
48
+ this.target = target;
49
+ this.key = key;
50
+ }
51
+ };
52
+ /** Catch-all for unexpected errors */
53
+ var UnknownCherryError = class extends CherryError {
54
+ type = "UnknownCherryError";
55
+ retryable = false;
56
+ constructor(cause) {
57
+ super(`Unknown error`, { cause });
58
+ }
59
+ };
60
+ /** Type guard for CherryError */
61
+ function isCherryError(error) {
62
+ return error instanceof CherryError;
63
+ }
64
+ /** Helper to create error ResultAsync */
65
+ function cherryErr(error) {
66
+ return errAsync(error);
67
+ }
68
+
69
+ //#endregion
70
+ //#region src/cherry_client.ts
71
+ const defaultFetcher = (req) => fetch(req.url, req.init);
72
+ function serializeQueryParams(params, options) {
73
+ if (options?.customSerializer) return options.customSerializer(params);
74
+ const searchParams = new URLSearchParams();
75
+ for (const [key, value] of Object.entries(params)) {
76
+ if (value === void 0 || value === null) continue;
77
+ if (Array.isArray(value)) switch (options?.arrayFormat ?? "repeat") {
78
+ case "repeat":
79
+ for (const item of value) searchParams.append(key, String(item));
80
+ break;
81
+ case "comma":
82
+ searchParams.set(key, value.join(","));
83
+ break;
84
+ case "brackets":
85
+ for (const item of value) searchParams.append(`${key}[]`, String(item));
86
+ break;
87
+ case "json":
88
+ try {
89
+ searchParams.set(key, JSON.stringify(value));
90
+ } catch (error) {
91
+ throw new SerializationError("query", key, error);
92
+ }
93
+ break;
94
+ }
95
+ else searchParams.set(key, String(value));
96
+ }
97
+ return searchParams.toString();
98
+ }
99
+ function createCherryClient(config) {
100
+ const fetcher = config.fetcher ?? defaultFetcher;
101
+ function call(route$1, params) {
102
+ return ResultAsync.fromPromise(executeRoute(route$1, params), (error) => {
103
+ if (error instanceof HttpError) return error;
104
+ if (error instanceof ValidationError) return error;
105
+ if (error instanceof NetworkError) return error;
106
+ if (error instanceof SerializationError) return error;
107
+ return new UnknownCherryError(error);
108
+ });
109
+ }
110
+ async function executeRoute(route$1, params) {
111
+ let pathParams = {};
112
+ if (route$1.pathParams) {
113
+ const result$1 = v.safeParse(route$1.pathParams, params);
114
+ if (!result$1.success) throw new ValidationError("request", result$1.issues);
115
+ pathParams = result$1.output;
116
+ }
117
+ let queryParams = {};
118
+ if (route$1.queryParams) {
119
+ const result$1 = v.safeParse(route$1.queryParams, params);
120
+ if (!result$1.success) throw new ValidationError("request", result$1.issues);
121
+ queryParams = result$1.output;
122
+ }
123
+ let bodyParams;
124
+ if (route$1.bodyParams) {
125
+ const result$1 = v.safeParse(route$1.bodyParams, params);
126
+ if (!result$1.success) throw new ValidationError("request", result$1.issues);
127
+ bodyParams = result$1.output;
128
+ }
129
+ let url = route$1.path.template;
130
+ for (const [key, value] of Object.entries(pathParams)) url = url.replace(`:${key}`, encodeURIComponent(String(value)));
131
+ const fullUrl = new URL(url, config.baseUrl);
132
+ if (Object.keys(queryParams).length > 0) fullUrl.search = serializeQueryParams(queryParams, route$1.queryParamOptions);
133
+ const headers = {
134
+ "Content-Type": "application/json",
135
+ ...await config.headers?.()
136
+ };
137
+ const init = {
138
+ method: route$1.method,
139
+ headers
140
+ };
141
+ if (bodyParams && route$1.method !== "GET") init.body = JSON.stringify(bodyParams);
142
+ const req = {
143
+ url: fullUrl.toString(),
144
+ init
145
+ };
146
+ let response;
147
+ try {
148
+ response = await fetcher(req);
149
+ } catch (error) {
150
+ throw new NetworkError(error);
151
+ }
152
+ if (!response.ok) {
153
+ const body = await response.text().catch(() => void 0);
154
+ throw new HttpError(response.status, response.statusText, body);
155
+ }
156
+ const json = await response.json();
157
+ const result = v.safeParse(route$1.response, json);
158
+ if (!result.success) throw new ValidationError("response", result.issues);
159
+ return result.output;
160
+ }
161
+ function forgeRouteMethods(routes) {
162
+ const out = {};
163
+ for (const [key, value] of Object.entries(routes)) if (value && typeof value === "object" && "method" in value && "path" in value) out[key] = (params) => call(value, params);
164
+ else if (value && typeof value === "object") out[key] = forgeRouteMethods(value);
165
+ return out;
166
+ }
167
+ return {
168
+ call,
169
+ ...config.routes ? forgeRouteMethods(config.routes) : {}
170
+ };
171
+ }
172
+
173
+ //#endregion
174
+ //#region src/route.ts
175
+ const HttpMethodSchema = v.picklist([
176
+ "GET",
177
+ "POST",
178
+ "PUT",
179
+ "PATCH",
180
+ "DELETE"
181
+ ]);
182
+ function route(config) {
183
+ v.parse(HttpMethodSchema, config.method);
184
+ if (config.path.paramNames.length > 0) {
185
+ if (!config.pathParams) throw new Error(`Route has path params [${config.path.paramNames.join(", ")}] but no pathParams schema`);
186
+ const schemaKeys = getSchemaKeys(config.pathParams);
187
+ for (const paramName of config.path.paramNames) if (!schemaKeys.includes(paramName)) throw new Error(`Path param ":${paramName}" not found in pathParams schema. Available: [${schemaKeys.join(", ")}]`);
188
+ for (const schemaKey of schemaKeys) if (!config.path.paramNames.includes(schemaKey)) throw new Error(`pathParams schema key "${schemaKey}" not present in path template. Template params: [${config.path.paramNames.join(", ")}]`);
189
+ }
190
+ return config;
191
+ }
192
+ function getSchemaKeys(schema) {
193
+ if ("entries" in schema && typeof schema.entries === "object" && schema.entries !== null) return Object.keys(schema.entries);
194
+ return [];
195
+ }
196
+
197
+ //#endregion
198
+ //#region src/path.ts
199
+ /** Create a path parameter marker */
200
+ function param(name) {
201
+ return `:${name}`;
202
+ }
203
+ /** Create an optional path parameter marker */
204
+ function optional(name) {
205
+ return `(:${name})`;
206
+ }
207
+ /**
208
+ * Tagged template for building path templates.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * // Simple path with one param
213
+ * const userPath = path`/users/${param("id")}`;
214
+ * // { template: "/users/:id", paramNames: ["id"] }
215
+ *
216
+ * // Multiple params
217
+ * const postPath = path`/users/${param("userId")}/posts/${param("postId")}`;
218
+ * // { template: "/users/:userId/posts/:postId", paramNames: ["userId", "postId"] }
219
+ *
220
+ * // Optional params
221
+ * const versionedPath = path`/api${optional("version")}/users`;
222
+ * // { template: "/api(:version)/users", paramNames: ["version"] }
223
+ *
224
+ * // No params
225
+ * const staticPath = path`/health`;
226
+ * // { template: "/health", paramNames: [] }
227
+ * ```
228
+ */
229
+ function path(strings, ...params) {
230
+ const paramNames = [];
231
+ let template = strings[0];
232
+ for (let i = 0; i < params.length; i++) {
233
+ const p = params[i];
234
+ template += p + strings[i + 1];
235
+ const match = p.match(/^\(?:(\w+)\)?$/);
236
+ if (match) paramNames.push(match[1]);
237
+ }
238
+ return {
239
+ template,
240
+ paramNames
241
+ };
242
+ }
243
+
244
+ //#endregion
245
+ export { CherryError, HttpError, NetworkError, SerializationError, UnknownCherryError, ValidationError, cherryErr, createCherryClient, isCherryError, optional, param, path, route, serializeQueryParams };
246
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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,\n TQueryParams extends v.BaseSchema<any, any, any> | undefined,\n TBodyParams extends v.BaseSchema<any, any, any> | undefined,\n TResponse extends 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,\n TQueryParams extends v.BaseSchema<any, any, any> | undefined,\n TBodyParams extends v.BaseSchema<any, any, any> | undefined,\n TResponse extends 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":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@b3-business/cherry",
3
+ "version": "0.1.0",
4
+ "description": "A tree-shakeable, minimal API client factory. Import only the routes you need — nothing more.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsdown",
21
+ "check": "run-s lint typecheck typecheck-test",
22
+ "lint": "oxlint src test",
23
+ "typecheck": "tsc --noEmit -p .",
24
+ "typecheck-test": "tsc --noEmit -p tsconfig.test.json",
25
+ "test": "bun test"
26
+ },
27
+ "dependencies": {
28
+ "neverthrow": "^8.2.0",
29
+ "valibot": "^1.2.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/bun": "^1.3.5",
33
+ "expect-type": "^1.3.0",
34
+ "npm-run-all": "^4.1.5",
35
+ "oxlint": "^1.36.0",
36
+ "tsdown": "^0.19.0-beta.2",
37
+ "typescript": "^5.9.3"
38
+ }
39
+ }