@abapify/speci 0.4.0 → 0.4.3

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.
@@ -0,0 +1,116 @@
1
+ import { c as RestEndpointDescriptor, p as Serializable } from "./types-BcGS790c.mjs";
2
+ //#region src/rest/server/types.d.ts
3
+ /**
4
+ * HTTP server request (abstraction over node:http IncomingMessage, Web Request, etc.)
5
+ */
6
+ interface ServerRequest {
7
+ method: string;
8
+ url: string;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ body?: string;
11
+ query: Record<string, string>;
12
+ }
13
+ /**
14
+ * HTTP server response (abstraction over node:http ServerResponse, Web Response, etc.)
15
+ */
16
+ interface ServerResponse {
17
+ status: number;
18
+ headers: Record<string, string>;
19
+ body?: string;
20
+ }
21
+ /**
22
+ * Extracted route definition from a contract endpoint.
23
+ *
24
+ * Contains all the information needed to register an HTTP route
25
+ * and perform content negotiation (JSON↔XML conversion).
26
+ */
27
+ interface RouteDefinition {
28
+ /** HTTP method (GET, POST, PUT, DELETE, etc.) */
29
+ method: string;
30
+ /** Resolved path pattern (e.g., '/sap/bc/adt/oo/classes/myclass') */
31
+ path: string;
32
+ /** Original path template before parameter resolution (e.g., '/sap/bc/adt/oo/classes/${name}') */
33
+ pathTemplate: string;
34
+ /** Path parameter names extracted from the template */
35
+ pathParamNames: string[];
36
+ /** Request body schema (for JSON↔XML conversion of request bodies) */
37
+ bodySchema?: Serializable;
38
+ /** Response schemas keyed by HTTP status code */
39
+ responseSchemas: Record<number, Serializable>;
40
+ /** Expected request headers */
41
+ requestHeaders?: Record<string, string>;
42
+ /** Response headers to include */
43
+ responseHeaders?: Record<string, string>;
44
+ }
45
+ /**
46
+ * Server handler function for a route.
47
+ *
48
+ * Receives the parsed request and route context, returns a response.
49
+ */
50
+ type ServerHandler = (request: ServerRequest, context: RouteContext) => Promise<ServerResponse> | ServerResponse;
51
+ /**
52
+ * Context provided to route handlers
53
+ */
54
+ interface RouteContext {
55
+ /** The matched route definition */
56
+ route: RouteDefinition;
57
+ /** Extracted path parameters (e.g., { name: 'myclass' }) */
58
+ params: Record<string, string>;
59
+ /** The original contract operation function */
60
+ operation: (...args: any[]) => RestEndpointDescriptor;
61
+ }
62
+ /**
63
+ * Configuration for createServer
64
+ */
65
+ interface ServerConfig {
66
+ /**
67
+ * Base URL prefix to strip from incoming requests before matching.
68
+ * For example, if your proxy is mounted at '/api/v1', set this to '/api/v1'.
69
+ * Defaults to '' (match against full path).
70
+ */
71
+ basePath?: string;
72
+ }
73
+ /**
74
+ * Result of createServer - a list of route definitions
75
+ */
76
+ interface ServerRoutes {
77
+ /** All extracted route definitions */
78
+ routes: RouteDefinition[];
79
+ /**
80
+ * Match an incoming request to a route definition.
81
+ * Returns the matched route and extracted path parameters, or null.
82
+ */
83
+ match(method: string, url: string, basePath?: string): {
84
+ route: RouteDefinition;
85
+ params: Record<string, string>;
86
+ } | null;
87
+ }
88
+ //#endregion
89
+ //#region src/rest/server/create-server.d.ts
90
+ /**
91
+ * Create server-ready route definitions from a REST contract.
92
+ *
93
+ * Walks the contract tree, extracts endpoint definitions, and returns
94
+ * structured route definitions with:
95
+ * - HTTP method and path
96
+ * - Body schema for request conversion (JSON→XML)
97
+ * - Response schemas for response conversion (XML→JSON)
98
+ * - Path parameter extraction
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const server = createServer(adtContract);
103
+ *
104
+ * // Match an incoming request
105
+ * const match = server.match('GET', '/sap/bc/adt/cts/transportrequests/DEVK900001');
106
+ * if (match) {
107
+ * console.log(match.route.method); // 'GET'
108
+ * console.log(match.params); // { p1: 'devk900001' }
109
+ * console.log(match.route.responseSchemas[200]); // Serializable schema
110
+ * }
111
+ * ```
112
+ */
113
+ declare function createServer<T extends Record<string, any>>(contract: T): ServerRoutes;
114
+ //#endregion
115
+ export { ServerHandler as a, ServerRoutes as c, ServerConfig as i, RouteContext as n, ServerRequest as o, RouteDefinition as r, ServerResponse as s, createServer as t };
116
+ //# sourceMappingURL=index-CBspBEtE.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-CBspBEtE.d.mts","names":[],"sources":["../src/rest/server/types.ts","../src/rest/server/create-server.ts"],"mappings":";;;;;UAWiB;EACf;EACA;EACA,SAAS;EACT;EACA,OAAO;;;;;UAMQ;EACf;EACA,SAAS;EACT;;;;;;;;UASe;;EAEf;;EAGA;;EAGA;;EAGA;;EAGA,aAAa;;EAGb,iBAAiB,eAAe;;EAGhC,iBAAiB;;EAGjB,kBAAkB;;;;;;;KAQR,iBACV,SAAS,eACT,SAAS,iBACN,QAAQ,kBAAkB;;;;UAKd;;EAEf,OAAO;;EAGP,QAAQ;;EAGR,eAAe,gBAAgB;;;;;UAMhB;;;;;;EAMf;;;;;UAMe;;EAEf,QAAQ;;;;;EAMR,MACE,gBACA,aACA;IACG,OAAO;IAAiB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCwDvB,aAAa,UAAU,qBACrC,UAAU,IACT"}
@@ -0,0 +1,2 @@
1
+ import { _ as ExtractDescriptor, a as Inferrable, b as OperationFunction, g as Contract, p as Serializable, v as ExtractParams, x as Schema, y as OperationDescriptor } from "./types-BcGS790c.mjs";
2
+ export { Contract, ExtractDescriptor, ExtractParams, type Inferrable, OperationDescriptor, OperationFunction, Schema, type Serializable };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,190 @@
1
+ import { _ as ExtractDescriptor, a as Inferrable, b as OperationFunction, c as RestEndpointDescriptor, d as RestOperationFunction, f as SchemaLike, h as schema, i as InferSuccessResponse, l as RestMetadata, m as createInferrable, n as InferErrorResponse, o as ResponseMap, p as Serializable, r as InferSchema, s as RestContract, t as ExtractResponse, u as RestMethod, v as ExtractParams, x as Schema } from "../types-BcGS790c.mjs";
2
+ import { a as ServerHandler, c as ServerRoutes, i as ServerConfig, n as RouteContext, o as ServerRequest, r as RouteDefinition, s as ServerResponse, t as createServer } from "../index-CBspBEtE.mjs";
3
+ //#region src/rest/helpers.d.ts
4
+ /**
5
+ * Options for REST endpoint helpers
6
+ */
7
+ interface RestEndpointOptions<TBodySchema = unknown, TResponses extends ResponseMap = ResponseMap> {
8
+ /** Request body schema - can be Inferrable schema or plain type */
9
+ body?: TBodySchema;
10
+ /** Query parameters schema */
11
+ query?: Schema;
12
+ /** Headers schema */
13
+ headers?: Schema;
14
+ /** Response schemas by status code (optional when using shortcut syntax) */
15
+ responses?: TResponses;
16
+ /** Optional REST metadata */
17
+ metadata?: RestMetadata;
18
+ }
19
+ /**
20
+ * HTTP helper object with optional global responses
21
+ */
22
+ type Http<TGlobalResponses extends ResponseMap = {}> = {
23
+ get: {
24
+ <TSuccess = unknown>(path: string): RestEndpointDescriptor<'GET', string, never, {
25
+ 200: TSuccess;
26
+ } & TGlobalResponses>;
27
+ <TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>): RestEndpointDescriptor<'GET', TPath, never, TResponses & TGlobalResponses>;
28
+ };
29
+ post: <TPath extends string, TBody = unknown, TResponses extends ResponseMap = ResponseMap>(path: TPath, options: RestEndpointOptions<TBody, TResponses>) => RestEndpointDescriptor<'POST', TPath, TBody, TResponses & TGlobalResponses>;
30
+ put: <TPath extends string, TBody = unknown, TResponses extends ResponseMap = ResponseMap>(path: TPath, options: RestEndpointOptions<TBody, TResponses>) => RestEndpointDescriptor<'PUT', TPath, TBody, TResponses & TGlobalResponses>;
31
+ patch: <TPath extends string, TBody = unknown, TResponses extends ResponseMap = ResponseMap>(path: TPath, options: RestEndpointOptions<TBody, TResponses>) => RestEndpointDescriptor<'PATCH', TPath, TBody, TResponses & TGlobalResponses>;
32
+ delete: {
33
+ (path: string): RestEndpointDescriptor<'DELETE', string, never, {
34
+ 204: undefined;
35
+ } & TGlobalResponses>;
36
+ <TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>): RestEndpointDescriptor<'DELETE', TPath, never, TResponses & TGlobalResponses>;
37
+ };
38
+ head: <TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>) => RestEndpointDescriptor<'HEAD', TPath, never, TResponses & TGlobalResponses>;
39
+ options: <TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>) => RestEndpointDescriptor<'OPTIONS', TPath, never, TResponses & TGlobalResponses>;
40
+ };
41
+ /**
42
+ * Create HTTP helpers for defining REST endpoints
43
+ *
44
+ * @param globalResponses - Optional: Global response map to merge with all endpoint responses
45
+ *
46
+ * @example
47
+ * // Without global responses
48
+ * const http = createHttp();
49
+ * http.get('/users', {
50
+ * responses: { 200: User[], 404: NotFoundError }
51
+ * })
52
+ *
53
+ * @example
54
+ * // With global error responses
55
+ * const globalErrors = {
56
+ * 400: {} as ApiError,
57
+ * 401: {} as ApiError,
58
+ * 500: {} as ApiError
59
+ * };
60
+ *
61
+ * const api = createHttp(globalErrors);
62
+ * api.get('/users', {
63
+ * responses: { 200: [] as User[] }
64
+ * // 400, 401, 500 are automatically added
65
+ * })
66
+ */
67
+ declare function createHttp<TGlobalResponses extends ResponseMap = {}>(globalResponses?: TGlobalResponses): Http<TGlobalResponses>;
68
+ /**
69
+ * Default HTTP helper object
70
+ *
71
+ * @example
72
+ * import { http } from '@abapify/speci/rest'
73
+ *
74
+ * http.get('/users', {
75
+ * responses: { 200: User[], 404: NotFoundError }
76
+ * })
77
+ *
78
+ * http.delete('/users/123', {
79
+ * responses: { 204: undefined, 404: NotFoundError }
80
+ * })
81
+ */
82
+ declare const http: Http<{}>;
83
+ //#endregion
84
+ //#region src/rest/client/types.d.ts
85
+ /**
86
+ * HTTP error with typed payload
87
+ */
88
+ declare class HttpError<TPayload = unknown> extends Error {
89
+ readonly status: number;
90
+ readonly payload: TPayload;
91
+ constructor(status: number, payload: TPayload, message?: string);
92
+ }
93
+ /**
94
+ * HTTP request options
95
+ */
96
+ interface HttpRequestOptions {
97
+ method: string;
98
+ url: string;
99
+ body?: unknown;
100
+ query?: Record<string, any>;
101
+ headers?: Record<string, string>;
102
+ bodySchema?: unknown;
103
+ responses?: Record<number, unknown>;
104
+ }
105
+ /**
106
+ * HTTP adapter interface - consumer provides their own implementation
107
+ *
108
+ * @template TDefaultResponse - Default response type for all requests (useful for testing)
109
+ */
110
+ interface HttpAdapter<TDefaultResponse = unknown> {
111
+ /**
112
+ * Execute an HTTP request
113
+ */
114
+ request<TResponse = TDefaultResponse>(options?: HttpRequestOptions): Promise<TResponse>;
115
+ }
116
+ /**
117
+ * Client configuration
118
+ */
119
+ interface ClientConfig {
120
+ /** Base URL for all requests */
121
+ baseUrl: string;
122
+ /** HTTP adapter implementation */
123
+ adapter: HttpAdapter;
124
+ /** Default headers for all requests */
125
+ headers?: Record<string, string>;
126
+ /** Request interceptor */
127
+ onRequest?: (options: any) => any | Promise<any>;
128
+ /** Response interceptor */
129
+ onResponse?: (response: any) => any | Promise<any>;
130
+ /** Error interceptor */
131
+ onError?: (error: any) => any | Promise<any>;
132
+ }
133
+ /**
134
+ * Check if body is an Inferrable schema (has _infer property)
135
+ */
136
+ type IsInferrableBody<TBody> = TBody extends {
137
+ _infer?: any;
138
+ } ? true : false;
139
+ /**
140
+ * Check if method is PUT or PATCH (update operations that typically send partial data)
141
+ */
142
+ type IsUpdateMethod<TMethod> = TMethod extends 'PUT' | 'PATCH' ? true : false;
143
+ /**
144
+ * Extract body type from a REST endpoint descriptor
145
+ * Only extracts if the body is an Inferrable schema
146
+ * For PUT/PATCH methods, returns Partial<T> since updates typically send partial data
147
+ */
148
+ type ExtractBodyType<TDescriptor> = TDescriptor extends RestEndpointDescriptor<infer TMethod, any, infer TBody, any> ? IsInferrableBody<TBody> extends true ? TBody extends {
149
+ _infer?: infer U;
150
+ } ? IsUpdateMethod<TMethod> extends true ? Partial<U> : U : never : never : never;
151
+ /**
152
+ * Build parameter list for a REST client method
153
+ * - If descriptor has a body with Inferrable schema, append the inferred type as a parameter
154
+ * - Otherwise, use the function's declared parameters
155
+ */
156
+ type BuildParams<T extends OperationFunction> = ExtractDescriptor<T> extends RestEndpointDescriptor ? ExtractBodyType<ExtractDescriptor<T>> extends never ? ExtractParams<T> : ExtractParams<T> extends [] ? [ExtractBodyType<ExtractDescriptor<T>>] : [...ExtractParams<T>, ExtractBodyType<ExtractDescriptor<T>>] : ExtractParams<T>;
157
+ /**
158
+ * Convert a REST operation function to a client method
159
+ * Only returns success response types (2xx) - errors are thrown as HttpError
160
+ * Includes typed error property for error response payloads
161
+ *
162
+ * Automatically infers parameter types from body schema if present
163
+ */
164
+ type RestClientMethod<T extends OperationFunction> = {
165
+ (...params: BuildParams<T>): Promise<ExtractDescriptor<T> extends RestEndpointDescriptor ? InferSuccessResponse<ExtractDescriptor<T>> : never>;
166
+ /** Typed error response payload - use with HttpError */
167
+ error: ExtractDescriptor<T> extends RestEndpointDescriptor ? InferErrorResponse<ExtractDescriptor<T>> : never;
168
+ /** Check if error is from this endpoint */
169
+ isError(error: unknown): error is HttpError<ExtractDescriptor<T> extends RestEndpointDescriptor ? InferErrorResponse<ExtractDescriptor<T>> : never>;
170
+ };
171
+ /**
172
+ * Convert a REST contract to a typed client.
173
+ * Uses Record<string, any> instead of Contract to allow concrete types without index signature.
174
+ */
175
+ type RestClient<T extends Record<string, any>> = { [K in keyof T]: T[K] extends OperationFunction ? RestClientMethod<T[K]> : T[K] extends Record<string, any> ? RestClient<T[K]> : never; };
176
+ //#endregion
177
+ //#region src/rest/client/create-client.d.ts
178
+ /**
179
+ * Create a typed REST client from a contract
180
+ */
181
+ declare function createClient<T extends Record<string, any>>(contract: T, config: ClientConfig): RestClient<T>;
182
+ //#endregion
183
+ //#region src/rest/client/fetch-adapter.d.ts
184
+ /**
185
+ * Create a fetch-based HTTP adapter
186
+ */
187
+ declare function createFetchAdapter(options?: RequestInit): HttpAdapter;
188
+ //#endregion
189
+ export { ClientConfig, type ExtractResponse, HttpAdapter, HttpError, HttpRequestOptions, type InferSchema, type InferSuccessResponse, type Inferrable, type ResponseMap, RestClient, RestClientMethod, type RestContract, type RestEndpointDescriptor, type RestEndpointOptions, type RestMetadata, type RestMethod, type RestOperationFunction, type RouteContext, type RouteDefinition, type SchemaLike, type Serializable, type ServerConfig, type ServerHandler, type ServerRequest, type ServerResponse, type ServerRoutes, createClient, createFetchAdapter, createHttp, createInferrable, createServer, http, schema };
190
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/rest/helpers.ts","../../src/rest/client/types.ts","../../src/rest/client/create-client.ts","../../src/rest/client/fetch-adapter.ts"],"mappings":";;;;;;UAciB,oBACf,uBACA,mBAAmB,cAAc;;EAGjC,OAAO;;EAGP,QAAQ;;EAGR,UAAU;;EAGV,YAAY;;EAGZ,WAAW;;;;;KAOR,KAAK,yBAAyB;EAEjC;KACG,oBACC,eACC;MAIC,KAAK;QAAa;KAGrB,sBAAsB,mBAAmB,aACxC,MAAM,OACN,SAAS,KAAK,2BAA2B,uBACxC,8BAED,cAEA,aAAa;;EAKjB,OACE,sBACA,iBACA,mBAAmB,cAAc,aAEjC,MAAM,OACN,SAAS,oBAAoB,OAAO,gBACjC,+BAEH,OACA,OACA,aAAa;EAIf,MACE,sBACA,iBACA,mBAAmB,cAAc,aAEjC,MAAM,OACN,SAAS,oBAAoB,OAAO,gBACjC,8BAEH,OACA,OACA,aAAa;EAIf,QACE,sBACA,iBACA,mBAAmB,cAAc,aAEjC,MAAM,OACN,SAAS,oBAAoB,OAAO,gBACjC,gCAEH,OACA,OACA,aAAa;EAIf;KAEI,eACC;MAIC;QAAmB;KAGtB,sBAAsB,mBAAmB,aACxC,MAAM,OACN,SAAS,KAAK,2BAA2B,uBACxC,iCAED,cAEA,aAAa;;EAIjB,OAAO,sBAAsB,mBAAmB,aAC9C,MAAM,OACN,SAAS,KAAK,2BAA2B,yBACtC,+BAEH,cAEA,aAAa;EAGf,UAAU,sBAAsB,mBAAmB,aACjD,MAAM,OACN,SAAS,KAAK,2BAA2B,yBACtC,kCAEH,cAEA,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+BD,WAAW,yBAAyB,kBAClD,kBAAkB,mBACjB,KAAK;;;;;;;;;;;;;;;cAsGK,MAAI;;;;;;cCpQJ,UAAU,4BAA4B;WAE/B;WACA,SAAS;EADT,YAAA,gBACA,SAAS,UACzB;;;;;UAUa;EACf;EACA;EACA;EACA,QAAQ;EACR,UAAU;EACV;EACA,YAAY;;;;;;;UAQG,YAAY;;;;EAI3B,QAAQ,YAAY,kBAClB,UAAU,qBACT,QAAQ;;;;;UAMI;;EAEf;;EAGA,SAAS;;EAGT,UAAU;;EAGV,aAAa,uBAAuB;;EAGpC,cAAc,wBAAwB;;EAGtC,WAAW,qBAAqB;;;;;KAM7B,iBAAiB,SAAS;EAAgB;;;;;KAK1C,eAAe,WAAW;;;;;;KAO1B,gBAAgB,eACnB,oBAAoB,6BACZ,oBAEA,cAGJ,iBAAiB,sBACf;EAAgB,eAAe;IAC7B,eAAe,wBACb,QAAQ,KACR;;;;;;KAUP,YAAY,UAAU,qBACzB,kBAAkB,WAAW,yBACzB,gBAAgB,kBAAkB,oBAChC,cAAc,KACd,cAAc,iBACX,gBAAgB,kBAAkB,WAC/B,cAAc,IAAI,gBAAgB,kBAAkB,OAC5D,cAAc;;;;;;;;KASR,iBAAiB,UAAU;MAEhC,QAAQ,YAAY,KACtB,QACD,kBAAkB,WAAW,yBACzB,qBAAqB,kBAAkB;;EAI7C,OAAO,kBAAkB,WAAW,yBAChC,mBAAmB,kBAAkB;;EAGzC,QACE,iBACC,SAAS,UACV,kBAAkB,WAAW,yBACzB,mBAAmB,kBAAkB;;;;;;KASjC,WAAW,UAAU,0BAC9B,WAAW,IAAI,EAAE,WAAW,oBACzB,iBAAiB,EAAE,MACnB,EAAE,WAAW,sBACX,WAAW,EAAE;;;;;;iBCfL,aAAa,UAAU,qBACrC,UAAU,GACV,QAAQ,eACP,WAAW;;;;;;iBC3IE,mBAAmB,UAAU,cAAc"}
@@ -0,0 +1,281 @@
1
+ import { t as createServer } from "../server-B7qOmujK.mjs";
2
+
3
+ //#region src/rest/types.ts
4
+ /**
5
+ * Create an Inferrable schema with automatic type inference
6
+ * Cleaner than the manual _infer pattern
7
+ */
8
+ function createInferrable() {
9
+ return { _infer: void 0 };
10
+ }
11
+ /**
12
+ * Helper to wrap a schema with type information
13
+ * Use this if your schema doesn't implement Inferrable
14
+ *
15
+ * @example
16
+ * responses: { 200: schema(UserSchema, {} as User) }
17
+ */
18
+ function schema(schemaObject, _type) {
19
+ return schemaObject;
20
+ }
21
+
22
+ //#endregion
23
+ //#region src/rest/helpers.ts
24
+ /**
25
+ * Create HTTP helpers for defining REST endpoints
26
+ *
27
+ * @param globalResponses - Optional: Global response map to merge with all endpoint responses
28
+ *
29
+ * @example
30
+ * // Without global responses
31
+ * const http = createHttp();
32
+ * http.get('/users', {
33
+ * responses: { 200: User[], 404: NotFoundError }
34
+ * })
35
+ *
36
+ * @example
37
+ * // With global error responses
38
+ * const globalErrors = {
39
+ * 400: {} as ApiError,
40
+ * 401: {} as ApiError,
41
+ * 500: {} as ApiError
42
+ * };
43
+ *
44
+ * const api = createHttp(globalErrors);
45
+ * api.get('/users', {
46
+ * responses: { 200: [] as User[] }
47
+ * // 400, 401, 500 are automatically added
48
+ * })
49
+ */
50
+ function createHttp(globalResponses) {
51
+ const mergeResponses = (responses) => {
52
+ if (!globalResponses) return responses;
53
+ return {
54
+ ...globalResponses,
55
+ ...responses
56
+ };
57
+ };
58
+ return {
59
+ get: (path, options) => {
60
+ if (!options) return {
61
+ method: "GET",
62
+ path,
63
+ responses: mergeResponses({ 200: void 0 })
64
+ };
65
+ return {
66
+ method: "GET",
67
+ path,
68
+ ...options,
69
+ responses: mergeResponses(options.responses || { 200: void 0 })
70
+ };
71
+ },
72
+ post: (path, options) => {
73
+ const { body, responses, ...rest } = options;
74
+ return {
75
+ method: "POST",
76
+ path,
77
+ body,
78
+ ...rest,
79
+ responses: mergeResponses(responses)
80
+ };
81
+ },
82
+ put: (path, options) => {
83
+ const { body, responses, ...rest } = options;
84
+ return {
85
+ method: "PUT",
86
+ path,
87
+ body,
88
+ ...rest,
89
+ responses: mergeResponses(responses)
90
+ };
91
+ },
92
+ patch: (path, options) => {
93
+ const { body, responses, ...rest } = options;
94
+ return {
95
+ method: "PATCH",
96
+ path,
97
+ body,
98
+ ...rest,
99
+ responses: mergeResponses(responses)
100
+ };
101
+ },
102
+ delete: (path, options) => {
103
+ if (!options) return {
104
+ method: "DELETE",
105
+ path,
106
+ responses: mergeResponses({ 204: void 0 })
107
+ };
108
+ return {
109
+ method: "DELETE",
110
+ path,
111
+ ...options,
112
+ responses: mergeResponses(options.responses || { 204: void 0 })
113
+ };
114
+ },
115
+ head: (path, options) => ({
116
+ method: "HEAD",
117
+ path,
118
+ ...options,
119
+ responses: mergeResponses(options.responses)
120
+ }),
121
+ options: (path, options) => ({
122
+ method: "OPTIONS",
123
+ path,
124
+ ...options,
125
+ responses: mergeResponses(options.responses)
126
+ })
127
+ };
128
+ }
129
+ /**
130
+ * Default HTTP helper object
131
+ *
132
+ * @example
133
+ * import { http } from '@abapify/speci/rest'
134
+ *
135
+ * http.get('/users', {
136
+ * responses: { 200: User[], 404: NotFoundError }
137
+ * })
138
+ *
139
+ * http.delete('/users/123', {
140
+ * responses: { 204: undefined, 404: NotFoundError }
141
+ * })
142
+ */
143
+ const http = createHttp();
144
+
145
+ //#endregion
146
+ //#region src/rest/client/types.ts
147
+ /**
148
+ * HTTP error with typed payload
149
+ */
150
+ var HttpError = class extends Error {
151
+ status;
152
+ payload;
153
+ constructor(status, payload, message) {
154
+ super(message || `HTTP ${status}`);
155
+ this.status = status;
156
+ this.payload = payload;
157
+ this.name = "HttpError";
158
+ }
159
+ };
160
+
161
+ //#endregion
162
+ //#region src/rest/client/create-client.ts
163
+ /**
164
+ * Extract path parameters from a path template
165
+ * Example: "/users/${id}/posts/${postId}" -> ["id", "postId"]
166
+ */
167
+ function extractPathParams(path) {
168
+ const matches = path.matchAll(/\$\{(\w+)\}/g);
169
+ return Array.from(matches, (m) => m[1]);
170
+ }
171
+ /**
172
+ * Replace path parameters with actual values
173
+ * Example: "/users/${id}" with {id: "123"} -> "/users/123"
174
+ */
175
+ function replacePath(path, params) {
176
+ return path.replaceAll(/\$\{(\w+)\}/g, (_, key) => {
177
+ const value = params[key];
178
+ if (value === void 0) throw new Error(`Missing path parameter: ${key}`);
179
+ return encodeURIComponent(String(value));
180
+ });
181
+ }
182
+ /**
183
+ * Check if a value is an Inferrable schema (has _infer property)
184
+ */
185
+ function isInferrableSchema(value) {
186
+ return value && typeof value === "object" && "_infer" in value;
187
+ }
188
+ /**
189
+ * Create a client method from an operation function
190
+ */
191
+ function createMethod(config, operationFn) {
192
+ const method = async (...args) => {
193
+ const descriptor = operationFn(...args);
194
+ const pathParamNames = descriptor.path ? extractPathParams(descriptor.path) : [];
195
+ const pathParams = {};
196
+ const firstArg = args[0];
197
+ if (firstArg && typeof firstArg === "object" && !Array.isArray(firstArg)) pathParamNames.forEach((name) => {
198
+ if (firstArg[name] !== void 0) pathParams[name] = firstArg[name];
199
+ });
200
+ else pathParamNames.forEach((name, index) => {
201
+ if (args[index] !== void 0) pathParams[name] = args[index];
202
+ });
203
+ let bodyData = descriptor.body;
204
+ let bodySchema = void 0;
205
+ const declaredParamCount = operationFn.length;
206
+ if (isInferrableSchema(descriptor.body)) {
207
+ bodySchema = descriptor.body;
208
+ bodyData = args[declaredParamCount];
209
+ }
210
+ const url = descriptor.path ? config.baseUrl + replacePath(descriptor.path, pathParams) : config.baseUrl;
211
+ let requestOptions = {
212
+ method: descriptor.method || "GET",
213
+ url,
214
+ body: bodyData,
215
+ bodySchema,
216
+ query: descriptor.query,
217
+ headers: {
218
+ ...config.headers,
219
+ ...descriptor.headers
220
+ },
221
+ responses: descriptor.responses
222
+ };
223
+ if (config.onRequest) requestOptions = await config.onRequest(requestOptions);
224
+ try {
225
+ let response = await config.adapter.request(requestOptions);
226
+ if (config.onResponse) response = await config.onResponse(response);
227
+ return response;
228
+ } catch (error) {
229
+ if (config.onError) return await config.onError(error);
230
+ throw error;
231
+ }
232
+ };
233
+ method.error = void 0;
234
+ method.isError = (error) => {
235
+ return error instanceof Error && error.name === "HttpError";
236
+ };
237
+ return method;
238
+ }
239
+ /**
240
+ * Create a typed REST client from a contract
241
+ */
242
+ function createClient(contract, config) {
243
+ const client = {};
244
+ for (const [key, value] of Object.entries(contract)) if (typeof value === "function") client[key] = createMethod(config, value);
245
+ else if (typeof value === "object" && value !== null) client[key] = createClient(value, config);
246
+ return client;
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/rest/client/fetch-adapter.ts
251
+ /**
252
+ * Create a fetch-based HTTP adapter
253
+ */
254
+ function createFetchAdapter(options) {
255
+ return { async request(opts) {
256
+ const { method, url, body, query, headers } = opts;
257
+ const fullUrl = url + (query ? "?" + new URLSearchParams(query).toString() : "");
258
+ const response = await fetch(fullUrl, {
259
+ ...options,
260
+ method,
261
+ headers: {
262
+ "Content-Type": "application/json",
263
+ ...options?.headers,
264
+ ...headers
265
+ },
266
+ body: body ? JSON.stringify(body) : void 0
267
+ });
268
+ if (!response.ok) {
269
+ const errorData = await response.json().catch(() => ({
270
+ error: "HTTP Error",
271
+ message: response.statusText
272
+ }));
273
+ throw new HttpError(response.status, errorData);
274
+ }
275
+ return await response.json();
276
+ } };
277
+ }
278
+
279
+ //#endregion
280
+ export { HttpError, createClient, createFetchAdapter, createHttp, createInferrable, createServer, http, schema };
281
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/rest/types.ts","../../src/rest/helpers.ts","../../src/rest/client/types.ts","../../src/rest/client/create-client.ts","../../src/rest/client/fetch-adapter.ts"],"sourcesContent":["/**\n * Speci REST - REST-specific types\n */\n\nimport type { OperationDescriptor, Schema } from '../core/types';\n\n/**\n * HTTP methods for REST APIs\n */\nexport type RestMethod =\n | 'GET'\n | 'POST'\n | 'PUT'\n | 'PATCH'\n | 'DELETE'\n | 'HEAD'\n | 'OPTIONS';\n\n/**\n * Inferrable schema interface (explicit _infer property)\n *\n * Schemas that implement this interface will have their types automatically inferred.\n *\n * @example\n * const UserSchema = {\n * ...yourSchemaDefinition,\n * _infer: undefined as unknown as User\n * } as const;\n *\n * responses: { 200: UserSchema } // Type is User\n */\nexport interface Inferrable<T = unknown> {\n /** Type inference marker - never accessed at runtime */\n _infer?: T;\n}\n\n/**\n * Serializable schema interface (parse/build methods + Inferrable)\n *\n * Extends Inferrable so schemas with parse() automatically get _infer marker.\n * This is the preferred pattern for schema libraries like ts-xsd.\n *\n * @example\n * const UserSchema = {\n * parse: (raw: string): User => JSON.parse(raw),\n * build: (data: User): string => JSON.stringify(data),\n * };\n *\n * responses: { 200: UserSchema } // Type is User (inferred via _infer from Inferrable)\n */\nexport interface Serializable<T = unknown> extends Inferrable<T> {\n /** Parse raw string to typed object */\n parse(raw: string): T;\n /** Build typed object to string */\n build?(data: T): string;\n}\n\n/**\n * Create an Inferrable schema with automatic type inference\n * Cleaner than the manual _infer pattern\n */\nexport function createInferrable<T>(): Inferrable<T> {\n return { _infer: undefined as unknown as T };\n}\n\n/**\n * Infer type from a schema\n *\n * Uses _infer property from Inferrable<T> (which Serializable extends).\n * Falls back to original type if no _infer property.\n *\n * @example\n * type A = InferSchema<{ _infer?: User }> // User\n * type B = InferSchema<string> // string (fallback)\n */\nexport type InferSchema<T> = T extends { _infer?: infer U }\n ? NonNullable<U>\n : T;\n\n/**\n * Schema-like object - can be any schema library (Zod, JSON Schema, custom, etc.)\n * Adapters interpret these based on their capabilities.\n */\nexport type SchemaLike = unknown;\n\n/**\n * Helper to wrap a schema with type information\n * Use this if your schema doesn't implement Inferrable\n *\n * @example\n * responses: { 200: schema(UserSchema, {} as User) }\n */\nexport function schema<T>(schemaObject: SchemaLike, _type: T): Inferrable<T> {\n return schemaObject as Inferrable<T>;\n}\n\n/**\n * Response map - maps status codes to response types or schemas\n *\n * Values can be:\n * - Type assertions: `undefined as unknown as MyType`\n * - Schema objects: `MyZodSchema`, `MyElementSchema`, etc.\n * - Schema with type: `MySchema as SchemaWithType<MyType>`\n * - The adapter will interpret them appropriately\n */\nexport type ResponseMap = Record<number, SchemaLike>;\n\n/**\n * REST endpoint descriptor\n * Extends the protocol-agnostic OperationDescriptor with REST-specific fields\n */\nexport interface RestEndpointDescriptor<\n TMethod extends RestMethod = RestMethod,\n TPath extends string = string,\n TBodySchema = unknown,\n TResponses extends ResponseMap = ResponseMap,\n> extends OperationDescriptor {\n /** HTTP method */\n method: TMethod;\n\n /** URL path with optional template variables */\n path: TPath;\n\n /** Request body schema - type will be inferred from Inferrable schemas */\n body?: InferSchema<TBodySchema>;\n\n /** Query parameters schema */\n query?: Schema;\n\n /** Headers schema */\n headers?: Schema;\n\n /** Response schemas by HTTP status code */\n responses: TResponses;\n\n /** REST-specific metadata */\n metadata?: RestMetadata;\n}\n\n/**\n * REST-specific metadata\n */\nexport interface RestMetadata {\n /** Endpoint description */\n description?: string;\n\n /** Tags for grouping */\n tags?: string[];\n\n /** Deprecation notice */\n deprecated?: boolean;\n\n /** Rate limit information */\n rateLimit?: {\n requests: number;\n window: string;\n };\n\n /** Cache control */\n cache?: {\n maxAge?: number;\n private?: boolean;\n };\n\n /** Any additional metadata */\n [key: string]: any;\n}\n\n/**\n * REST operation function type\n */\nexport type RestOperationFunction<\n TParams extends any[] = any[],\n TDescriptor extends RestEndpointDescriptor = RestEndpointDescriptor,\n> = (...params: TParams) => TDescriptor;\n\n/**\n * REST Contract - enforces that all properties are either:\n * - RestOperationFunction (endpoint)\n * - Nested RestContract (namespace)\n */\nexport type RestContract = {\n [key: string]: RestOperationFunction | RestContract;\n};\n\n/**\n * Extract response type for a specific HTTP status code\n * Automatically infers types from Inferrable schemas\n */\nexport type ExtractResponse<\n T extends RestEndpointDescriptor,\n Status extends keyof T['responses'],\n> = InferSchema<T['responses'][Status]>;\n\n/**\n * Infer the success response type (2xx status codes only)\n * Filters out error responses (4xx, 5xx) and only returns 2xx response types\n * Automatically infers types from Inferrable schemas\n */\nexport type InferSuccessResponse<T extends RestEndpointDescriptor> = Exclude<\n {\n [K in keyof T['responses']]: K extends\n | 200\n | 201\n | 202\n | 203\n | 204\n | 205\n | 206\n | 207\n | 208\n | 226\n ? InferSchema<T['responses'][K]>\n : never;\n }[keyof T['responses']],\n never | undefined\n>;\n\n/**\n * Infer the error response type (non-2xx status codes)\n * Captures all error responses (4xx, 5xx) as a union type\n */\nexport type InferErrorResponse<T extends RestEndpointDescriptor> = Exclude<\n {\n [K in keyof T['responses']]: K extends\n | 200\n | 201\n | 202\n | 203\n | 204\n | 205\n | 206\n | 207\n | 208\n | 226\n ? never\n : T['responses'][K];\n }[keyof T['responses']],\n never | undefined\n>;\n","/**\n * Speci REST - Helper functions for REST endpoints\n */\n\nimport type { Schema } from '../core/types';\nimport type {\n RestEndpointDescriptor,\n RestMetadata,\n ResponseMap,\n} from './types';\n\n/**\n * Options for REST endpoint helpers\n */\nexport interface RestEndpointOptions<\n TBodySchema = unknown,\n TResponses extends ResponseMap = ResponseMap,\n> {\n /** Request body schema - can be Inferrable schema or plain type */\n body?: TBodySchema;\n\n /** Query parameters schema */\n query?: Schema;\n\n /** Headers schema */\n headers?: Schema;\n\n /** Response schemas by status code (optional when using shortcut syntax) */\n responses?: TResponses;\n\n /** Optional REST metadata */\n metadata?: RestMetadata;\n}\n\n/**\n * HTTP helper object with optional global responses\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\ntype Http<TGlobalResponses extends ResponseMap = {}> = {\n // GET - with shortcut syntax\n get: {\n <TSuccess = unknown>(\n path: string,\n ): RestEndpointDescriptor<\n 'GET',\n string,\n never,\n { 200: TSuccess } & TGlobalResponses\n >;\n\n <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ): RestEndpointDescriptor<\n 'GET',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n };\n\n // POST - always use options (no shortcut)\n post: <\n TPath extends string,\n TBody = unknown,\n TResponses extends ResponseMap = ResponseMap,\n >(\n path: TPath,\n options: RestEndpointOptions<TBody, TResponses>,\n ) => RestEndpointDescriptor<\n 'POST',\n TPath,\n TBody,\n TResponses & TGlobalResponses\n >;\n\n // PUT - always use options (no shortcut)\n put: <\n TPath extends string,\n TBody = unknown,\n TResponses extends ResponseMap = ResponseMap,\n >(\n path: TPath,\n options: RestEndpointOptions<TBody, TResponses>,\n ) => RestEndpointDescriptor<\n 'PUT',\n TPath,\n TBody,\n TResponses & TGlobalResponses\n >;\n\n // PATCH - always use options (no shortcut)\n patch: <\n TPath extends string,\n TBody = unknown,\n TResponses extends ResponseMap = ResponseMap,\n >(\n path: TPath,\n options: RestEndpointOptions<TBody, TResponses>,\n ) => RestEndpointDescriptor<\n 'PATCH',\n TPath,\n TBody,\n TResponses & TGlobalResponses\n >;\n\n // DELETE - with shortcut syntax\n delete: {\n (\n path: string,\n ): RestEndpointDescriptor<\n 'DELETE',\n string,\n never,\n { 204: undefined } & TGlobalResponses\n >;\n\n <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ): RestEndpointDescriptor<\n 'DELETE',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n };\n\n head: <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ) => RestEndpointDescriptor<\n 'HEAD',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n\n options: <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ) => RestEndpointDescriptor<\n 'OPTIONS',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n};\n\n/**\n * Create HTTP helpers for defining REST endpoints\n *\n * @param globalResponses - Optional: Global response map to merge with all endpoint responses\n *\n * @example\n * // Without global responses\n * const http = createHttp();\n * http.get('/users', {\n * responses: { 200: User[], 404: NotFoundError }\n * })\n *\n * @example\n * // With global error responses\n * const globalErrors = {\n * 400: {} as ApiError,\n * 401: {} as ApiError,\n * 500: {} as ApiError\n * };\n *\n * const api = createHttp(globalErrors);\n * api.get('/users', {\n * responses: { 200: [] as User[] }\n * // 400, 401, 500 are automatically added\n * })\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport function createHttp<TGlobalResponses extends ResponseMap = {}>(\n globalResponses?: TGlobalResponses,\n): Http<TGlobalResponses> {\n const mergeResponses = (responses: any) => {\n if (!globalResponses) return responses;\n return { ...globalResponses, ...responses };\n };\n\n return {\n get: (path: string, options?: any) => {\n // Shortcut: api.get<User[]>('/path')\n if (!options) {\n return {\n method: 'GET' as const,\n path,\n responses: mergeResponses({ 200: undefined }),\n };\n }\n // Full: api.get('/path', { responses: {...} })\n return {\n method: 'GET' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses || { 200: undefined }),\n };\n },\n post: (path, options) => {\n const { body, responses, ...rest } = options;\n return {\n method: 'POST' as const,\n path,\n body,\n ...rest,\n responses: mergeResponses(responses),\n };\n },\n\n put: (path, options) => {\n const { body, responses, ...rest } = options;\n return {\n method: 'PUT' as const,\n path,\n body,\n ...rest,\n responses: mergeResponses(responses),\n };\n },\n\n patch: (path, options) => {\n const { body, responses, ...rest } = options;\n return {\n method: 'PATCH' as const,\n path,\n body,\n ...rest,\n responses: mergeResponses(responses),\n };\n },\n delete: (path: string, options?: any) => {\n // Shortcut: api.delete('/path')\n if (!options) {\n return {\n method: 'DELETE' as const,\n path,\n responses: mergeResponses({ 204: undefined }),\n };\n }\n // Full: api.delete('/path', { responses: {...} })\n return {\n method: 'DELETE' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses || { 204: undefined }),\n };\n },\n head: (path: string, options: any) => ({\n method: 'HEAD' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses),\n }),\n options: (path: string, options: any) => ({\n method: 'OPTIONS' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses),\n }),\n } as Http<TGlobalResponses>;\n}\n\n/**\n * Default HTTP helper object\n *\n * @example\n * import { http } from '@abapify/speci/rest'\n *\n * http.get('/users', {\n * responses: { 200: User[], 404: NotFoundError }\n * })\n *\n * http.delete('/users/123', {\n * responses: { 204: undefined, 404: NotFoundError }\n * })\n */\nexport const http = createHttp();\n","/**\n * Speci REST - Client Types\n *\n * REST-specific client interface types.\n */\n\nimport type {\n OperationFunction,\n ExtractParams,\n ExtractDescriptor,\n} from '../../core/types';\nimport type {\n RestEndpointDescriptor,\n InferSuccessResponse,\n InferErrorResponse,\n} from '../types';\n\n/**\n * HTTP error with typed payload\n */\nexport class HttpError<TPayload = unknown> extends Error {\n constructor(\n public readonly status: number,\n public readonly payload: TPayload,\n message?: string,\n ) {\n super(message || `HTTP ${status}`);\n this.name = 'HttpError';\n }\n}\n\n/**\n * HTTP request options\n */\nexport interface HttpRequestOptions {\n method: string;\n url: string;\n body?: unknown;\n query?: Record<string, any>;\n headers?: Record<string, string>;\n bodySchema?: unknown; // Schema for serializing body (passed when body is Inferrable)\n responses?: Record<number, unknown>; // Response schemas by status code\n}\n\n/**\n * HTTP adapter interface - consumer provides their own implementation\n *\n * @template TDefaultResponse - Default response type for all requests (useful for testing)\n */\nexport interface HttpAdapter<TDefaultResponse = unknown> {\n /**\n * Execute an HTTP request\n */\n request<TResponse = TDefaultResponse>(\n options?: HttpRequestOptions,\n ): Promise<TResponse>;\n}\n\n/**\n * Client configuration\n */\nexport interface ClientConfig {\n /** Base URL for all requests */\n baseUrl: string;\n\n /** HTTP adapter implementation */\n adapter: HttpAdapter;\n\n /** Default headers for all requests */\n headers?: Record<string, string>;\n\n /** Request interceptor */\n onRequest?: (options: any) => any | Promise<any>;\n\n /** Response interceptor */\n onResponse?: (response: any) => any | Promise<any>;\n\n /** Error interceptor */\n onError?: (error: any) => any | Promise<any>;\n}\n\n/**\n * Check if body is an Inferrable schema (has _infer property)\n */\ntype IsInferrableBody<TBody> = TBody extends { _infer?: any } ? true : false;\n\n/**\n * Check if method is PUT or PATCH (update operations that typically send partial data)\n */\ntype IsUpdateMethod<TMethod> = TMethod extends 'PUT' | 'PATCH' ? true : false;\n\n/**\n * Extract body type from a REST endpoint descriptor\n * Only extracts if the body is an Inferrable schema\n * For PUT/PATCH methods, returns Partial<T> since updates typically send partial data\n */\ntype ExtractBodyType<TDescriptor> =\n TDescriptor extends RestEndpointDescriptor<\n infer TMethod,\n any,\n infer TBody,\n any\n >\n ? IsInferrableBody<TBody> extends true\n ? TBody extends { _infer?: infer U }\n ? IsUpdateMethod<TMethod> extends true\n ? Partial<U> // PUT/PATCH: allow partial body\n : U // POST: require full body\n : never\n : never\n : never;\n\n/**\n * Build parameter list for a REST client method\n * - If descriptor has a body with Inferrable schema, append the inferred type as a parameter\n * - Otherwise, use the function's declared parameters\n */\ntype BuildParams<T extends OperationFunction> =\n ExtractDescriptor<T> extends RestEndpointDescriptor\n ? ExtractBodyType<ExtractDescriptor<T>> extends never\n ? ExtractParams<T> // No body - use declared params\n : ExtractParams<T> extends []\n ? [ExtractBodyType<ExtractDescriptor<T>>] // Empty params + body - add body param\n : [...ExtractParams<T>, ExtractBodyType<ExtractDescriptor<T>>] // Has params + body - append body\n : ExtractParams<T>;\n\n/**\n * Convert a REST operation function to a client method\n * Only returns success response types (2xx) - errors are thrown as HttpError\n * Includes typed error property for error response payloads\n *\n * Automatically infers parameter types from body schema if present\n */\nexport type RestClientMethod<T extends OperationFunction> = {\n (\n ...params: BuildParams<T>\n ): Promise<\n ExtractDescriptor<T> extends RestEndpointDescriptor\n ? InferSuccessResponse<ExtractDescriptor<T>>\n : never\n >;\n /** Typed error response payload - use with HttpError */\n error: ExtractDescriptor<T> extends RestEndpointDescriptor\n ? InferErrorResponse<ExtractDescriptor<T>>\n : never;\n /** Check if error is from this endpoint */\n isError(\n error: unknown,\n ): error is HttpError<\n ExtractDescriptor<T> extends RestEndpointDescriptor\n ? InferErrorResponse<ExtractDescriptor<T>>\n : never\n >;\n};\n\n/**\n * Convert a REST contract to a typed client.\n * Uses Record<string, any> instead of Contract to allow concrete types without index signature.\n */\nexport type RestClient<T extends Record<string, any>> = {\n [K in keyof T]: T[K] extends OperationFunction\n ? RestClientMethod<T[K]>\n : T[K] extends Record<string, any>\n ? RestClient<T[K]>\n : never;\n};\n","/**\n * Speci v0.1 - Client Generator\n *\n * Creates a typed client from a contract specification.\n */\n\nimport type { OperationFunction } from '../../core/types';\nimport type { RestClient, ClientConfig } from './types';\n\n/**\n * Extract path parameters from a path template\n * Example: \"/users/${id}/posts/${postId}\" -> [\"id\", \"postId\"]\n */\nfunction extractPathParams(path: string): string[] {\n const matches = path.matchAll(/\\$\\{(\\w+)\\}/g);\n return Array.from(matches, (m) => m[1]);\n}\n\n/**\n * Replace path parameters with actual values\n * Example: \"/users/${id}\" with {id: \"123\"} -> \"/users/123\"\n */\nfunction replacePath(path: string, params: Record<string, any>): string {\n return path.replaceAll(/\\$\\{(\\w+)\\}/g, (_, key) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: ${key}`);\n }\n return encodeURIComponent(String(value));\n });\n}\n\n/**\n * Check if a value is an Inferrable schema (has _infer property)\n */\nfunction isInferrableSchema(value: any): boolean {\n return value && typeof value === 'object' && '_infer' in value;\n}\n\n/**\n * Create a client method from an operation function\n */\nfunction createMethod(\n config: ClientConfig,\n operationFn: OperationFunction,\n): any {\n const method = async (...args: any[]) => {\n // Execute the operation function with the provided args\n // Contract functions receive params (object or individual), we pass them through\n const descriptor: any = operationFn(...args);\n\n // For REST endpoints, handle path parameters\n const pathParamNames = descriptor.path\n ? extractPathParams(descriptor.path)\n : [];\n\n // Build path params object\n // If first arg is an object with path param keys, use it; otherwise use positional args\n const pathParams: Record<string, any> = {};\n const firstArg = args[0];\n\n if (firstArg && typeof firstArg === 'object' && !Array.isArray(firstArg)) {\n // Object-based parameters - extract from first arg\n pathParamNames.forEach((name) => {\n if (firstArg[name] !== undefined) {\n pathParams[name] = firstArg[name];\n }\n });\n } else {\n // Positional parameters - use arg indices\n pathParamNames.forEach((name, index) => {\n if (args[index] !== undefined) {\n pathParams[name] = args[index];\n }\n });\n }\n\n // Handle body parameter\n // If descriptor.body is an Inferrable schema, body data comes after path params\n let bodyData = descriptor.body;\n let bodySchema = undefined;\n\n // Use operationFn.length to determine how many path params the function declared\n // This works because template strings consume the params before we see them\n const declaredParamCount = operationFn.length;\n\n if (isInferrableSchema(descriptor.body)) {\n // Body is a schema - body data comes after all declared parameters\n bodySchema = descriptor.body;\n // Body is at position: number of declared params (path params consumed by template)\n bodyData = args[declaredParamCount];\n }\n\n // Replace path parameters if path exists\n const url = descriptor.path\n ? config.baseUrl + replacePath(descriptor.path, pathParams)\n : config.baseUrl;\n\n // Prepare request options (REST-specific, but works generically)\n let requestOptions = {\n method: descriptor.method || 'GET',\n url,\n body: bodyData, // Actual data (not schema)\n bodySchema, // Schema for adapter to use\n query: descriptor.query,\n headers: {\n ...config.headers,\n ...descriptor.headers,\n },\n responses: descriptor.responses, // Pass responses for adapter\n };\n\n // Apply request interceptor\n if (config.onRequest) {\n requestOptions = await config.onRequest(requestOptions);\n }\n\n try {\n // Execute request\n let response = await config.adapter.request(requestOptions);\n\n // Apply response interceptor\n if (config.onResponse) {\n response = await config.onResponse(response);\n }\n\n return response;\n } catch (error) {\n // Apply error interceptor\n if (config.onError) {\n return await config.onError(error);\n }\n throw error;\n }\n };\n\n // Add error type property and isError method for type checking\n (method as any).error = undefined;\n (method as any).isError = (error: unknown): error is any => {\n return error instanceof Error && error.name === 'HttpError';\n };\n\n return method;\n}\n\n/**\n * Create a typed REST client from a contract\n */\nexport function createClient<T extends Record<string, any>>(\n contract: T,\n config: ClientConfig,\n): RestClient<T> {\n const client: any = {};\n\n for (const [key, value] of Object.entries(contract)) {\n if (typeof value === 'function') {\n // It's an operation function\n client[key] = createMethod(config, value as OperationFunction);\n } else if (typeof value === 'object' && value !== null) {\n // It's a nested contract\n client[key] = createClient(value as Record<string, any>, config);\n }\n }\n\n return client as RestClient<T>;\n}\n","/**\n * Speci v0.1 - Fetch Adapter\n *\n * Native fetch API adapter for Speci clients.\n */\n\nimport type { HttpAdapter } from './types';\nimport { HttpError } from './types';\n\n/**\n * Create a fetch-based HTTP adapter\n */\nexport function createFetchAdapter(options?: RequestInit): HttpAdapter {\n return {\n async request<TResponse = unknown>(opts: {\n method: string;\n url: string;\n body?: unknown;\n query?: Record<string, any>;\n headers?: Record<string, string>;\n }): Promise<TResponse> {\n const { method, url, body, query, headers } = opts;\n // Build query string\n const queryString = query\n ? '?' + new URLSearchParams(query).toString()\n : '';\n\n const fullUrl = url + queryString;\n\n // Execute fetch\n const response = await fetch(fullUrl, {\n ...options,\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...options?.headers,\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n\n // Check if response is successful (2xx)\n if (!response.ok) {\n // Parse error response\n const errorData = await response.json().catch(() => ({\n error: 'HTTP Error',\n message: response.statusText,\n }));\n throw new HttpError(response.status, errorData);\n }\n\n // Parse successful response\n const data = await response.json();\n return data as TResponse;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { a as ServerHandler, c as ServerRoutes, i as ServerConfig, n as RouteContext, o as ServerRequest, r as RouteDefinition, s as ServerResponse, t as createServer } from "../../index-CBspBEtE.mjs";
2
+ export { type RouteContext, type RouteDefinition, type ServerConfig, type ServerHandler, type ServerRequest, type ServerResponse, type ServerRoutes, createServer };
@@ -0,0 +1,3 @@
1
+ import { t as createServer } from "../../server-B7qOmujK.mjs";
2
+
3
+ export { createServer };
@@ -0,0 +1,136 @@
1
+ //#region src/rest/server/create-server.ts
2
+ /** Sentinel value used to mark parameter positions in resolved paths */
3
+ const PARAM_MARKER = "\0";
4
+ /**
5
+ * Check if a value is a RestEndpointDescriptor (has method and path)
6
+ */
7
+ function isEndpointDescriptor(value) {
8
+ return typeof value === "object" && value !== null && "method" in value && "path" in value && "responses" in value;
9
+ }
10
+ /**
11
+ * Check if a schema is Serializable (has parse method)
12
+ */
13
+ function isSerializable(schema) {
14
+ return typeof schema === "object" && schema !== null && typeof schema.parse === "function";
15
+ }
16
+ /**
17
+ * Walk a contract tree and extract all endpoint definitions.
18
+ *
19
+ * Calls each operation function with a sentinel value to get the resolved
20
+ * path, then replaces the sentinel with regex capture groups for matching.
21
+ */
22
+ function tryExtractEndpoint(obj) {
23
+ try {
24
+ const paramCount = obj.length || 1;
25
+ const descriptor = obj(...Array.from({ length: paramCount }, () => PARAM_MARKER));
26
+ if (isEndpointDescriptor(descriptor)) return {
27
+ operation: obj,
28
+ descriptor
29
+ };
30
+ } catch {}
31
+ return null;
32
+ }
33
+ function walkContract(obj, parentPath) {
34
+ if (typeof obj === "function") {
35
+ const result = tryExtractEndpoint(obj);
36
+ return result ? [result] : [];
37
+ }
38
+ if (typeof obj === "object" && obj !== null) {
39
+ const endpoints = [];
40
+ for (const [key, value] of Object.entries(obj)) endpoints.push(...walkContract(value, [...parentPath, key]));
41
+ return endpoints;
42
+ }
43
+ return [];
44
+ }
45
+ /**
46
+ * Convert a resolved path with sentinel markers into a regex pattern.
47
+ *
48
+ * Also extracts path parameter names from the original function's parameter list.
49
+ *
50
+ * @example
51
+ * // For path '/users/\x00/posts/\x00' with param names ['userId', 'postId']
52
+ * // Returns: /\/users\/([^/]+)\/posts\/([^/]+)/
53
+ */
54
+ function buildPathInfo(resolvedPath) {
55
+ const sentinelCount = resolvedPath.split(PARAM_MARKER).length - 1;
56
+ const paramNames = Array.from({ length: sentinelCount }, (_, i) => `p${i + 1}`);
57
+ const regexStr = resolvedPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").split(PARAM_MARKER).join("([^/]+)");
58
+ let template = resolvedPath;
59
+ for (const name of paramNames) template = template.replace(PARAM_MARKER, `\${${name}}`);
60
+ return {
61
+ regex: new RegExp(`^${regexStr}$`),
62
+ template,
63
+ paramNames
64
+ };
65
+ }
66
+ /**
67
+ * Create server-ready route definitions from a REST contract.
68
+ *
69
+ * Walks the contract tree, extracts endpoint definitions, and returns
70
+ * structured route definitions with:
71
+ * - HTTP method and path
72
+ * - Body schema for request conversion (JSON→XML)
73
+ * - Response schemas for response conversion (XML→JSON)
74
+ * - Path parameter extraction
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const server = createServer(adtContract);
79
+ *
80
+ * // Match an incoming request
81
+ * const match = server.match('GET', '/sap/bc/adt/cts/transportrequests/DEVK900001');
82
+ * if (match) {
83
+ * console.log(match.route.method); // 'GET'
84
+ * console.log(match.params); // { p1: 'devk900001' }
85
+ * console.log(match.route.responseSchemas[200]); // Serializable schema
86
+ * }
87
+ * ```
88
+ */
89
+ function createServer(contract) {
90
+ const routes = walkContract(contract, []).map(({ operation, descriptor }) => {
91
+ const resolvedPath = descriptor.path || "";
92
+ const { regex, template, paramNames } = buildPathInfo(resolvedPath);
93
+ const bodySchema = descriptor.body && isSerializable(descriptor.body) ? descriptor.body : void 0;
94
+ const responseSchemas = {};
95
+ if (descriptor.responses) {
96
+ for (const [status, schema] of Object.entries(descriptor.responses)) if (isSerializable(schema)) responseSchemas[Number(status)] = schema;
97
+ }
98
+ return {
99
+ method: descriptor.method,
100
+ path: resolvedPath,
101
+ pathTemplate: template,
102
+ pathParamNames: paramNames,
103
+ bodySchema,
104
+ responseSchemas,
105
+ requestHeaders: descriptor.headers,
106
+ _regex: regex
107
+ };
108
+ });
109
+ return {
110
+ routes,
111
+ match(method, url, basePath = "") {
112
+ const pathname = url.split("?")[0];
113
+ let relativePath = pathname;
114
+ if (basePath && pathname.startsWith(basePath)) relativePath = pathname.slice(basePath.length) || "/";
115
+ const targetMethod = method.toUpperCase();
116
+ for (const route of routes) {
117
+ if (route.method !== targetMethod) continue;
118
+ const routeWithRegex = route;
119
+ const match = relativePath.match(routeWithRegex._regex);
120
+ if (match) {
121
+ const params = {};
122
+ for (let i = 0; i < route.pathParamNames.length; i++) params[route.pathParamNames[i]] = match[i + 1] || "";
123
+ return {
124
+ route,
125
+ params
126
+ };
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ };
132
+ }
133
+
134
+ //#endregion
135
+ export { createServer as t };
136
+ //# sourceMappingURL=server-B7qOmujK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-B7qOmujK.mjs","names":[],"sources":["../src/rest/server/create-server.ts"],"sourcesContent":["/**\n * Speci REST - Server Generator\n *\n * Creates server-ready route definitions from REST contracts.\n *\n * This is the server-side counterpart of createClient. It walks a contract\n * tree, extracts endpoint definitions (method, path, body schema, response\n * schemas), and returns structured route definitions that can be used to\n * build HTTP servers with any framework (node:http, Express, Hono, etc.).\n *\n * @example\n * ```typescript\n * import { createServer } from '@abapify/speci/rest';\n * import { adtContract } from '@abapify/adt-contracts';\n *\n * const server = createServer(adtContract);\n *\n * // Use with node:http\n * import { createServer as httpCreate } from 'node:http';\n *\n * httpCreate((req, res) => {\n * const match = server.match(req.method!, req.url!);\n * if (match) {\n * // Forward to downstream SAP, handle JSON↔XML conversion, etc.\n * }\n * });\n * ```\n */\n\nimport type { OperationFunction } from '../../core/types';\nimport type { RestEndpointDescriptor, Serializable } from '../types';\nimport type { RouteDefinition, ServerRoutes } from './types';\n\n/** Sentinel value used to mark parameter positions in resolved paths */\nconst PARAM_MARKER = '\\x00';\n\n/**\n * Check if a value is a RestEndpointDescriptor (has method and path)\n */\nfunction isEndpointDescriptor(value: unknown): value is RestEndpointDescriptor {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'method' in value &&\n 'path' in value &&\n 'responses' in value\n );\n}\n\n/**\n * Check if a schema is Serializable (has parse method)\n */\nfunction isSerializable(schema: unknown): schema is Serializable {\n return (\n typeof schema === 'object' &&\n schema !== null &&\n typeof (schema as any).parse === 'function'\n );\n}\n\n/**\n * Walk a contract tree and extract all endpoint definitions.\n *\n * Calls each operation function with a sentinel value to get the resolved\n * path, then replaces the sentinel with regex capture groups for matching.\n */\nfunction tryExtractEndpoint(\n obj: OperationFunction,\n): { operation: OperationFunction; descriptor: RestEndpointDescriptor } | null {\n try {\n const paramCount = obj.length || 1;\n const sentinelArgs = Array.from({ length: paramCount }, () => PARAM_MARKER);\n const descriptor = obj(...sentinelArgs);\n if (isEndpointDescriptor(descriptor)) {\n return { operation: obj, descriptor };\n }\n } catch {\n // Some functions may need different args - skip\n }\n return null;\n}\n\nfunction walkContract(\n obj: any,\n parentPath: string[],\n): Array<{ operation: OperationFunction; descriptor: RestEndpointDescriptor }> {\n if (typeof obj === 'function') {\n const result = tryExtractEndpoint(obj as OperationFunction);\n return result ? [result] : [];\n }\n\n if (typeof obj === 'object' && obj !== null) {\n const endpoints: Array<{\n operation: OperationFunction;\n descriptor: RestEndpointDescriptor;\n }> = [];\n for (const [key, value] of Object.entries(obj)) {\n endpoints.push(...walkContract(value, [...parentPath, key]));\n }\n return endpoints;\n }\n\n return [];\n}\n\n/**\n * Convert a resolved path with sentinel markers into a regex pattern.\n *\n * Also extracts path parameter names from the original function's parameter list.\n *\n * @example\n * // For path '/users/\\x00/posts/\\x00' with param names ['userId', 'postId']\n * // Returns: /\\/users\\/([^/]+)\\/posts\\/([^/]+)/\n */\nfunction buildPathInfo(resolvedPath: string): {\n regex: RegExp;\n template: string;\n paramNames: string[];\n} {\n const sentinelCount = resolvedPath.split(PARAM_MARKER).length - 1;\n\n // Generate parameter names (p1, p2, ... if we don't know the real names)\n const paramNames = Array.from(\n { length: sentinelCount },\n (_, i) => `p${i + 1}`,\n );\n\n // Build regex by replacing sentinels with capture groups\n const escaped = resolvedPath.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const regexStr = escaped.split(PARAM_MARKER).join('([^/]+)');\n\n // Build template by replacing sentinels with ${paramName} syntax\n let template = resolvedPath;\n for (const name of paramNames) {\n template = template.replace(PARAM_MARKER, `\\${${name}}`);\n }\n\n return {\n regex: new RegExp(`^${regexStr}$`),\n template,\n paramNames,\n };\n}\n\n/**\n * Create server-ready route definitions from a REST contract.\n *\n * Walks the contract tree, extracts endpoint definitions, and returns\n * structured route definitions with:\n * - HTTP method and path\n * - Body schema for request conversion (JSON→XML)\n * - Response schemas for response conversion (XML→JSON)\n * - Path parameter extraction\n *\n * @example\n * ```typescript\n * const server = createServer(adtContract);\n *\n * // Match an incoming request\n * const match = server.match('GET', '/sap/bc/adt/cts/transportrequests/DEVK900001');\n * if (match) {\n * console.log(match.route.method); // 'GET'\n * console.log(match.params); // { p1: 'devk900001' }\n * console.log(match.route.responseSchemas[200]); // Serializable schema\n * }\n * ```\n */\nexport function createServer<T extends Record<string, any>>(\n contract: T,\n): ServerRoutes {\n const endpoints = walkContract(contract, []);\n\n const routes: RouteDefinition[] = endpoints.map(\n ({ operation, descriptor }) => {\n const resolvedPath = descriptor.path || '';\n\n const { regex, template, paramNames } = buildPathInfo(resolvedPath);\n\n // Extract body schema\n const bodySchema =\n descriptor.body && isSerializable(descriptor.body)\n ? descriptor.body\n : undefined;\n\n // Extract response schemas (only those that are Serializable)\n const responseSchemas: Record<number, Serializable> = {};\n if (descriptor.responses) {\n for (const [status, schema] of Object.entries(descriptor.responses)) {\n if (isSerializable(schema)) {\n responseSchemas[Number(status)] = schema;\n }\n }\n }\n\n return {\n method: descriptor.method,\n path: resolvedPath,\n pathTemplate: template,\n pathParamNames: paramNames,\n bodySchema,\n responseSchemas,\n requestHeaders: descriptor.headers as\n | Record<string, string>\n | undefined,\n _regex: regex,\n } as RouteDefinition & { _regex: RegExp };\n },\n );\n\n return {\n routes,\n\n match(\n method: string,\n url: string,\n basePath = '',\n ): { route: RouteDefinition; params: Record<string, string> } | null {\n const pathname = url.split('?')[0];\n let relativePath = pathname;\n if (basePath && pathname.startsWith(basePath)) {\n relativePath = pathname.slice(basePath.length) || '/';\n }\n\n const targetMethod = method.toUpperCase();\n for (const route of routes) {\n if (route.method !== targetMethod) continue;\n\n const routeWithRegex = route as RouteDefinition & { _regex: RegExp };\n const match = relativePath.match(routeWithRegex._regex);\n if (match) {\n const params: Record<string, string> = {};\n for (let i = 0; i < route.pathParamNames.length; i++) {\n params[route.pathParamNames[i]] = match[i + 1] || '';\n }\n return { route, params };\n }\n }\n\n return null;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,207 @@
1
+ //#region src/core/types.d.ts
2
+ /**
3
+ * Speci v0.1 - Core Types (Protocol-Agnostic)
4
+ *
5
+ * These types work across all protocols: REST, GraphQL, gRPC, WebSocket, etc.
6
+ * Protocol-specific types are in their respective modules.
7
+ */
8
+ /**
9
+ * Schema type - can be any type that describes data structure
10
+ * Works with: Zod, JSON Schema, TypeScript types, Protobuf, GraphQL types, etc.
11
+ */
12
+ type Schema<T = any> = T;
13
+ /**
14
+ * Generic operation descriptor
15
+ * Protocol-specific descriptors extend this base interface
16
+ */
17
+ interface OperationDescriptor {
18
+ /** Optional metadata */
19
+ metadata?: {
20
+ /** Operation description */
21
+ description?: string;
22
+ /** Tags for grouping */
23
+ tags?: string[];
24
+ /** Deprecation notice */
25
+ deprecated?: boolean;
26
+ /** Any additional metadata */
27
+ [key: string]: any;
28
+ };
29
+ }
30
+ /**
31
+ * Arrow function that defines an operation contract
32
+ * Generic across all protocols
33
+ */
34
+ type OperationFunction<TParams extends any[] = any[], TDescriptor extends OperationDescriptor = OperationDescriptor> = (...params: TParams) => TDescriptor;
35
+ /**
36
+ * Contract - a collection of operation functions
37
+ * Works for REST, GraphQL, gRPC, etc.
38
+ */
39
+ type Contract = {
40
+ [key: string]: OperationFunction | Contract;
41
+ };
42
+ /**
43
+ * Extract parameter types from an operation function
44
+ */
45
+ type ExtractParams<T> = T extends ((...args: infer P) => any) ? P : never;
46
+ /**
47
+ * Extract descriptor from an operation function
48
+ */
49
+ type ExtractDescriptor<T> = T extends ((...args: any[]) => infer D) ? D : never;
50
+ //#endregion
51
+ //#region src/rest/types.d.ts
52
+ /**
53
+ * HTTP methods for REST APIs
54
+ */
55
+ type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
56
+ /**
57
+ * Inferrable schema interface (explicit _infer property)
58
+ *
59
+ * Schemas that implement this interface will have their types automatically inferred.
60
+ *
61
+ * @example
62
+ * const UserSchema = {
63
+ * ...yourSchemaDefinition,
64
+ * _infer: undefined as unknown as User
65
+ * } as const;
66
+ *
67
+ * responses: { 200: UserSchema } // Type is User
68
+ */
69
+ interface Inferrable<T = unknown> {
70
+ /** Type inference marker - never accessed at runtime */
71
+ _infer?: T;
72
+ }
73
+ /**
74
+ * Serializable schema interface (parse/build methods + Inferrable)
75
+ *
76
+ * Extends Inferrable so schemas with parse() automatically get _infer marker.
77
+ * This is the preferred pattern for schema libraries like ts-xsd.
78
+ *
79
+ * @example
80
+ * const UserSchema = {
81
+ * parse: (raw: string): User => JSON.parse(raw),
82
+ * build: (data: User): string => JSON.stringify(data),
83
+ * };
84
+ *
85
+ * responses: { 200: UserSchema } // Type is User (inferred via _infer from Inferrable)
86
+ */
87
+ interface Serializable<T = unknown> extends Inferrable<T> {
88
+ /** Parse raw string to typed object */
89
+ parse(raw: string): T;
90
+ /** Build typed object to string */
91
+ build?(data: T): string;
92
+ }
93
+ /**
94
+ * Create an Inferrable schema with automatic type inference
95
+ * Cleaner than the manual _infer pattern
96
+ */
97
+ declare function createInferrable<T>(): Inferrable<T>;
98
+ /**
99
+ * Infer type from a schema
100
+ *
101
+ * Uses _infer property from Inferrable<T> (which Serializable extends).
102
+ * Falls back to original type if no _infer property.
103
+ *
104
+ * @example
105
+ * type A = InferSchema<{ _infer?: User }> // User
106
+ * type B = InferSchema<string> // string (fallback)
107
+ */
108
+ type InferSchema<T> = T extends {
109
+ _infer?: infer U;
110
+ } ? NonNullable<U> : T;
111
+ /**
112
+ * Schema-like object - can be any schema library (Zod, JSON Schema, custom, etc.)
113
+ * Adapters interpret these based on their capabilities.
114
+ */
115
+ type SchemaLike = unknown;
116
+ /**
117
+ * Helper to wrap a schema with type information
118
+ * Use this if your schema doesn't implement Inferrable
119
+ *
120
+ * @example
121
+ * responses: { 200: schema(UserSchema, {} as User) }
122
+ */
123
+ declare function schema<T>(schemaObject: SchemaLike, _type: T): Inferrable<T>;
124
+ /**
125
+ * Response map - maps status codes to response types or schemas
126
+ *
127
+ * Values can be:
128
+ * - Type assertions: `undefined as unknown as MyType`
129
+ * - Schema objects: `MyZodSchema`, `MyElementSchema`, etc.
130
+ * - Schema with type: `MySchema as SchemaWithType<MyType>`
131
+ * - The adapter will interpret them appropriately
132
+ */
133
+ type ResponseMap = Record<number, SchemaLike>;
134
+ /**
135
+ * REST endpoint descriptor
136
+ * Extends the protocol-agnostic OperationDescriptor with REST-specific fields
137
+ */
138
+ interface RestEndpointDescriptor<TMethod extends RestMethod = RestMethod, TPath extends string = string, TBodySchema = unknown, TResponses extends ResponseMap = ResponseMap> extends OperationDescriptor {
139
+ /** HTTP method */
140
+ method: TMethod;
141
+ /** URL path with optional template variables */
142
+ path: TPath;
143
+ /** Request body schema - type will be inferred from Inferrable schemas */
144
+ body?: InferSchema<TBodySchema>;
145
+ /** Query parameters schema */
146
+ query?: Schema;
147
+ /** Headers schema */
148
+ headers?: Schema;
149
+ /** Response schemas by HTTP status code */
150
+ responses: TResponses;
151
+ /** REST-specific metadata */
152
+ metadata?: RestMetadata;
153
+ }
154
+ /**
155
+ * REST-specific metadata
156
+ */
157
+ interface RestMetadata {
158
+ /** Endpoint description */
159
+ description?: string;
160
+ /** Tags for grouping */
161
+ tags?: string[];
162
+ /** Deprecation notice */
163
+ deprecated?: boolean;
164
+ /** Rate limit information */
165
+ rateLimit?: {
166
+ requests: number;
167
+ window: string;
168
+ };
169
+ /** Cache control */
170
+ cache?: {
171
+ maxAge?: number;
172
+ private?: boolean;
173
+ };
174
+ /** Any additional metadata */
175
+ [key: string]: any;
176
+ }
177
+ /**
178
+ * REST operation function type
179
+ */
180
+ type RestOperationFunction<TParams extends any[] = any[], TDescriptor extends RestEndpointDescriptor = RestEndpointDescriptor> = (...params: TParams) => TDescriptor;
181
+ /**
182
+ * REST Contract - enforces that all properties are either:
183
+ * - RestOperationFunction (endpoint)
184
+ * - Nested RestContract (namespace)
185
+ */
186
+ type RestContract = {
187
+ [key: string]: RestOperationFunction | RestContract;
188
+ };
189
+ /**
190
+ * Extract response type for a specific HTTP status code
191
+ * Automatically infers types from Inferrable schemas
192
+ */
193
+ type ExtractResponse<T extends RestEndpointDescriptor, Status extends keyof T['responses']> = InferSchema<T['responses'][Status]>;
194
+ /**
195
+ * Infer the success response type (2xx status codes only)
196
+ * Filters out error responses (4xx, 5xx) and only returns 2xx response types
197
+ * Automatically infers types from Inferrable schemas
198
+ */
199
+ type InferSuccessResponse<T extends RestEndpointDescriptor> = Exclude<{ [K in keyof T['responses']]: K extends 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 ? InferSchema<T['responses'][K]> : never; }[keyof T['responses']], never | undefined>;
200
+ /**
201
+ * Infer the error response type (non-2xx status codes)
202
+ * Captures all error responses (4xx, 5xx) as a union type
203
+ */
204
+ type InferErrorResponse<T extends RestEndpointDescriptor> = Exclude<{ [K in keyof T['responses']]: K extends 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 ? never : T['responses'][K]; }[keyof T['responses']], never | undefined>;
205
+ //#endregion
206
+ export { ExtractDescriptor as _, Inferrable as a, OperationFunction as b, RestEndpointDescriptor as c, RestOperationFunction as d, SchemaLike as f, Contract as g, schema as h, InferSuccessResponse as i, RestMetadata as l, createInferrable as m, InferErrorResponse as n, ResponseMap as o, Serializable as p, InferSchema as r, RestContract as s, ExtractResponse as t, RestMethod as u, ExtractParams as v, Schema as x, OperationDescriptor as y };
207
+ //# sourceMappingURL=types-BcGS790c.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-BcGS790c.d.mts","names":[],"sources":["../src/core/types.ts","../src/rest/types.ts"],"mappings":";;;;;;;;;;;KAWY,OAAO,WAAW;;;;;UAMb;;EAEf;;IAEE;;IAGA;;IAGA;;KAGC;;;;;;;KAQO,kBACV,+BACA,oBAAoB,sBAAsB,2BACpC,QAAQ,YAAY;;;;;KAMhB;GACT,cAAc,oBAAoB;;;;;KAMzB,cAAc,KAAK,eAAc,YAAY,aAAY;;;;KAKzD,kBAAkB,KAAK,eAAc,sBAAsB,KACnE;;;;;;KCnDQ;;;;;;;;;;;;;;UAsBK,WAAW;;EAE1B,SAAS;;;;;;;;;;;;;;;;UAiBM,aAAa,qBAAqB,WAAW;;EAE5D,MAAM,cAAc;;EAEpB,OAAO,MAAM;;;;;;iBAOC,iBAAiB,MAAM,WAAW;;;;;;;;;;;KActC,YAAY,KAAK;EAAY,eAAe;IACpD,YAAY,KACZ;;;;;KAMQ;;;;;;;;iBASI,OAAO,GAAG,cAAc,YAAY,OAAO,IAAI,WAAW;;;;;;;;;;KAa9D,cAAc,eAAe;;;;;UAMxB,uBACf,gBAAgB,aAAa,YAC7B,+BACA,uBACA,mBAAmB,cAAc,qBACzB;;EAER,QAAQ;;EAGR,MAAM;;EAGN,OAAO,YAAY;;EAGnB,QAAQ;;EAGR,UAAU;;EAGV,WAAW;;EAGX,WAAW;;;;;UAMI;;EAEf;;EAGA;;EAGA;;EAGA;IACE;IACA;;;EAIF;IACE;IACA;;;GAID;;;;;KAMS,sBACV,+BACA,oBAAoB,yBAAyB,8BACvC,QAAQ,YAAY;;;;;;KAOhB;GACT,cAAc,wBAAwB;;;;;;KAO7B,gBACV,UAAU,wBACV,qBAAqB,kBACnB,YAAY,eAAe;;;;;;KAOnB,qBAAqB,UAAU,0BAA0B,WAEhE,WAAW,iBAAiB,sEAWzB,YAAY,eAAe,qBAEzB;;;;;KAQE,mBAAmB,UAAU,0BAA0B,WAE9D,WAAW,iBAAiB,8EAYzB,eAAe,YACb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abapify/speci",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "Minimal arrow-function-based contract specification system for TypeScript",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -11,6 +11,7 @@
11
11
  "exports": {
12
12
  ".": "./dist/index.mjs",
13
13
  "./rest": "./dist/rest/index.mjs",
14
+ "./rest/server": "./dist/rest/server/index.mjs",
14
15
  "./package.json": "./package.json"
15
16
  },
16
17
  "files": [