UNPKG

zodsei

Version:

Contract-first type-safe HTTP client with Zod validation

1 lines 75.2 kB
{"version":3,"sources":["../src/errors.ts","../src/adapters/fetch.ts","../src/adapters/axios.ts","../src/adapters/ky.ts","../src/validation.ts","../src/utils/path.ts","../src/middleware/index.ts","../src/adapters/index.ts","../src/schema.ts","../src/client.ts","../src/types.ts","../src/index.ts","../src/middleware/retry.ts","../src/middleware/cache.ts","../src/utils/request.ts"],"sourcesContent":["import { z } from 'zod';\n\n// Base error class\nexport class ZodseiError extends Error {\n constructor(message: string, public readonly code: string) {\n super(message);\n this.name = 'ZodseiError';\n }\n}\n\n// Validation error\nexport class ValidationError extends ZodseiError {\n constructor(\n message: string,\n public readonly issues: z.ZodIssue[],\n public readonly type: 'request' | 'response' = 'request'\n ) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n\n static fromZodError(error: z.ZodError, type: 'request' | 'response' = 'request'): ValidationError {\n const message = `${type} validation failed: ${error.issues.map(issue => \n `${issue.path.join('.')}: ${issue.message}`\n ).join(', ')}`;\n \n return new ValidationError(message, error.issues, type);\n }\n}\n\n// HTTP error\nexport class HttpError extends ZodseiError {\n constructor(\n message: string,\n public readonly status: number,\n public readonly statusText: string,\n public readonly response?: any\n ) {\n super(message, 'HTTP_ERROR');\n this.name = 'HttpError';\n }\n\n static fromResponse(response: Response, data?: any): HttpError {\n const message = `HTTP ${response.status}: ${response.statusText}`;\n return new HttpError(message, response.status, response.statusText, data);\n }\n}\n\n// Network error\nexport class NetworkError extends ZodseiError {\n constructor(message: string, public readonly originalError: Error) {\n super(message, 'NETWORK_ERROR');\n this.name = 'NetworkError';\n }\n}\n\n// Configuration error\nexport class ConfigError extends ZodseiError {\n constructor(message: string) {\n super(message, 'CONFIG_ERROR');\n this.name = 'ConfigError';\n }\n}\n\n// Timeout error\nexport class TimeoutError extends ZodseiError {\n constructor(timeout: number) {\n super(`Request timeout after ${timeout}ms`, 'TIMEOUT_ERROR');\n this.name = 'TimeoutError';\n }\n}\n","import { HttpAdapter } from './index';\nimport { RequestContext, ResponseContext } from '../types';\nimport { HttpError, NetworkError, TimeoutError } from '../errors';\n\n/**\n * Fetch adapter configuration\n */\nexport interface FetchAdapterConfig extends RequestInit {\n timeout?: number;\n}\n\n/**\n * Fetch HTTP adapter\n */\nexport class FetchAdapter implements HttpAdapter {\n readonly name = 'fetch';\n private config: FetchAdapterConfig;\n\n constructor(config: FetchAdapterConfig = {}) {\n this.config = {\n timeout: 30000,\n ...config,\n };\n }\n\n async request(context: RequestContext): Promise<ResponseContext> {\n try {\n const init = this.createRequestInit(context);\n const response = await fetch(context.url, init);\n\n // Parse response data\n const data = await this.parseResponseData(response);\n\n // Build response context\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n const responseContext: ResponseContext = {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data,\n };\n\n // Check HTTP status\n if (!response.ok) {\n throw HttpError.fromResponse(response, data);\n }\n\n return responseContext;\n } catch (error) {\n if (error instanceof HttpError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new TimeoutError(this.config.timeout || 0);\n }\n throw new NetworkError(`Fetch request failed: ${error.message}`, error);\n }\n\n throw new NetworkError('Unknown fetch error', error as Error);\n }\n }\n\n private createRequestInit(context: RequestContext): RequestInit {\n const init: RequestInit = {\n method: context.method.toUpperCase(),\n headers: {\n 'Content-Type': 'application/json',\n ...context.headers,\n },\n credentials: this.config.credentials,\n mode: this.config.mode,\n cache: this.config.cache,\n redirect: this.config.redirect,\n referrer: this.config.referrer,\n referrerPolicy: this.config.referrerPolicy,\n integrity: this.config.integrity,\n };\n\n // Add request body\n if (context.body !== undefined && !['GET', 'HEAD'].includes(context.method.toUpperCase())) {\n if (typeof context.body === 'object') {\n init.body = JSON.stringify(context.body);\n } else {\n init.body = context.body;\n }\n }\n\n // Add timeout control\n if (this.config.timeout && this.config.timeout > 0) {\n const controller = new AbortController();\n setTimeout(() => controller.abort(), this.config.timeout);\n init.signal = controller.signal;\n }\n\n return init;\n }\n\n private async parseResponseData(response: Response): Promise<any> {\n const contentType = response.headers.get('content-type');\n\n if (contentType?.includes('application/json')) {\n return response.json();\n } else if (contentType?.includes('text/')) {\n return response.text();\n } else if (\n contentType?.includes('application/octet-stream') ||\n contentType?.includes('application/pdf')\n ) {\n return response.blob();\n } else {\n // Try to parse as JSON, fallback to text\n try {\n return await response.json();\n } catch {\n return response.text();\n }\n }\n }\n}\n","import type { HttpAdapter } from './index';\nimport type { RequestContext, ResponseContext } from '../types';\nimport { HttpError, NetworkError, TimeoutError } from '../errors';\nimport type { CreateAxiosDefaults } from 'axios';\n\n/**\n * Axios adapter configuration\n */\nexport type AxiosAdapterConfig = CreateAxiosDefaults;\n\n/**\n * Axios HTTP adapter\n */\nexport class AxiosAdapter implements HttpAdapter {\n readonly name = 'axios';\n private config: AxiosAdapterConfig;\n private axios: any;\n\n constructor(config: AxiosAdapterConfig = {}) {\n this.config = {\n timeout: 30000,\n validateStatus: () => true, // We handle status validation ourselves\n ...config,\n };\n }\n\n private async getAxios() {\n if (!this.axios) {\n try {\n const axiosModule = await import('axios');\n this.axios = axiosModule.default || axiosModule;\n } catch (_error) {\n throw new Error('axios is not installed. Please install it with: npm install axios');\n }\n }\n return this.axios;\n }\n\n // Interceptors are not supported. Use middleware in the client instead.\n\n async request(context: RequestContext): Promise<ResponseContext> {\n try {\n const axios = await this.getAxios();\n const axiosConfig = this.createAxiosConfig(context);\n\n const response = await axios.request(axiosConfig);\n\n const responseContext: ResponseContext = {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers || {},\n data: response.data,\n };\n\n // Check HTTP status\n if (response.status >= 400) {\n throw new HttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n response.statusText,\n response.data\n );\n }\n\n return responseContext;\n } catch (error: any) {\n if (error instanceof HttpError) {\n throw error;\n }\n\n // Handle Axios errors\n if (error.isAxiosError) {\n if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {\n throw new TimeoutError(this.config.timeout || 0);\n }\n\n if (error.response) {\n // Server responded with error status code\n throw new HttpError(\n `HTTP ${error.response.status}: ${error.response.statusText}`,\n error.response.status,\n error.response.statusText,\n error.response.data\n );\n } else if (error.request) {\n // Request was made but no response received\n throw new NetworkError(`Network request failed: ${error.message}`, error);\n }\n }\n\n throw new NetworkError(`Axios request failed: ${error.message}`, error);\n }\n }\n\n private createAxiosConfig(context: RequestContext): any {\n const config: any = {\n url: context.url,\n method: context.method.toLowerCase(),\n headers: {\n 'Content-Type': 'application/json',\n ...context.headers,\n },\n timeout: this.config.timeout,\n maxRedirects: this.config.maxRedirects,\n validateStatus: this.config.validateStatus,\n maxContentLength: this.config.maxContentLength,\n maxBodyLength: this.config.maxBodyLength,\n withCredentials: this.config.withCredentials,\n auth: this.config.auth,\n proxy: this.config.proxy,\n };\n\n // Add request body\n if (context.body !== undefined && !['GET', 'HEAD'].includes(context.method.toUpperCase())) {\n config.data = context.body;\n }\n\n // Add query parameters\n if (context.query && Object.keys(context.query).length > 0) {\n config.params = context.query;\n }\n\n return config;\n }\n}\n","import { HttpAdapter } from './index';\nimport { RequestContext, ResponseContext } from '../types';\nimport { HttpError, NetworkError, TimeoutError } from '../errors';\nimport type { Options as KyOptions } from 'ky';\n/**\n * Ky adapter configuration\n */\nexport type KyAdapterConfig = KyOptions;\n\n/**\n * Ky HTTP adapter\n */\nexport class KyAdapter implements HttpAdapter {\n readonly name = 'ky';\n private config: KyAdapterConfig;\n private ky: any;\n\n constructor(config: KyAdapterConfig = {}) {\n this.config = {\n timeout: 30000,\n throwHttpErrors: false, // We handle errors ourselves\n ...config,\n };\n }\n\n private async getKy() {\n if (!this.ky) {\n try {\n const kyModule = await import('ky');\n this.ky = kyModule.default || kyModule;\n } catch (_error) {\n throw new Error('ky is not installed. Please install it with: npm install ky');\n }\n }\n return this.ky;\n }\n\n async request(context: RequestContext): Promise<ResponseContext> {\n try {\n const ky = await this.getKy();\n const kyOptions = this.createKyOptions(context);\n\n const response = await ky(context.url, kyOptions);\n\n // Parse response data\n const data = await this.parseResponseData(response);\n\n // Build response headers\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value: string, key: string) => {\n responseHeaders[key] = value;\n });\n\n const responseContext: ResponseContext = {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data,\n };\n\n // Check HTTP status\n if (!response.ok) {\n throw new HttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n response.statusText,\n data\n );\n }\n\n return responseContext;\n } catch (error: any) {\n if (error instanceof HttpError) {\n throw error;\n }\n\n // Handle Ky errors\n if (error.name === 'HTTPError') {\n const response = error.response;\n const data = await this.parseResponseData(response).catch(() => null);\n\n throw new HttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n response.statusText,\n data\n );\n }\n\n if (error.name === 'TimeoutError') {\n throw new TimeoutError(this.config.timeout || 0);\n }\n\n throw new NetworkError(`Ky request failed: ${error.message}`, error);\n }\n }\n\n private createKyOptions(context: RequestContext): any {\n const options: any = {\n method: context.method.toLowerCase(),\n headers: {\n 'Content-Type': 'application/json',\n ...context.headers,\n },\n timeout: this.config.timeout,\n retry: this.config.retry,\n throwHttpErrors: this.config.throwHttpErrors,\n credentials: this.config.credentials,\n mode: this.config.mode,\n cache: this.config.cache,\n redirect: this.config.redirect,\n referrer: this.config.referrer,\n referrerPolicy: this.config.referrerPolicy,\n integrity: this.config.integrity,\n };\n\n // Add request body\n if (context.body !== undefined && !['GET', 'HEAD'].includes(context.method.toUpperCase())) {\n if (typeof context.body === 'object') {\n options.json = context.body;\n } else {\n options.body = context.body;\n }\n }\n\n // Add query parameters\n if (context.query && Object.keys(context.query).length > 0) {\n options.searchParams = context.query;\n }\n\n return options;\n }\n\n private async parseResponseData(response: any): Promise<any> {\n const contentType = response.headers.get('content-type');\n\n if (contentType?.includes('application/json')) {\n return response.json();\n } else if (contentType?.includes('text/')) {\n return response.text();\n } else if (\n contentType?.includes('application/octet-stream') ||\n contentType?.includes('application/pdf')\n ) {\n return response.blob();\n } else {\n // Try to parse as JSON, fallback to text\n try {\n return await response.json();\n } catch {\n return response.text();\n }\n }\n }\n}\n","import { z } from 'zod';\nimport { ValidationError } from './errors';\n\n/**\n * Validation utility functions\n */\n\n// Validate request data\nexport function validateRequest<T>(schema: z.ZodSchema<T> | undefined, data: unknown): T {\n if (!schema) {\n return data as T;\n }\n \n try {\n return schema.parse(data);\n } catch (error) {\n if (error instanceof z.ZodError) {\n throw ValidationError.fromZodError(error, 'request');\n }\n throw error;\n }\n}\n\n// Validate response data\nexport function validateResponse<T>(schema: z.ZodSchema<T> | undefined, data: unknown): T {\n if (!schema) {\n return data as T;\n }\n \n try {\n return schema.parse(data);\n } catch (error) {\n if (error instanceof z.ZodError) {\n throw ValidationError.fromZodError(error, 'response');\n }\n throw error;\n }\n}\n\n// Safe parse (no error throwing)\nexport function safeParseRequest<T>(\n schema: z.ZodSchema<T> | undefined, \n data: unknown\n): { success: true; data: T } | { success: false; error: ValidationError } {\n if (!schema) {\n return { success: true, data: data as T };\n }\n \n try {\n const result = schema.parse(data);\n return { success: true, data: result };\n } catch (error) {\n if (error instanceof z.ZodError) {\n return { success: false, error: ValidationError.fromZodError(error, 'request') };\n }\n return { \n success: false, \n error: new ValidationError('Unknown validation error', [], 'request') \n };\n }\n}\n\n// Safe parse response\nexport function safeParseResponse<T>(\n schema: z.ZodSchema<T> | undefined, \n data: unknown\n): { success: true; data: T } | { success: false; error: ValidationError } {\n if (!schema) {\n return { success: true, data: data as T };\n }\n \n try {\n const result = schema.parse(data);\n return { success: true, data: result };\n } catch (error) {\n if (error instanceof z.ZodError) {\n return { success: false, error: ValidationError.fromZodError(error, 'response') };\n }\n return { \n success: false, \n error: new ValidationError('Unknown validation error', [], 'response') \n };\n }\n}\n\n// Create optional validator\nexport function createValidator<T>(schema: z.ZodSchema<T> | undefined, enabled: boolean) {\n return {\n validateRequest: enabled \n ? (data: unknown) => validateRequest(schema, data)\n : (data: unknown) => data as T,\n \n validateResponse: enabled \n ? (data: unknown) => validateResponse(schema, data)\n : (data: unknown) => data as T,\n \n safeParseRequest: (data: unknown) => safeParseRequest(schema, data),\n safeParseResponse: (data: unknown) => safeParseResponse(schema, data),\n };\n}\n","/**\n * Path handling utility functions\n */\n\n// Extract path parameter names\nexport function extractPathParamNames(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n return matches ? matches.map(match => match.slice(1)) : [];\n}\n\n// Replace path parameters\nexport function replacePath(path: string, params: Record<string, string>): string {\n let result = path;\n \n for (const [key, value] of Object.entries(params)) {\n result = result.replace(`:${key}`, encodeURIComponent(value));\n }\n \n return result;\n}\n\n// Build query string\nexport function buildQueryString(params: Record<string, any>): string {\n const searchParams = new URLSearchParams();\n \n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n value.forEach(item => searchParams.append(key, String(item)));\n } else {\n searchParams.append(key, String(value));\n }\n }\n }\n \n const queryString = searchParams.toString();\n return queryString ? `?${queryString}` : '';\n}\n\n// Build complete URL\nexport function buildUrl(baseUrl: string, path: string, query?: Record<string, any>): string {\n const cleanBaseUrl = baseUrl.replace(/\\/$/, '');\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n const queryString = query ? buildQueryString(query) : '';\n \n return `${cleanBaseUrl}${cleanPath}${queryString}`;\n}\n\n// Separate path params and query params\nexport function separateParams(\n path: string, \n data: Record<string, any> | null | undefined\n): { pathParams: Record<string, string>; queryParams: Record<string, any> } {\n const pathParamNames = extractPathParamNames(path);\n const pathParams: Record<string, string> = {};\n const queryParams: Record<string, any> = {};\n \n // Handle null/undefined data\n if (!data) {\n return { pathParams, queryParams };\n }\n \n for (const [key, value] of Object.entries(data)) {\n if (pathParamNames.includes(key)) {\n pathParams[key] = String(value);\n } else {\n queryParams[key] = value;\n }\n }\n \n return { pathParams, queryParams };\n}\n\n// Check if request body is needed\nexport function shouldHaveBody(method: string): boolean {\n return !['GET', 'HEAD', 'DELETE'].includes(method.toUpperCase());\n}\n","import type { Middleware, RequestContext, ResponseContext } from '../types';\n\n/**\n * Middleware system\n */\n\n// Middleware executor\nexport class MiddlewareExecutor {\n constructor(private middleware: Middleware[] = []) {}\n\n // Execute middleware chain\n async execute(\n request: RequestContext,\n finalHandler: (request: RequestContext) => Promise<ResponseContext>\n ): Promise<ResponseContext> {\n if (this.middleware.length === 0) {\n return finalHandler(request);\n }\n\n let index = 0;\n\n const next = async (req: RequestContext): Promise<ResponseContext> => {\n if (index >= this.middleware.length) {\n return finalHandler(req);\n }\n\n const middleware = this.middleware[index++];\n return middleware(req, next);\n };\n\n return next(request);\n }\n\n // Add middleware\n use(middleware: Middleware): void {\n this.middleware.push(middleware);\n }\n\n // Get middleware list\n getMiddleware(): Middleware[] {\n return [...this.middleware];\n }\n}\n\n// Create middleware executor\nexport function createMiddlewareExecutor(middleware: Middleware[] = []): MiddlewareExecutor {\n return new MiddlewareExecutor(middleware);\n}\n\n// Compose multiple middleware\nexport function composeMiddleware(...middleware: Middleware[]): Middleware {\n return async (request, next) => {\n const executor = new MiddlewareExecutor(middleware);\n return executor.execute(request, next);\n };\n}\n","import { RequestContext, ResponseContext } from '../types';\n\n/**\n * HTTP adapter interface\n */\nexport interface HttpAdapter {\n /**\n * Execute HTTP request\n */\n request(context: RequestContext): Promise<ResponseContext>;\n\n /**\n * Adapter name\n */\n readonly name: string;\n}\n\n/**\n * Supported adapter types\n */\nexport type AdapterType = 'fetch' | 'axios' | 'ky';\n\n/**\n * Adapter factory function\n */\nexport async function createAdapter(\n type: AdapterType,\n config?: Record<string, unknown>\n): Promise<HttpAdapter> {\n // Create adapter based on type\n switch (type) {\n case 'fetch': {\n const { FetchAdapter } = await import('./fetch');\n return new FetchAdapter(config);\n }\n\n case 'axios': {\n const { AxiosAdapter } = await import('./axios');\n return new AxiosAdapter(config);\n }\n\n case 'ky': {\n const { KyAdapter } = await import('./ky');\n return new KyAdapter(config);\n }\n\n default:\n throw new Error(`Unsupported adapter type: ${type}`);\n }\n}\n\n/**\n * Check if adapter is available\n */\nexport async function isAdapterAvailable(type: AdapterType): Promise<boolean> {\n try {\n switch (type) {\n case 'fetch':\n return typeof fetch !== 'undefined';\n\n case 'axios':\n await import('axios');\n return true;\n\n case 'ky':\n await import('ky');\n return true;\n\n default:\n return false;\n }\n } catch {\n return false;\n }\n}\n\n/**\n * Get default adapter\n */\nexport async function getDefaultAdapter(config?: Record<string, any>): Promise<HttpAdapter> {\n // Prefer fetch (built-in)\n if (await isAdapterAvailable('fetch')) {\n return createAdapter('fetch', config);\n }\n\n // Try axios next\n if (await isAdapterAvailable('axios')) {\n return createAdapter('axios', config);\n }\n\n // Finally try ky\n if (await isAdapterAvailable('ky')) {\n return createAdapter('ky', config);\n }\n\n throw new Error(\n 'No HTTP adapter available. Please install axios or ky, or ensure fetch is available.'\n );\n}\n","import { z } from 'zod';\nimport type { Contract, EndpointDefinition } from './types';\n\n/**\n * Schema inference and extraction utilities\n */\n\n/**\n * Extract request type from endpoint definition\n */\nexport type InferRequestType<T extends EndpointDefinition> = \n T['request'] extends z.ZodSchema ? z.infer<T['request']> : void;\n\n/**\n * Extract response type from endpoint definition\n */\nexport type InferResponseType<T extends EndpointDefinition> = \n T['response'] extends z.ZodSchema ? z.infer<T['response']> : unknown;\n\n/**\n * Extract all endpoint types from a contract\n */\nexport type InferContractTypes<T extends Contract> = {\n [K in keyof T]: T[K] extends EndpointDefinition\n ? {\n request: InferRequestType<T[K]>;\n response: InferResponseType<T[K]>;\n endpoint: T[K];\n }\n : T[K] extends Contract\n ? InferContractTypes<T[K]>\n : never;\n};\n\n/**\n * Schema extraction utilities\n */\nexport class SchemaExtractor<T extends Contract> {\n constructor(private contract: T) {}\n\n /**\n * Get endpoint definition by path\n */\n getEndpoint<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? T[K] : never {\n const endpoint = this.contract[path];\n if (this.isEndpointDefinition(endpoint)) {\n return endpoint as T[K] extends EndpointDefinition ? T[K] : never;\n }\n throw new Error(`Endpoint \"${String(path)}\" not found or is not a valid endpoint`);\n }\n\n /**\n * Get nested contract by path\n */\n getNested<K extends keyof T>(path: K): T[K] extends Contract ? SchemaExtractor<T[K]> : never {\n const nested = this.contract[path];\n if (this.isNestedContract(nested)) {\n return new SchemaExtractor(nested as T[K] & Contract) as T[K] extends Contract\n ? SchemaExtractor<T[K]>\n : never;\n }\n throw new Error(`Nested contract \"${String(path)}\" not found or is not a valid contract`);\n }\n\n /**\n * Get request schema for an endpoint\n */\n getRequestSchema<K extends keyof T>(\n path: K\n ): T[K] extends EndpointDefinition ? T[K]['request'] : never {\n const endpoint = this.getEndpoint(path);\n return endpoint.request as T[K] extends EndpointDefinition ? T[K]['request'] : never;\n }\n\n /**\n * Get response schema for an endpoint\n */\n getResponseSchema<K extends keyof T>(\n path: K\n ): T[K] extends EndpointDefinition ? T[K]['response'] : never {\n const endpoint = this.getEndpoint(path);\n return endpoint.response as T[K] extends EndpointDefinition ? T[K]['response'] : never;\n }\n\n /**\n * Get all schemas for an endpoint\n */\n getEndpointSchemas<K extends keyof T>(\n path: K\n ): T[K] extends EndpointDefinition\n ? { request: T[K]['request']; response: T[K]['response']; endpoint: T[K] }\n : never {\n const endpoint = this.getEndpoint(path);\n const result = {\n request: endpoint.request,\n response: endpoint.response,\n endpoint: endpoint as T[K] & EndpointDefinition,\n };\n return result as T[K] extends EndpointDefinition\n ? { request: T[K]['request']; response: T[K]['response']; endpoint: T[K] }\n : never;\n }\n\n /**\n * Get all endpoint paths in the contract\n */\n getEndpointPaths(): Array<keyof T> {\n return Object.keys(this.contract).filter((key) =>\n this.isEndpointDefinition(this.contract[key])\n ) as Array<keyof T>;\n }\n\n /**\n * Get all nested contract paths\n */\n getNestedPaths(): Array<keyof T> {\n return Object.keys(this.contract).filter((key) =>\n this.isNestedContract(this.contract[key])\n ) as Array<keyof T>;\n }\n\n /**\n * Generate OpenAPI-like schema description\n */\n describeEndpoint<K extends keyof T>(\n path: K\n ): T[K] extends EndpointDefinition\n ? {\n path: string;\n method: string;\n requestSchema: z.ZodSchema | undefined;\n responseSchema: z.ZodSchema | undefined;\n requestType: string;\n responseType: string;\n }\n : never {\n const endpoint = this.getEndpoint(path);\n\n const result = {\n path: endpoint.path,\n method: endpoint.method,\n requestSchema: endpoint.request,\n responseSchema: endpoint.response,\n requestType: endpoint.request ? this.getSchemaDescription(endpoint.request) : 'void',\n responseType: endpoint.response ? this.getSchemaDescription(endpoint.response) : 'unknown',\n };\n\n return result as T[K] extends EndpointDefinition\n ? {\n path: string;\n method: string;\n requestSchema: z.ZodSchema;\n responseSchema: z.ZodSchema;\n requestType: string;\n responseType: string;\n }\n : never;\n }\n\n /**\n * Generate schema description for documentation\n */\n private getSchemaDescription(schema: z.ZodSchema | undefined): string {\n if (!schema) {\n return 'undefined';\n }\n \n try {\n // Try to get a basic description of the schema\n if (schema instanceof z.ZodObject) {\n const shape = schema.shape;\n const fields = Object.keys(shape).map((key) => {\n const field = shape[key] as z.ZodSchema;\n return `${key}: ${this.getZodTypeDescription(field)}`;\n });\n return `{ ${fields.join(', ')} }`;\n }\n return this.getZodTypeDescription(schema);\n } catch {\n return 'unknown';\n }\n }\n\n /**\n * Get basic Zod type description\n */\n private getZodTypeDescription(schema: z.ZodSchema): string {\n // Use the _def property to determine the type, which is more reliable\n const def = (schema as z.ZodSchema & { _def?: { typeName?: string; type?: z.ZodSchema; innerType?: z.ZodSchema; value?: unknown } })._def;\n if (def?.typeName) {\n switch (def.typeName) {\n case 'ZodString': return 'string';\n case 'ZodNumber': return 'number';\n case 'ZodBoolean': return 'boolean';\n case 'ZodArray': \n return def.type ? `${this.getZodTypeDescription(def.type)}[]` : 'array';\n case 'ZodOptional': \n return def.innerType ? `${this.getZodTypeDescription(def.innerType)}?` : 'optional';\n case 'ZodNullable': \n return def.innerType ? `${this.getZodTypeDescription(def.innerType)} | null` : 'nullable';\n case 'ZodObject': return 'object';\n case 'ZodUnion': return 'union';\n case 'ZodLiteral': return `literal(${JSON.stringify(def.value)})`;\n case 'ZodEnum': return 'enum';\n default: return def.typeName.replace('Zod', '').toLowerCase();\n }\n }\n \n // Fallback to instanceof checks for older versions\n try {\n if (schema instanceof z.ZodString) return 'string';\n if (schema instanceof z.ZodNumber) return 'number';\n if (schema instanceof z.ZodBoolean) return 'boolean';\n if (schema instanceof z.ZodArray) {\n // Safe access to element property\n const element = (schema as z.ZodArray<z.ZodSchema>).element;\n return element ? `${this.getZodTypeDescription(element)}[]` : 'array';\n }\n if (schema instanceof z.ZodOptional) {\n // Safe access to unwrap method\n const inner = (schema as z.ZodOptional<z.ZodSchema>).unwrap();\n return inner ? `${this.getZodTypeDescription(inner)}?` : 'optional';\n }\n if (schema instanceof z.ZodNullable) {\n // Safe access to unwrap method\n const inner = (schema as z.ZodNullable<z.ZodSchema>).unwrap();\n return inner ? `${this.getZodTypeDescription(inner)} | null` : 'nullable';\n }\n if (schema instanceof z.ZodObject) return 'object';\n } catch {\n // Ignore errors and return unknown\n }\n return 'unknown';\n }\n\n /**\n * Check if a value is an endpoint definition\n */\n private isEndpointDefinition(value: unknown): value is EndpointDefinition {\n return (\n Boolean(value) &&\n typeof value === 'object' &&\n value !== null &&\n 'path' in value &&\n 'method' in value\n );\n }\n\n /**\n * Check if a value is a nested contract\n */\n private isNestedContract(value: unknown): value is Contract {\n return (\n Boolean(value) &&\n typeof value === 'object' &&\n value !== null &&\n !this.isEndpointDefinition(value)\n );\n }\n}\n\n/**\n * Create a schema extractor for a contract\n */\nexport function createSchemaExtractor<T extends Contract>(contract: T): SchemaExtractor<T> {\n return new SchemaExtractor(contract);\n}\n\n/**\n * Utility type to infer endpoint method signature\n */\nexport type InferEndpointMethod<T extends EndpointDefinition> = (\n ...args: T['request'] extends z.ZodSchema \n ? [data: InferRequestType<T>] \n : []\n) => Promise<InferResponseType<T>>;\n\n/**\n * Utility to extract type information at runtime\n */\nexport function extractTypeInfo<T extends EndpointDefinition>(endpoint: T) {\n return {\n requestSchema: endpoint.request,\n responseSchema: endpoint.response,\n method: endpoint.method,\n path: endpoint.path,\n hasRequestSchema: Boolean(endpoint.request),\n hasResponseSchema: Boolean(endpoint.response),\n };\n}\n","import {\n Contract,\n EndpointDefinition,\n ClientConfig,\n InternalClientConfig,\n RequestContext,\n ResponseContext,\n ApiClient,\n EndpointMethodWithSchema,\n InferRequestType,\n InferResponseType,\n} from './types';\nimport { validateRequest, validateResponse } from './validation';\nimport { separateParams, buildUrl, replacePath, shouldHaveBody } from './utils/path';\nimport { createMiddlewareExecutor, MiddlewareExecutor } from './middleware';\nimport { createAdapter, HttpAdapter } from './adapters';\nimport { SchemaExtractor, createSchemaExtractor } from './schema';\n\n/**\n * Zodsei client core implementation\n */\nexport class ZodseiClient<T extends Contract> {\n private readonly contract: T;\n private readonly config: InternalClientConfig;\n private readonly middlewareExecutor: MiddlewareExecutor;\n private adapter: HttpAdapter | null = null;\n public readonly $schema: SchemaExtractor<T>;\n\n constructor(contract: T, config: ClientConfig) {\n this.contract = contract;\n this.config = this.normalizeConfig(config);\n this.middlewareExecutor = createMiddlewareExecutor(this.config.middleware);\n this.$schema = createSchemaExtractor(contract);\n\n // Create proxy object for dynamic method calls with nested support\n return new Proxy(this, {\n get: (target, prop: string | symbol) => {\n if (typeof prop === 'string') {\n // Check if it's a direct endpoint\n if (prop in this.contract && this.isEndpointDefinition(this.contract[prop])) {\n return this.createEndpointMethod(prop);\n }\n\n // Check if it's a nested contract\n if (prop in this.contract && this.isNestedContract(this.contract[prop])) {\n return this.createNestedClient(this.contract[prop] as Contract);\n }\n }\n return (target as any)[prop];\n },\n }) as ZodseiClient<T> & ApiClient<T>;\n }\n\n /**\n * Normalize configuration\n */\n private normalizeConfig(config: ClientConfig): InternalClientConfig {\n return {\n baseUrl: config.baseUrl.replace(/\\/$/, ''),\n validateRequest: config.validateRequest ?? true,\n validateResponse: config.validateResponse ?? true,\n headers: config.headers ?? {},\n timeout: config.timeout ?? 30000,\n retries: config.retries ?? 0,\n middleware: config.middleware ?? [],\n adapter: config.adapter ?? 'fetch',\n adapterConfig: config.adapterConfig ?? {},\n };\n }\n\n /**\n * Check if a value is an endpoint definition\n */\n private isEndpointDefinition(value: any): value is EndpointDefinition {\n return (\n value &&\n typeof value === 'object' &&\n 'path' in value &&\n 'method' in value\n );\n }\n\n /**\n * Check if a value is a nested contract\n */\n private isNestedContract(value: any): value is Contract {\n return value && typeof value === 'object' && !this.isEndpointDefinition(value);\n }\n\n /**\n * Create nested client for sub-contracts\n */\n private createNestedClient(nestedContract: Contract): any {\n return new Proxy(\n {},\n {\n get: (_target, prop: string | symbol) => {\n if (typeof prop === 'string') {\n // Check if it's a direct endpoint in nested contract\n if (prop in nestedContract && this.isEndpointDefinition(nestedContract[prop])) {\n return this.createEndpointMethod(\n `${prop}`,\n nestedContract[prop] as EndpointDefinition\n );\n }\n\n // Check if it's further nested\n if (prop in nestedContract && this.isNestedContract(nestedContract[prop])) {\n return this.createNestedClient(nestedContract[prop] as Contract);\n }\n }\n return undefined;\n },\n }\n );\n }\n\n /**\n * Create endpoint method with schema access\n */\n private createEndpointMethod(endpointName: string, endpoint?: EndpointDefinition) {\n const targetEndpoint = endpoint || (this.contract[endpointName] as EndpointDefinition);\n\n const method = async (...args: any[]) => {\n // 如果有 request schema,取第一个参数;否则传 undefined\n const data = targetEndpoint.request ? args[0] : undefined;\n return this.executeEndpoint(targetEndpoint, data) as Promise<InferResponseType<typeof targetEndpoint>>;\n };\n\n // Attach schema information to the method\n (method as EndpointMethodWithSchema<typeof targetEndpoint>).schema = {\n request: targetEndpoint.request,\n response: targetEndpoint.response,\n endpoint: targetEndpoint,\n };\n\n // Attach type inference helpers (for development/debugging)\n (method as EndpointMethodWithSchema<typeof targetEndpoint>).infer = {\n request: (targetEndpoint.request ? {} : undefined) as InferRequestType<typeof targetEndpoint>,\n response: (targetEndpoint.response ? {} : {}) as InferResponseType<typeof targetEndpoint>,\n };\n\n return method as EndpointMethodWithSchema<typeof targetEndpoint>;\n }\n\n /**\n * Execute endpoint request\n */\n private async executeEndpoint(endpoint: EndpointDefinition, data: any): Promise<any> {\n // Validate request data\n const validatedData = this.config.validateRequest\n ? validateRequest(endpoint.request, data)\n : data;\n\n // Build request context\n const requestContext = this.buildRequestContext(endpoint, validatedData);\n\n // Execute middleware chain\n const response = await this.middlewareExecutor.execute(requestContext, (ctx) =>\n this.executeHttpRequest(ctx)\n );\n\n // Validate response data\n const validatedResponse = this.config.validateResponse\n ? validateResponse(endpoint.response, response.data)\n : response.data;\n\n return validatedResponse;\n }\n\n /**\n * Build request context\n */\n private buildRequestContext(endpoint: EndpointDefinition, data: any): RequestContext {\n const { path, method } = endpoint;\n\n // Separate path params and query params\n const { pathParams, queryParams } = separateParams(path, data);\n\n // Replace path parameters\n const finalPath = replacePath(path, pathParams);\n\n // Build URL\n const url =\n method.toLowerCase() === 'get'\n ? buildUrl(this.config.baseUrl, finalPath, queryParams)\n : buildUrl(this.config.baseUrl, finalPath);\n\n // Determine request body\n const body = shouldHaveBody(method)\n ? method.toLowerCase() === 'get'\n ? undefined\n : data\n : undefined;\n\n return {\n url,\n method,\n headers: { ...this.config.headers },\n body,\n params: pathParams,\n query: method.toLowerCase() === 'get' ? queryParams : undefined,\n };\n }\n\n /**\n * Get adapter\n */\n private async getAdapter(): Promise<HttpAdapter> {\n if (!this.adapter) {\n const adapterConfig = {\n timeout: this.config.timeout,\n ...this.config.adapterConfig,\n };\n\n if (typeof this.config.adapter === 'string') {\n this.adapter = await createAdapter(this.config.adapter, adapterConfig);\n } else {\n // Default to fetch adapter\n this.adapter = await createAdapter('fetch', adapterConfig);\n }\n }\n return this.adapter;\n }\n\n /**\n * Execute HTTP request\n */\n private async executeHttpRequest(context: RequestContext): Promise<ResponseContext> {\n const adapter = await this.getAdapter();\n return adapter.request(context);\n }\n\n /**\n * Get configuration\n */\n public getConfig(): Readonly<InternalClientConfig> {\n return { ...this.config };\n }\n\n /**\n * Get contract\n */\n public getContract(): Readonly<T> {\n return { ...this.contract };\n }\n\n /**\n * Add middleware\n */\n public use(middleware: any): void {\n this.middlewareExecutor.use(middleware);\n }\n}\n\n/**\n * Create client with enhanced schema support\n */\nexport function createClient<T extends Contract>(\n contract: T,\n config: ClientConfig\n): ZodseiClient<T> & ApiClient<T> {\n return new ZodseiClient(contract, config) as ZodseiClient<T> & ApiClient<T>;\n}\n","import { z } from 'zod';\nimport type { AdapterType } from './adapters';\nimport type { FetchAdapterConfig } from './adapters/fetch';\nimport type { AxiosAdapterConfig } from './adapters/axios';\nimport type { KyAdapterConfig } from './adapters/ky';\nimport type { SchemaExtractor } from './schema';\n\n// HTTP method types\nexport type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options';\n\n// Endpoint definition interface\nexport interface EndpointDefinition {\n path: string;\n method: HttpMethod;\n request?: z.ZodSchema;\n response?: z.ZodSchema;\n}\n\n// Contract type\n/**\n * Contract definition - can be nested\n */\nexport interface Contract {\n [key: string]: EndpointDefinition | Contract;\n}\n\n/**\n * Helper function to define a contract with proper type inference\n * Preserves literal types while ensuring type safety\n * Supports nested contracts\n */\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n\n/**\n * Create client type from contract - supports nested access with schema support\n */\nexport type ApiClient<T extends Contract> = {\n [K in keyof T]: T[K] extends EndpointDefinition\n ? EndpointMethodWithSchema<T[K]>\n : T[K] extends Contract\n ? ApiClient<T[K]>\n : never;\n} & {\n $schema: SchemaExtractor<T>;\n};\n\n// Base client configuration\ninterface BaseClientConfig {\n baseUrl: string;\n validateRequest?: boolean;\n validateResponse?: boolean;\n headers?: Record<string, string>;\n timeout?: number;\n retries?: number;\n middleware?: Middleware[];\n}\n\n// Type-safe client configuration with conditional adapterConfig\nexport type ClientConfig =\n | (BaseClientConfig & {\n adapter?: 'fetch';\n adapterConfig?: FetchAdapterConfig;\n })\n | (BaseClientConfig & {\n adapter: 'axios';\n adapterConfig?: AxiosAdapterConfig;\n })\n | (BaseClientConfig & {\n adapter: 'ky';\n adapterConfig?: KyAdapterConfig;\n })\n | (BaseClientConfig & {\n adapter?: undefined;\n adapterConfig?: FetchAdapterConfig; // Default to fetch\n });\n\n// Internal configuration type for client implementation\nexport interface InternalClientConfig {\n baseUrl: string;\n validateRequest: boolean;\n validateResponse: boolean;\n headers: Record<string, string>;\n timeout: number;\n retries: number;\n middleware: Middleware[];\n adapter: AdapterType | undefined;\n adapterConfig: Record<string, any>;\n}\n\n// Middleware types\nexport type Middleware = (\n request: RequestContext,\n next: (request: RequestContext) => Promise<ResponseContext>\n) => Promise<ResponseContext>;\n\n// Request context\nexport interface RequestContext {\n url: string;\n method: HttpMethod;\n headers: Record<string, string>;\n body?: any;\n params?: Record<string, string>;\n query?: Record<string, any>;\n}\n\n// Response context\nexport interface ResponseContext {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n data: any;\n}\n\n// Path parameter extraction type\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param]: string } & ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : object;\n\n// Request data separation type\nexport type SeparateRequestData<T> =\n T extends Record<string, any>\n ? {\n pathParams: ExtractPathParams<string>;\n queryParams: Omit<T, keyof ExtractPathParams<string>>;\n body: T;\n }\n : {\n pathParams: object;\n queryParams: object;\n body: T;\n };\n\n// Schema inference types\nexport type InferRequestType<T extends EndpointDefinition> = \n T['request'] extends z.ZodSchema ? z.infer<T['request']> : void;\n\nexport type InferResponseType<T extends EndpointDefinition> = \n T['response'] extends z.ZodSchema ? z.infer<T['response']> : unknown;\n\n// Enhanced endpoint method with schema access\nexport interface EndpointMethodWithSchema<T extends EndpointDefinition> {\n (\n ...args: T['request'] extends z.ZodSchema \n ? [data: InferRequestType<T>] \n : []\n ): Promise<InferResponseType<T>>;\n schema: {\n request: T['request'];\n response: T['response'];\n endpoint: T;\n };\n infer: {\n request: InferRequestType<T>;\n response: InferResponseType<T>;\n };\n}\n\n// Legacy type alias for backward compatibility\nexport type EnhancedApiClient<T extends Contract> = ApiClient<T>;\n","/**\n * Zodsei - Contract-first type-safe HTTP client with Zod validation\n */\n\n// Core exports\nexport { createClient, ZodseiClient } from './client';\nexport { defineContract } from './types';\n\n// Schema exports\nexport {\n SchemaExtractor,\n createSchemaExtractor,\n extractTypeInfo,\n type InferRequestType,\n type InferResponseType,\n type InferContractTypes,\n type InferEndpointMethod,\n} from './schema';\n\n// Type exports\nexport type {\n Contract,\n EndpointDefinition,\n ClientConfig,\n ApiClient,\n EnhancedApiClient,\n EndpointMethodWithSchema,\n HttpMethod,\n RequestContext,\n ResponseContext,\n Middleware,\n ExtractPathParams,\n SeparateRequestData,\n} from './types';\n\n// Error class exports\nexport {\n ZodseiError,\n ValidationError,\n HttpError,\n NetworkError,\n ConfigError,\n TimeoutError,\n} from './errors';\n\n// Validation utility exports\nexport {\n validateRequest,\n validateResponse,\n safeParseRequest,\n safeParseResponse,\n createValidator,\n} from './validation';\n\n// Middleware exports\nexport { createMiddlewareExecutor, composeMiddleware } from './middleware';\nexport { retryMiddleware, simpleRetry } from './middleware/retry';\nexport {\n cacheMiddleware,\n simpleCache,\n MemoryCacheStorage,\n type CacheConfig,\n type CacheStorage,\n type CacheEntry,\n} from './middleware/cache';\n\n// Utility function exports\nexport {\n extractPathParamNames,\n replacePath,\n buildQueryString,\n buildUrl,\n separateParams,\n shouldHaveBody,\n} from './utils/path';\n\nexport { mergeHeaders } from './utils/request';\n\n// Adapter exports\nexport {\n createAdapter,\n getDefaultAdapter,\n isAdapterAvailable,\n type HttpAdapter,\n type AdapterType,\n} from './adapters';\n\nexport { FetchAdapter, type FetchAdapterConfig } from './adapters/fetch';\nexport { AxiosAdapter, type AxiosAdapterConfig } from './adapters/axios';\nexport { KyAdapter, type KyAdapterConfig } from './adapters/ky';\n\n// Re-export zod for user convenience\nexport { z } from 'zod';\n","import type { Middleware } from '../types';\nimport { HttpError } from '../errors';\n\n/**\n * Retry middleware configuration\n */\nexport interface RetryConfig {\n retries: number;\n delay: number;\n backoff?: 'linear' | 'exponential';\n retryCondition?: (error: Error) => boolean;\n onRetry?: (attempt: number, error: Error) => void;\n}\n\n// Default retry condition\nfunction defaultRetryCondition(error: Error): boolean {\n if (error instanceof HttpError) {\n // Retry server errors and some client errors\n return error.status >= 500 || error.status === 408 || error.status === 429;\n }\n // Retry network errors\n return true;\n}\n\n// Calculate delay time\nfunction calculateDelay(\n attempt: number,\n baseDelay: number,\n backoff: 'linear' | 'exponential'\n): number {\n switch (backoff) {\n case 'exponential':\n return baseDelay * Math.pow(2, attempt);\n case 'linear':\n default:\n return baseDelay * (attempt + 1);\n }\n}\n\n// Delay function\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Create retry middleware\n */\nexport function retryMiddleware(config: RetryConfig): Middleware {\n const {\n retries,\n delay: baseDelay,\n backoff = 'exponential',\n retryCondition = defaultRetryCondition,\n onRetry,\n } = config;\n\n return async (request, next) => {\n let lastError: Error;\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n return await next(request);\n } catch (error) {\n lastError = error as Error;\n\n // If it's the last attempt, throw error directly\n if (attempt === retries) {\n throw lastError;\n }\n\n // Check if should retry\n if (!retryCondition(lastError)) {\n throw lastError;\n }\n\n // Call retry callback\n if (onRetry) {\n onRetry(attempt + 1, lastError);\n }\n\n // Calculate delay and wait\n const delayTime = calculateDelay(attempt, baseDelay, backoff);\n await delay(delayTime);\n }\n }\n\n throw lastError!;\n };\n}\n\n/**\n * Create simple retry middleware\n */\nexport function simpleRetry(retries: number, delay: number = 1000): Middleware {\n return retryMiddleware({\n retries,\n delay,\n backoff: 'exponential',\n });\n}\n","import type { Middleware, RequestContext, ResponseContext } from '../types';\n\n/**\n * Cache middleware configuration\n */\nexport interface CacheConfig {\n ttl: number; // Cache time (milliseconds)\n keyGenerator?: (request: RequestContext) => string;\n shouldCache?: (request: RequestContext, response: ResponseContext) => boolean;\n storage?: CacheStorage;\n}\n\n/**\n * Cache storage interface\n */\nexport interface CacheStorage {\n get(key: string): Promise<CacheEntry | null>;\n set(key: string, entry: CacheEntry): Promise<void>;\n delete(key: string): Promise<void>;\n clear(): Promise<void>;\n}\n\n/**\n * Cache entry\n */\nexport interfa