@abapify/speci 0.4.34 → 0.4.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,464 +1,36 @@
1
- # speci
1
+ # @abapify/speci
2
2
 
3
- [![version](https://img.shields.io/github/package-json/v/abapify/adt-cli?filename=packages/speci/package.json)](https://github.com/abapify/adt-cli/pkgs/npm/%40abapify%2Fspeci)
3
+ [![npm](https://img.shields.io/npm/v/%40abapify%2Fspeci)](https://www.npmjs.com/package/@abapify/speci)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/abapify/adt-cli/blob/main/LICENSE)
4
5
 
5
- **Minimal arrow-function-based contract specification system for TypeScript**
6
+ Minimal arrow-function-based contract specification system for TypeScript.
6
7
 
7
- Zero decorators. Zero DSL. Zero dependencies. Just TypeScript arrow functions.
8
-
9
- Part of the **ADT Toolkit** foundation - see [main README](../../README.md) for architecture overview.
10
-
11
- ## Philosophy
12
-
13
- An endpoint = an arrow function whose parameters define the contract and whose return value defines the operation.
14
-
15
- ```typescript
16
- import { http } from '@abapify/speci/rest';
17
-
18
- // Shortcut syntax - super clean!
19
- const updateUser = (id: string, user: UserInput) =>
20
- http.put<User>(`/users/${id}`, user);
21
- ```
22
-
23
- This is extremely expressive while staying minimal and TypeScript-native. Choose between shortcut syntax for simplicity or full syntax for control.
24
-
25
- ## Developed with ts-xsd
26
-
27
- Speci was developed in tight combination with [ts-xsd](../ts-xsd) to support **XML-based contracts**. While most REST libraries assume JSON, Speci's `Serializable<T>` interface enables seamless integration with XML schemas:
28
-
29
- ```typescript
30
- import { parse, build, type XsdSchema, type InferXsd } from 'ts-xsd';
31
- import { http } from '@abapify/speci/rest';
32
-
33
- // ts-xsd schema with parse/build
34
- const TransportSchema = {
35
- /* ... */
36
- } as const satisfies XsdSchema;
37
- type Transport = InferXsd<typeof TransportSchema>;
38
-
39
- const transportApi = {
40
- // Speci infers types from ts-xsd's Serializable interface
41
- create: (transport: Transport) =>
42
- http.post<Transport>('/transports', transport),
43
- };
44
- ```
45
-
46
- This makes Speci ideal for enterprise APIs that use XML (SAP ADT, SOAP services, etc.) while remaining fully compatible with JSON-based schemas like Zod.
47
-
48
- ## Modular Architecture
49
-
50
- Speci is organized into protocol-specific modules:
51
-
52
- - **`speci`** - Core types and utilities (protocol-agnostic)
53
- - **`speci/rest`** - REST API (helpers, types, client generation)
54
- - **`speci/openapi`** - OpenAPI generation (planned)
55
- - **`speci/cli`** - CLI generation (planned)
56
- - **`speci/graphql`** - GraphQL (planned)
57
- - **`speci/grpc`** - gRPC (planned)
58
-
59
- Each protocol module is self-contained with its own types, helpers, and client generation.
60
-
61
- ## Installation
8
+ ## Install
62
9
 
63
10
  ```bash
64
- npm install speci
65
- # or
66
- bun add speci
67
- ```
68
-
69
- ## Quick Start
70
-
71
- ### 1. Define Your Contract
72
-
73
- ```typescript
74
- import { http, type RestContract } from '@abapify/speci/rest';
75
-
76
- // Define schemas (use any schema library: Zod, JSON Schema, etc.)
77
- interface User {
78
- id: string;
79
- name: string;
80
- email: string;
81
- }
82
-
83
- interface CreateUserInput {
84
- name: string;
85
- email: string;
86
- }
87
-
88
- interface ErrorResponse {
89
- error: string;
90
- message: string;
91
- }
92
-
93
- // Define your API contract - choose your style!
94
- export const api = {
95
- users: {
96
- // Shortcut syntax - just specify the success type
97
- list: () => http.get<User[]>('/users'),
98
-
99
- // Full syntax - explicit control over all responses
100
- get: (id: string) =>
101
- http.get(`/users/${id}`, {
102
- responses: {
103
- 200: undefined as unknown as User,
104
- 404: undefined as unknown as ErrorResponse,
105
- },
106
- }),
107
-
108
- // Shortcut - pass body directly
109
- create: (user: CreateUserInput) => http.post<User>('/users', user),
110
-
111
- // Full syntax with body and responses
112
- update: (id: string, user: Partial<User>) =>
113
- http.put(`/users/${id}`, {
114
- body: user,
115
- responses: {
116
- 200: undefined as unknown as User,
117
- 404: undefined as unknown as ErrorResponse,
118
- },
119
- }),
120
-
121
- // Shortcut - defaults to 204 response
122
- delete: (id: string) => http.delete(`/users/${id}`),
123
- },
124
- } satisfies RestContract;
11
+ npm install @abapify/speci
125
12
  ```
126
13
 
127
- ### 2. Generate a Typed Client
128
-
129
- ```typescript
130
- import {
131
- createClient,
132
- createFetchAdapter,
133
- HttpError,
134
- } from '@abapify/speci/rest';
135
-
136
- const client = createClient(api, {
137
- baseUrl: 'https://api.example.com',
138
- adapter: createFetchAdapter(),
139
- });
14
+ ## Usage
140
15
 
141
- // Use it with full type safety - returns only success types
142
- const users = await client.users.list(); // Type: User[]
143
- const user = await client.users.get('123'); // Type: User
144
-
145
- // Errors are thrown as HttpError with typed payloads
146
- try {
147
- const user = await client.users.get('999');
148
- } catch (error) {
149
- if (client.users.get.isError(error)) {
150
- // error.payload is typed as ErrorResponse!
151
- console.error(error.status, error.payload.error);
152
- }
153
- }
154
- ```
155
-
156
- ## Core Concepts
157
-
158
- ### Endpoint Descriptors
159
-
160
- Every endpoint is defined by an arrow function that returns a descriptor:
161
-
162
- ```typescript
163
- const endpoint = (...params) => ({
164
- method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS',
165
- path: '/path/with/${params}',
166
- body: any, // Request body schema
167
- query: any, // Query parameters schema
168
- headers: any, // Headers schema
169
- responses: {
170
- // Response schemas by status code
171
- 200: SuccessSchema,
172
- 400: ErrorSchema,
173
- },
174
- metadata: {
175
- // Optional metadata
176
- description: 'Endpoint description',
177
- tags: ['user', 'admin'],
178
- deprecated: false,
179
- },
180
- });
181
- ```
182
-
183
- ### HTTP Helper Object
184
-
185
- The `@abapify/speci/rest` module provides an `http` object with all HTTP methods:
186
-
187
- ```typescript
16
+ ```ts
188
17
  import { http } from '@abapify/speci/rest';
189
18
 
190
- // Shortcut syntax - super clean!
191
- http.get<SuccessType>(path); // GET with 200 response
192
- http.post<SuccessType>(path, body); // POST with 201 response
193
- http.put<SuccessType>(path, body); // PUT with 200 response
194
- http.patch<SuccessType>(path, body); // PATCH with 200 response
195
- http.delete(path); // DELETE with 204 response
196
-
197
- // Full syntax - explicit control
198
- http.get(path, { responses: { 200: Type, 404: Error } });
199
- http.post(path, { body, responses: { 201: Type, 400: Error } });
200
-
201
- // Examples
202
19
  const getUser = (id: string) => http.get<User>(`/users/${id}`);
203
- const createUser = (user: CreateUserInput) => http.post<User>('/users', user);
204
- ```
205
-
206
- **Why `http` object?** The `delete` keyword is reserved in JavaScript, so we use `http.delete` instead of a standalone `delete` function.
207
-
208
- ### Schema Support
209
-
210
- Speci supports **any schema library** - Zod, JSON Schema, custom schemas, etc. Use the `schema()` helper for clean syntax:
211
-
212
- ```typescript
213
- import { z } from 'zod';
214
- import { schema } from '@abapify/speci/rest';
215
-
216
- // Zod schemas
217
- const UserSchema = z.object({
218
- id: z.string(),
219
- name: z.string(),
220
- email: z.string().email(),
221
- });
222
-
223
- type User = z.infer<typeof UserSchema>;
224
-
225
- const api = {
226
- getUser: (id: string) =>
227
- http.get(`/users/${id}`, {
228
- responses: { 200: schema(UserSchema, {} as User) }, // ✅ Schema + type
229
- }),
230
- };
231
-
232
- // Custom schemas (e.g., XML schemas for ADT)
233
- const ClassSchema = {
234
- element: 'class',
235
- attributes: ['name', 'type'],
236
- // ... your schema definition
237
- };
238
-
239
- interface ClassXml {
240
- name: string;
241
- type: string;
242
- // ... type definition
243
- }
244
-
245
- const adtApi = {
246
- getClass: (name: string) =>
247
- http.get(`/classes/${name}`, {
248
- responses: { 200: schema(ClassSchema, {} as ClassXml) }, // ✅ Schema + type
249
- }),
250
- };
251
- ```
252
-
253
- **How it works:**
254
-
255
- - `schema(schemaObject, {} as Type)` provides both runtime schema and compile-time type
256
- - Your **adapter** sees the schema object and uses it to parse/validate
257
- - TypeScript sees the type for full type safety
258
- - Type-only assertions still work: `undefined as unknown as Type`
259
-
260
- This makes Speci **schema-agnostic** - use whatever validation library you prefer!
261
-
262
- ### Global Error Responses
263
-
264
- Avoid repeating error types across all endpoints:
265
-
266
- ```typescript
267
- import { createHttp } from '@abapify/speci/rest';
268
-
269
- // 1. Define global error responses
270
- const globalErrors = {
271
- 400: undefined as unknown as ApiError,
272
- 401: undefined as unknown as ApiError,
273
- 403: undefined as unknown as ApiError,
274
- 404: undefined as unknown as ApiError,
275
- 500: undefined as unknown as ApiError,
276
- } as const;
277
-
278
- // 2. Create http instance with global errors
279
- const api = createHttp(globalErrors);
280
-
281
- // 3. Now only specify success responses!
282
- const userApi = {
283
- list: () => api.get<User[]>('/users'),
284
- // 400, 401, 403, 404, 500 automatically added!
285
-
286
- get: (id: string) => api.get<User>(`/users/${id}`),
287
- // Global errors merged automatically
288
-
289
- create: (user: CreateUserInput) => api.post<User>('/users', user),
290
- // All endpoints get global errors
291
- };
292
- ```
293
-
294
- ### Path Parameters
295
-
296
- Path parameters are automatically extracted from template literals:
297
-
298
- ```typescript
299
- const getPost = (userId: string, postId: string) =>
300
- http.get<Post>(`/users/${userId}/posts/${postId}`);
301
-
302
- // Speci automatically maps:
303
- // - First param (userId) → ${userId} in path
304
- // - Second param (postId) → ${postId} in path
305
- ```
306
-
307
- ### Error Handling
308
-
309
- Errors are thrown as `HttpError` with typed payloads:
310
-
311
- ```typescript
312
- import { HttpError } from '@abapify/speci/rest';
313
-
314
- try {
315
- const user = await client.users.get('123');
316
- // user is typed as User (not User | ErrorResponse)
317
- } catch (error) {
318
- // Option 1: Use endpoint-specific type guard
319
- if (client.users.get.isError(error)) {
320
- // error.payload is typed as ErrorResponse
321
- console.error(`HTTP ${error.status}:`, error.payload.error);
322
- }
323
-
324
- // Option 2: Generic HttpError check
325
- else if (error instanceof HttpError) {
326
- console.error(`HTTP ${error.status}:`, error.payload);
327
- }
328
-
329
- // Option 3: Network or other errors
330
- else {
331
- console.error('Network error:', error);
332
- }
333
- }
334
- ```
335
-
336
- ### Custom HTTP Adapters
337
-
338
- Speci is adapter-agnostic. Bring your own HTTP client:
339
-
340
- ```typescript
341
- import type { HttpAdapter } from '@abapify/speci/rest';
342
-
343
- const myAdapter: HttpAdapter = {
344
- async request({ method, url, body, query, headers }) {
345
- // Use any HTTP client: axios, got, ky, wretch, etc.
346
- return await yourHttpClient.request({ method, url, body, query, headers });
347
- },
348
- };
349
-
350
- const client = createClient(api, {
351
- baseUrl: 'https://api.example.com',
352
- adapter: myAdapter,
353
- });
354
- ```
355
-
356
- ### Interceptors
357
-
358
- Add request/response/error interceptors:
359
-
360
- ```typescript
361
- const client = createClient(api, {
362
- baseUrl: 'https://api.example.com',
363
- adapter: createFetchAdapter(),
364
-
365
- // Add auth token to all requests
366
- onRequest: async (options) => ({
367
- ...options,
368
- headers: {
369
- ...options.headers,
370
- Authorization: `Bearer ${getToken()}`,
371
- },
372
- }),
373
-
374
- // Transform responses
375
- onResponse: async (response) => {
376
- console.log('Response:', response);
377
- return response;
378
- },
379
-
380
- // Handle errors
381
- onError: async (error) => {
382
- console.error('Error:', error);
383
- throw error;
384
- },
385
- });
386
- ```
387
-
388
- ## What Speci Can Generate
389
-
390
- From your arrow-function contracts, Speci can generate:
391
-
392
- - ✅ **Typed clients** (implemented)
393
- - 🚧 **Server routing** (planned)
394
- - 🚧 **OpenAPI specs** (planned)
395
- - 🚧 **Mock servers** (planned)
396
- - 🚧 **Test fixtures** (planned)
397
- - 🚧 **CLI tools** (planned)
398
- - 🚧 **GraphQL schemas** (planned)
399
- - 🚧 **gRPC definitions** (planned)
400
-
401
- ## Why Arrow Functions?
402
-
403
- ✅ **No TypeScript AST parsing** - TS gives you function types natively
404
- ✅ **No decorators** - They're optional syntactic sugar
405
- ✅ **No template-literal parsing** - Simple variable extraction
406
- ✅ **Perfectly readable** - Looks like ordinary domain code
407
- ✅ **Fully expressible** - All REST/HATEOAS/gRPC concepts can be wrapped
408
- ✅ **Zero framework coupling** - Pure TypeScript
409
-
410
- ## Coming from ts-rest?
411
-
412
- Speci uses the same contract-first philosophy, but with arrow functions:
413
-
414
- ```typescript
415
- // ts-rest
416
- const contract = c.router({
417
- getUser: {
418
- method: 'GET',
419
- path: '/users/:id',
420
- responses: { 200: UserSchema },
421
- },
422
- });
423
-
424
- // Speci
425
- const contract = {
426
- getUser: (id: string) => http.get<User>(`/users/${id}`),
427
- };
20
+ const updateUser = (id: string, user: UserInput) =>
21
+ http.put<User>(`/users/${id}`, user);
428
22
  ```
429
23
 
430
- Same type safety, less boilerplate. Choose ts-rest for maturity, Speci for simplicity and XML support.
431
-
432
- ## Comparison with ts-rest
433
-
434
- [ts-rest](https://ts-rest.com/) is an excellent, mature library for type-safe REST contracts and we recommend it for most use cases. Speci covers specific scenarios where you need: **dynamic routing as functions** (endpoints defined as arrow functions with parameters), **schema flexibility** (any schema library via adapters, not just Zod), and a **flexible plugin system** for custom extensions. If ts-rest's contract-first approach works for you, use it—it's battle-tested and production-ready.
435
-
436
- ## Comparison with Other Tools
24
+ ## Features
437
25
 
438
- | Feature | Speci | ts-rest | tRPC | OpenAPI |
439
- | ------------------ | --------------- | ----------- | ---------- | ---------- |
440
- | **Syntax** | Arrow functions | Builder API | Procedures | YAML/JSON |
441
- | **Type Safety** | ✅ Full | ✅ Full | ✅ Full | ⚠️ Codegen |
442
- | **Dependencies** | 0 | Few | Many | Many |
443
- | **Learning Curve** | Minimal | Low | Medium | High |
444
- | **Client** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
445
- | **Server** | 🚧 Planned | ✅ Yes | ✅ Yes | ⚠️ Partial |
446
- | **Validation** | 🚧 Planned | ✅ Zod | ✅ Zod | ⚠️ Varies |
26
+ - Arrow-function contract specs with full type inference
27
+ - Generate typed clients and servers from one contract
28
+ - Schema-agnostic (works with `ts-xsd`, Zod, …)
447
29
 
448
30
  ## Documentation
449
31
 
450
- - [Body Parameter Inference](./docs/body-inference.md) - Automatic body type inference from schemas
451
-
452
- ## License
453
-
454
- MIT
455
-
456
- ## Contributing
32
+ **[adt-cli.netlify.app/sdk/packages/speci](https://adt-cli.netlify.app/sdk/packages/speci)**
457
33
 
458
- Contributions welcome! This is v0.1 - the minimal viable core. Future versions will add:
34
+ ---
459
35
 
460
- - Server adapters (Express, Fastify, Hono, etc.)
461
- - OpenAPI generation
462
- - Mock server generation
463
- - Schema validation (Zod, JSON Schema, etc.)
464
- - And more!
36
+ Part of [**abapify**](https://github.com/abapify/adt-cli) — open, typed tooling for SAP ABAP Development Tools (ADT): CLI, MCP server, and SDK.
@@ -1,4 +1,4 @@
1
- import { c as RestEndpointDescriptor, p as Serializable } from "./types-B9U8g_J3.mjs";
1
+ import { c as RestEndpointDescriptor, f as Serializable } from "./types-irfsjGFv.mjs";
2
2
  /**
3
3
  * HTTP server request (abstraction over node:http IncomingMessage, Web Request, etc.)
4
4
  */
@@ -109,4 +109,4 @@ interface ServerRoutes {
109
109
  */
110
110
  declare function createServer<T extends Record<string, any>>(contract: T): ServerRoutes;
111
111
  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 };
112
- //# sourceMappingURL=index-CTc5gKBG.d.mts.map
112
+ //# sourceMappingURL=index-tt7oXC8m.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index-CTc5gKBG.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;;;;;;;;;;;;;;;;;;;;;;;;;;iBC0HvB,aAAa,UAAU,qBACrC,UAAU,IACT"}
1
+ {"version":3,"file":"index-tt7oXC8m.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;;;;;;;;;;;;;;;;;;;;;;;;;;iBC0HvB,aAAa,UAAU,qBACrC,UAAU,IACT"}
package/dist/index.d.mts CHANGED
@@ -1,2 +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-B9U8g_J3.mjs";
1
+ import { _ as ExtractParams, a as Inferrable, b as Schema, f as Serializable, g as ExtractDescriptor, h as Contract, v as OperationDescriptor, y as OperationFunction } from "./types-irfsjGFv.mjs";
2
2
  export { Contract, ExtractDescriptor, ExtractParams, type Inferrable, OperationDescriptor, OperationFunction, Schema, type Serializable };
@@ -1,5 +1,5 @@
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-B9U8g_J3.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-CTc5gKBG.mjs";
1
+ import { _ as ExtractParams, a as Inferrable, b as Schema, c as RestEndpointDescriptor, d as RestOperationFunction, f as Serializable, g as ExtractDescriptor, i as InferSuccessResponse, l as RestMetadata, m as schema, n as InferErrorResponse, o as ResponseMap, p as createInferrable, r as InferSchema, s as RestContract, t as ExtractResponse, u as RestMethod, y as OperationFunction } from "../types-irfsjGFv.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-tt7oXC8m.mjs";
3
3
  /**
4
4
  * Options for REST endpoint helpers
5
5
  */
@@ -121,11 +121,11 @@ interface ClientConfig {
121
121
  /** Default headers for all requests */
122
122
  headers?: Record<string, string>;
123
123
  /** Request interceptor */
124
- onRequest?: (options: any) => any | Promise<any>;
124
+ onRequest?: (options: any) => any;
125
125
  /** Response interceptor */
126
- onResponse?: (response: any) => any | Promise<any>;
126
+ onResponse?: (response: any) => any;
127
127
  /** Error interceptor */
128
- onError?: (error: any) => any | Promise<any>;
128
+ onError?: (error: any) => any;
129
129
  }
130
130
  /**
131
131
  * Check if body is an Inferrable schema (has _infer property)
@@ -178,5 +178,5 @@ declare function createClient<T extends Record<string, any>>(contract: T, config
178
178
  * Create a fetch-based HTTP adapter
179
179
  */
180
180
  declare function createFetchAdapter(options?: RequestInit): HttpAdapter;
181
- 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 };
181
+ 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 Serializable, type ServerConfig, type ServerHandler, type ServerRequest, type ServerResponse, type ServerRoutes, createClient, createFetchAdapter, createHttp, createInferrable, createServer, http, schema };
182
182
  //# sourceMappingURL=index.d.mts.map
@@ -1 +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"}
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;;EAGb,cAAc;;EAGd,WAAW;;;;;KAMR,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"}
@@ -1,4 +1,4 @@
1
- import { t as createServer } from "../server-BuLDJYvH.mjs";
1
+ import { t as createServer } from "../server-q84R0M2o.mjs";
2
2
 
3
3
  // src/rest/types.ts
4
4
  /**
@@ -1 +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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"}
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' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | '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 * 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: unknown, _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, unknown>;\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 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226\n ? InferSchema<T['responses'][K]>\n : never;\n }[keyof T['responses']],\n 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 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226\n ? never\n : T['responses'][K];\n }[keyof T['responses']],\n 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;\n\n /** Response interceptor */\n onResponse?: (response: any) => any;\n\n /** Error interceptor */\n onError?: (error: any) => 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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"}
@@ -1,2 +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-CTc5gKBG.mjs";
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-tt7oXC8m.mjs";
2
2
  export { type RouteContext, type RouteDefinition, type ServerConfig, type ServerHandler, type ServerRequest, type ServerResponse, type ServerRoutes, createServer };
@@ -1,3 +1,3 @@
1
- import { t as createServer } from "../../server-BuLDJYvH.mjs";
1
+ import { t as createServer } from "../../server-q84R0M2o.mjs";
2
2
 
3
3
  export { createServer };
@@ -54,7 +54,7 @@ function walkContract(obj, parentPath) {
54
54
  function buildPathInfo(resolvedPath) {
55
55
  const sentinelCount = resolvedPath.split(PARAM_MARKER).length - 1;
56
56
  const paramNames = Array.from({ length: sentinelCount }, (_, i) => `p${i + 1}`);
57
- const regexStr = resolvedPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").split(PARAM_MARKER).join("([^/]+)");
57
+ const regexStr = resolvedPath.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`).split(PARAM_MARKER).join("([^/]+)");
58
58
  let template = resolvedPath;
59
59
  for (const name of paramNames) template = template.replace(PARAM_MARKER, `\${${name}}`);
60
60
  return {
@@ -63,6 +63,43 @@ function buildPathInfo(resolvedPath) {
63
63
  paramNames
64
64
  };
65
65
  }
66
+ function routeFromEndpoint({ descriptor }) {
67
+ const resolvedPath = descriptor.path || "";
68
+ const { regex, template, paramNames } = buildPathInfo(resolvedPath);
69
+ const bodySchema = descriptor.body && isSerializable(descriptor.body) ? descriptor.body : void 0;
70
+ const responseSchemas = {};
71
+ if (descriptor.responses) {
72
+ for (const [status, schema] of Object.entries(descriptor.responses)) if (isSerializable(schema)) responseSchemas[Number(status)] = schema;
73
+ }
74
+ return {
75
+ method: descriptor.method,
76
+ path: resolvedPath,
77
+ pathTemplate: template,
78
+ pathParamNames: paramNames,
79
+ bodySchema,
80
+ responseSchemas,
81
+ requestHeaders: descriptor.headers,
82
+ _regex: regex
83
+ };
84
+ }
85
+ function matchRoute(routes, method, url, basePath) {
86
+ const pathname = url.split("?")[0];
87
+ const relativePath = basePath && pathname.startsWith(basePath) ? pathname.slice(basePath.length) || "/" : pathname;
88
+ const targetMethod = method.toUpperCase();
89
+ for (const route of routes) {
90
+ if (route.method !== targetMethod) continue;
91
+ const match = route._regex.exec(relativePath);
92
+ if (match) {
93
+ const params = {};
94
+ for (let i = 0; i < route.pathParamNames.length; i++) params[route.pathParamNames[i]] = match[i + 1] || "";
95
+ return {
96
+ route,
97
+ params
98
+ };
99
+ }
100
+ }
101
+ return null;
102
+ }
66
103
  /**
67
104
  * Create server-ready route definitions from a REST contract.
68
105
  *
@@ -87,48 +124,12 @@ function buildPathInfo(resolvedPath) {
87
124
  * ```
88
125
  */
89
126
  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
- });
127
+ const routes = walkContract(contract, []).map(routeFromEndpoint);
109
128
  return {
110
129
  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 match = route._regex.exec(relativePath);
119
- if (match) {
120
- const params = {};
121
- for (let i = 0; i < route.pathParamNames.length; i++) params[route.pathParamNames[i]] = match[i + 1] || "";
122
- return {
123
- route,
124
- params
125
- };
126
- }
127
- }
128
- return null;
129
- }
130
+ match: (method, url, basePath = "") => matchRoute(routes, method, url, basePath)
130
131
  };
131
132
  }
132
133
 
133
134
  export { createServer as t };
134
- //# sourceMappingURL=server-BuLDJYvH.mjs.map
135
+ //# sourceMappingURL=server-q84R0M2o.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-q84R0M2o.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, String.raw`\\$&`);\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\ntype WalkedEndpoint = {\n operation: OperationFunction;\n descriptor: RestEndpointDescriptor;\n};\n\nfunction routeFromEndpoint({\n descriptor,\n}: WalkedEndpoint): RouteDefinition & { _regex: RegExp } {\n const resolvedPath = descriptor.path || '';\n const { regex, template, paramNames } = buildPathInfo(resolvedPath);\n\n const bodySchema =\n descriptor.body && isSerializable(descriptor.body)\n ? descriptor.body\n : undefined;\n\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 Record<string, string> | undefined,\n _regex: regex,\n } as RouteDefinition & { _regex: RegExp };\n}\n\nfunction matchRoute(\n routes: RouteDefinition[],\n method: string,\n url: string,\n basePath: string,\n): { route: RouteDefinition; params: Record<string, string> } | null {\n const pathname = url.split('?')[0];\n const relativePath =\n basePath && pathname.startsWith(basePath)\n ? pathname.slice(basePath.length) || '/'\n : pathname;\n\n const targetMethod = method.toUpperCase();\n for (const route of routes) {\n if (route.method !== targetMethod) continue;\n const match = (route as RouteDefinition & { _regex: RegExp })._regex.exec(\n relativePath,\n );\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 return null;\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 routes: RouteDefinition[] = walkContract(contract, []).map(\n routeFromEndpoint,\n );\n\n return {\n routes,\n match: (method, url, basePath = '') =>\n matchRoute(routes, method, url, basePath),\n };\n}\n"],"mappings":"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"}
@@ -105,11 +105,6 @@ declare function createInferrable<T>(): Inferrable<T>;
105
105
  type InferSchema<T> = T extends {
106
106
  _infer?: infer U;
107
107
  } ? NonNullable<U> : T;
108
- /**
109
- * Schema-like object - can be any schema library (Zod, JSON Schema, custom, etc.)
110
- * Adapters interpret these based on their capabilities.
111
- */
112
- type SchemaLike = unknown;
113
108
  /**
114
109
  * Helper to wrap a schema with type information
115
110
  * Use this if your schema doesn't implement Inferrable
@@ -117,7 +112,7 @@ type SchemaLike = unknown;
117
112
  * @example
118
113
  * responses: { 200: schema(UserSchema, {} as User) }
119
114
  */
120
- declare function schema<T>(schemaObject: SchemaLike, _type: T): Inferrable<T>;
115
+ declare function schema<T>(schemaObject: unknown, _type: T): Inferrable<T>;
121
116
  /**
122
117
  * Response map - maps status codes to response types or schemas
123
118
  *
@@ -127,7 +122,7 @@ declare function schema<T>(schemaObject: SchemaLike, _type: T): Inferrable<T>;
127
122
  * - Schema with type: `MySchema as SchemaWithType<MyType>`
128
123
  * - The adapter will interpret them appropriately
129
124
  */
130
- type ResponseMap = Record<number, SchemaLike>;
125
+ type ResponseMap = Record<number, unknown>;
131
126
  /**
132
127
  * REST endpoint descriptor
133
128
  * Extends the protocol-agnostic OperationDescriptor with REST-specific fields
@@ -193,11 +188,11 @@ type ExtractResponse<T extends RestEndpointDescriptor, Status extends keyof T['r
193
188
  * Filters out error responses (4xx, 5xx) and only returns 2xx response types
194
189
  * Automatically infers types from Inferrable schemas
195
190
  */
196
- 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>;
191
+ 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']], undefined>;
197
192
  /**
198
193
  * Infer the error response type (non-2xx status codes)
199
194
  * Captures all error responses (4xx, 5xx) as a union type
200
195
  */
201
- 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>;
202
- 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 };
203
- //# sourceMappingURL=types-B9U8g_J3.d.mts.map
196
+ 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']], undefined>;
197
+ export { ExtractParams as _, Inferrable as a, Schema as b, RestEndpointDescriptor as c, RestOperationFunction as d, Serializable as f, ExtractDescriptor as g, Contract as h, InferSuccessResponse as i, RestMetadata as l, schema as m, InferErrorResponse as n, ResponseMap as o, createInferrable as p, InferSchema as r, RestContract as s, ExtractResponse as t, RestMethod as u, OperationDescriptor as v, OperationFunction as y };
198
+ //# sourceMappingURL=types-irfsjGFv.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-irfsjGFv.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;;;;;;;;;;;;;;UAgBK,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;;;;;;;;iBASY,OAAO,GAAG,uBAAuB,OAAO,IAAI,WAAW;;;;;;;;;;KAa3D,cAAc;;;;;UAMT,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,sEAEzB,YAAY,eAAe,qBAEzB;;;;;KAQE,mBAAmB,UAAU,0BAA0B,WAE9D,WAAW,iBAAiB,8EAGzB,eAAe,YACb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abapify/speci",
3
- "version": "0.4.34",
3
+ "version": "0.4.36",
4
4
  "description": "Minimal arrow-function-based contract specification system for TypeScript",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-BuLDJYvH.mjs","names":["routeWithRegex"],"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\ntype WalkedEndpoint = {\n operation: OperationFunction;\n descriptor: RestEndpointDescriptor;\n};\n\nfunction routeFromEndpoint({\n descriptor,\n}: WalkedEndpoint): RouteDefinition & { _regex: RegExp } {\n const resolvedPath = descriptor.path || '';\n const { regex, template, paramNames } = buildPathInfo(resolvedPath);\n\n const bodySchema =\n descriptor.body && isSerializable(descriptor.body)\n ? descriptor.body\n : undefined;\n\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 Record<string, string> | undefined,\n _regex: regex,\n } as RouteDefinition & { _regex: RegExp };\n}\n\nfunction matchRoute(\n routes: RouteDefinition[],\n method: string,\n url: string,\n basePath: string,\n): { route: RouteDefinition; params: Record<string, string> } | null {\n const pathname = url.split('?')[0];\n const relativePath =\n basePath && pathname.startsWith(basePath)\n ? pathname.slice(basePath.length) || '/'\n : pathname;\n\n const targetMethod = method.toUpperCase();\n for (const route of routes) {\n if (route.method !== targetMethod) continue;\n const match = relativePath.match(\n (route as RouteDefinition & { _regex: RegExp })._regex,\n );\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 return null;\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> | 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 = routeWithRegex._regex.exec(relativePath);\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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-B9U8g_J3.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"}