zodsei
Version:
Contract-first type-safe HTTP client with Zod validation
441 lines (428 loc) • 14.6 kB
text/typescript
import { z } from 'zod';
export { z } from 'zod';
import { AxiosInstance } from 'axios';
/**
* Schema inference and extraction utilities
*/
/**
* Extract request type from endpoint definition
*/
type InferRequestType$1<T extends EndpointDefinition> = T['request'] extends z.ZodType ? z.infer<T['request']> : void;
/**
* Extract response type from endpoint definition
*/
type InferResponseType$1<T extends EndpointDefinition> = T['response'] extends z.ZodType ? z.infer<T['response']> : unknown;
/**
* Extract all endpoint types from a contract
*/
type InferContractTypes<T extends Contract> = {
[K in keyof T]: T[K] extends EndpointDefinition ? {
request: InferRequestType$1<T[K]>;
response: InferResponseType$1<T[K]>;
endpoint: T[K];
} : T[K] extends Contract ? InferContractTypes<T[K]> : never;
};
/**
* Schema extraction utilities
*/
declare class SchemaExtractor<T extends Contract> {
private contract;
constructor(contract: T);
/**
* Get endpoint definition by path
*/
getEndpoint<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? T[K] : never;
/**
* Get nested contract by path
*/
getNested<K extends keyof T>(path: K): T[K] extends Contract ? SchemaExtractor<T[K]> : never;
/**
* Get request schema for an endpoint
*/
getRequestSchema<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? T[K]['request'] : never;
/**
* Get response schema for an endpoint
*/
getResponseSchema<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? T[K]['response'] : never;
/**
* Get all schemas for an endpoint
*/
getEndpointSchemas<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? {
request: T[K]['request'];
response: T[K]['response'];
endpoint: T[K];
} : never;
/**
* Get all endpoint paths in the contract
*/
getEndpointPaths(): Array<keyof T>;
/**
* Get all nested contract paths
*/
getNestedPaths(): Array<keyof T>;
/**
* Generate OpenAPI-like schema description
*/
describeEndpoint<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? {
path: string;
method: string;
requestSchema: z.ZodType | undefined;
responseSchema: z.ZodType | undefined;
requestType: string;
responseType: string;
} : never;
/**
* Generate schema description for documentation
*/
private getSchemaDescription;
/**
* Get basic Zod type description
*/
private getZodTypeDescription;
/**
* Check if a value is an endpoint definition
*/
private isEndpointDefinition;
/**
* Check if a value is a nested contract
*/
private isNestedContract;
}
/**
* Create a schema extractor for a contract
*/
declare function createSchemaExtractor<T extends Contract>(contract: T): SchemaExtractor<T>;
/**
* Utility type to infer endpoint method signature
*/
type InferEndpointMethod<T extends EndpointDefinition> = (...args: T['request'] extends z.ZodSchema ? [data: InferRequestType$1<T>] : []) => Promise<InferResponseType$1<T>>;
/**
* Utility to extract type information at runtime
*/
declare function extractTypeInfo<T extends EndpointDefinition>(endpoint: T): {
requestSchema: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> | undefined;
responseSchema: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> | undefined;
method: HttpMethod;
path: string;
hasRequestSchema: boolean;
hasResponseSchema: boolean;
};
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options';
interface EndpointDefinition {
path: string;
method: HttpMethod;
request?: z.ZodType;
response?: z.ZodType;
}
/**
* Contract definition - can be nested
*/
interface Contract {
[key: string]: EndpointDefinition | Contract;
}
/**
* Helper function to define a contract with proper type inference
* Preserves literal types while ensuring type safety
* Supports nested contracts
*/
declare function defineContract<T extends Contract>(contract: T): T;
/**
* Create client type from contract - supports nested access with schema support
*/
type ApiClient<T extends Contract> = {
[K in keyof T]: T[K] extends EndpointDefinition ? EndpointMethodWithSchema<T[K]> : T[K] extends Contract ? ApiClient<T[K]> : never;
} & {
$schema: SchemaExtractor<T>;
};
interface BaseClientConfig {
validateRequest?: boolean;
validateResponse?: boolean;
middleware?: Middleware[];
}
type ClientConfig = BaseClientConfig & {
axios: AxiosInstance;
};
interface InternalClientConfig {
validateRequest: boolean;
validateResponse: boolean;
middleware: Middleware[];
axios: AxiosInstance;
}
type Middleware = (request: RequestContext, next: (request: RequestContext) => Promise<ResponseContext>) => Promise<ResponseContext>;
interface RequestContext {
url: string;
method: HttpMethod;
headers: Record<string, string>;
body?: unknown;
params?: Record<string, string>;
query?: Record<string, unknown>;
}
interface ResponseContext {
status: number;
statusText: string;
headers: Record<string, string>;
data: unknown;
}
type ExtractPathParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
[K in Param]: string;
} & ExtractPathParams<`/${Rest}`> : T extends `${infer _Start}:${infer Param}` ? {
[K in Param]: string;
} : object;
type SeparateRequestData<T> = T extends Record<string, unknown> ? {
pathParams: ExtractPathParams<string>;
queryParams: Omit<T, keyof ExtractPathParams<string>>;
body: T;
} : {
pathParams: object;
queryParams: object;
body: T;
};
type InferRequestType<T extends EndpointDefinition> = T['request'] extends z.ZodType ? z.infer<T['request']> : void;
type InferResponseType<T extends EndpointDefinition> = T['response'] extends z.ZodType ? z.infer<T['response']> : unknown;
interface EndpointMethodWithSchema<T extends EndpointDefinition> {
(...args: T['request'] extends z.ZodType ? [data: InferRequestType<T>] : []): Promise<InferResponseType<T>>;
schema: {
request: T['request'];
response: T['response'];
endpoint: T;
};
infer: {
request: InferRequestType<T>;
response: InferResponseType<T>;
};
}
type EnhancedApiClient<T extends Contract> = ApiClient<T>;
/**
* Zodsei client core implementation
*/
declare class ZodseiClient<T extends Contract> {
private readonly contract;
private readonly config;
private readonly middlewareExecutor;
private adapter;
readonly $schema: SchemaExtractor<T>;
constructor(contract: T, config: ClientConfig);
/**
* Normalize configuration
*/
private normalizeConfig;
/**
* Check if a value is an endpoint definition
*/
private isEndpointDefinition;
/**
* Check if a value is a nested contract
*/
private isNestedContract;
/**
* Create nested client for sub-contracts
*/
private createNestedClient;
/**
* Create endpoint method with schema access
*/
private createEndpointMethod;
/**
* Execute endpoint request
*/
private executeEndpoint;
/**
* Build request context
*/
private buildRequestContext;
/**
* Get adapter
*/
private getAdapter;
/**
* Execute HTTP request
*/
private executeHttpRequest;
/**
* Get configuration
*/
getConfig(): Readonly<InternalClientConfig>;
/**
* Get contract
*/
getContract(): Readonly<T>;
/**
* Add middleware
*/
use(middleware: (request: RequestContext, next: (request: RequestContext) => Promise<ResponseContext>) => Promise<ResponseContext>): void;
}
/**
* Create client with enhanced schema support
*/
declare function createClient<T extends Contract>(contract: T, config: ClientConfig): ZodseiClient<T> & ApiClient<T>;
declare class ZodseiError extends Error {
readonly code: string;
constructor(message: string, code: string);
}
declare class ValidationError extends ZodseiError {
readonly issues: z.core.$ZodIssue[];
readonly type: 'request' | 'response';
constructor(message: string, issues: z.core.$ZodIssue[], type?: 'request' | 'response');
static fromZodError(error: z.ZodError, type?: 'request' | 'response'): ValidationError;
}
declare class HttpError extends ZodseiError {
readonly status: number;
readonly statusText: string;
readonly response?: unknown | undefined;
constructor(message: string, status: number, statusText: string, response?: unknown | undefined);
static fromResponse(response: Response, data?: unknown): HttpError;
}
declare class NetworkError extends ZodseiError {
readonly originalError: Error;
constructor(message: string, originalError: Error);
}
declare class ConfigError extends ZodseiError {
constructor(message: string);
}
declare class TimeoutError extends ZodseiError {
constructor(timeout: number);
}
/**
* Validation utility functions
*/
declare function validateRequest<T>(schema: z.ZodType<T> | undefined, data: unknown): T;
declare function validateResponse<T>(schema: z.ZodType<T> | undefined, data: unknown): T;
declare function safeParseRequest<T>(schema: z.ZodType<T> | undefined, data: unknown): {
success: true;
data: T;
} | {
success: false;
error: ValidationError;
};
declare function safeParseResponse<T>(schema: z.ZodType<T> | undefined, data: unknown): {
success: true;
data: T;
} | {
success: false;
error: ValidationError;
};
declare function createValidator<T>(schema: z.ZodType<T> | undefined, enabled: boolean): {
validateRequest: (data: unknown) => T;
validateResponse: (data: unknown) => T;
safeParseRequest: (data: unknown) => {
success: false;
error: ValidationError;
} | {
success: true;
data: T;
};
safeParseResponse: (data: unknown) => {
success: false;
error: ValidationError;
} | {
success: true;
data: T;
};
};
/**
* Middleware system
*/
declare class MiddlewareExecutor {
private middleware;
constructor(middleware?: Middleware[]);
execute(request: RequestContext, finalHandler: (request: RequestContext) => Promise<ResponseContext>): Promise<ResponseContext>;
use(middleware: Middleware): void;
getMiddleware(): Middleware[];
}
declare function createMiddlewareExecutor(middleware?: Middleware[]): MiddlewareExecutor;
declare function composeMiddleware(...middleware: Middleware[]): Middleware;
/**
* Retry middleware configuration
*/
interface RetryConfig {
retries: number;
delay: number;
backoff?: 'linear' | 'exponential';
retryCondition?: (error: Error) => boolean;
onRetry?: (attempt: number, error: Error) => void;
}
/**
* Create retry middleware
*/
declare function retryMiddleware(config: RetryConfig): Middleware;
/**
* Create simple retry middleware
*/
declare function simpleRetry(retries: number, delay?: number): Middleware;
/**
* Cache middleware configuration
*/
interface CacheConfig {
ttl: number;
keyGenerator?: (request: RequestContext) => string;
shouldCache?: (request: RequestContext, response: ResponseContext) => boolean;
storage?: CacheStorage;
}
/**
* Cache storage interface
*/
interface CacheStorage {
get(key: string): Promise<CacheEntry | null>;
set(key: string, entry: CacheEntry): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}
/**
* Cache entry
*/
interface CacheEntry {
data: ResponseContext;
timestamp: number;
ttl: number;
}
/**
* Memory cache storage implementation
*/
declare class MemoryCacheStorage implements CacheStorage {
private cache;
get(key: string): Promise<CacheEntry | null>;
set(key: string, entry: CacheEntry): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
size(): number;
cleanup(): void;
}
/**
* Create cache middleware
*/
declare function cacheMiddleware(config: CacheConfig): Middleware;
/**
* Create simple cache middleware
*/
declare function simpleCache(ttl: number): Middleware;
/**
* Path handling utility functions
*/
declare function extractPathParamNames(path: string): string[];
declare function replacePath(path: string, params: Record<string, string>): string;
declare function buildQueryString(params: Record<string, unknown>): string;
declare function buildUrl(path: string, query?: Record<string, unknown>): string;
declare function separateParams(path: string, data: Record<string, unknown> | null | undefined): {
pathParams: Record<string, string>;
queryParams: Record<string, unknown>;
};
declare function shouldHaveBody(method: string): boolean;
/**
* Request handling utility functions
*
* Note: This file contains utility functions that were originally designed
* for general HTTP request handling, but are now handled by individual adapters.
* These functions are kept for backward compatibility and potential future use.
*/
declare function mergeHeaders(defaultHeaders: Record<string, string>, requestHeaders?: Record<string, string>): Record<string, string>;
/**
* Axios HTTP adapter
*/
declare class AxiosAdapter {
readonly name = "axios";
private axios;
constructor(axiosInstance: AxiosInstance);
request(context: RequestContext): Promise<ResponseContext>;
private createAxiosConfig;
}
export { type ApiClient, AxiosAdapter, type CacheConfig, type CacheEntry, type CacheStorage, type ClientConfig, ConfigError, type Contract, type EndpointDefinition, type EndpointMethodWithSchema, type EnhancedApiClient, type ExtractPathParams, HttpError, type HttpMethod, type InferContractTypes, type InferEndpointMethod, type InferRequestType$1 as InferRequestType, type InferResponseType$1 as InferResponseType, MemoryCacheStorage, type Middleware, NetworkError, type RequestContext, type ResponseContext, SchemaExtractor, type SeparateRequestData, TimeoutError, ValidationError, ZodseiClient, ZodseiError, buildQueryString, buildUrl, cacheMiddleware, composeMiddleware, createClient, createMiddlewareExecutor, createSchemaExtractor, createValidator, defineContract, extractPathParamNames, extractTypeInfo, mergeHeaders, replacePath, retryMiddleware, safeParseRequest, safeParseResponse, separateParams, shouldHaveBody, simpleCache, simpleRetry, validateRequest, validateResponse };