@abapify/speci 0.4.34 → 0.4.38
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 +17 -445
- package/dist/{index-CTc5gKBG.d.mts → index-tt7oXC8m.d.mts} +2 -2
- package/dist/{index-CTc5gKBG.d.mts.map → index-tt7oXC8m.d.mts.map} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/rest/index.d.mts +6 -6
- package/dist/rest/index.d.mts.map +1 -1
- package/dist/rest/index.mjs +1 -1
- package/dist/rest/index.mjs.map +1 -1
- package/dist/rest/server/index.d.mts +1 -1
- package/dist/rest/server/index.mjs +1 -1
- package/dist/{server-BuLDJYvH.mjs → server-SVHmvvEB.mjs} +48 -40
- package/dist/server-SVHmvvEB.mjs.map +1 -0
- package/dist/{types-B9U8g_J3.d.mts → types-irfsjGFv.d.mts} +6 -11
- package/dist/types-irfsjGFv.d.mts.map +1 -0
- package/package.json +1 -1
- package/dist/server-BuLDJYvH.mjs.map +0 -1
- package/dist/types-B9U8g_J3.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -1,464 +1,36 @@
|
|
|
1
|
-
# speci
|
|
1
|
+
# @abapify/speci
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@abapify/speci)
|
|
4
|
+
[](https://github.com/abapify/adt-cli/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Minimal arrow-function-based contract specification system for TypeScript.
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
34
|
+
---
|
|
459
35
|
|
|
460
|
-
-
|
|
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,
|
|
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-
|
|
112
|
+
//# sourceMappingURL=index-tt7oXC8m.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;iBCsIvB,aAAa,UAAU,qBACrC,UAAU,IACT"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
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 };
|
package/dist/rest/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as
|
|
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-
|
|
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
|
|
124
|
+
onRequest?: (options: any) => any;
|
|
125
125
|
/** Response interceptor */
|
|
126
|
-
onResponse?: (response: any) => any
|
|
126
|
+
onResponse?: (response: any) => any;
|
|
127
127
|
/** Error interceptor */
|
|
128
|
-
onError?: (error: any) => 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
|
|
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
|
|
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"}
|
package/dist/rest/index.mjs
CHANGED
package/dist/rest/index.mjs.map
CHANGED
|
@@ -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-
|
|
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 };
|
|
@@ -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,
|
|
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,50 @@ 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
|
+
/** Strip the query string and any configured base path from a request URL. */
|
|
86
|
+
function relativeRequestPath(url, basePath) {
|
|
87
|
+
const pathname = url.split("?")[0];
|
|
88
|
+
if (basePath && pathname.startsWith(basePath)) return pathname.slice(basePath.length) || "/";
|
|
89
|
+
return pathname;
|
|
90
|
+
}
|
|
91
|
+
/** Build the params record from a regex match against a route. */
|
|
92
|
+
function paramsFromMatch(route, match) {
|
|
93
|
+
const params = {};
|
|
94
|
+
for (let i = 0; i < route.pathParamNames.length; i++) params[route.pathParamNames[i]] = match[i + 1] || "";
|
|
95
|
+
return params;
|
|
96
|
+
}
|
|
97
|
+
function matchRoute(routes, method, url, basePath) {
|
|
98
|
+
const relativePath = relativeRequestPath(url, basePath);
|
|
99
|
+
const targetMethod = method.toUpperCase();
|
|
100
|
+
for (const route of routes) {
|
|
101
|
+
if (route.method !== targetMethod) continue;
|
|
102
|
+
const match = route._regex.exec(relativePath);
|
|
103
|
+
if (match) return {
|
|
104
|
+
route,
|
|
105
|
+
params: paramsFromMatch(route, match)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
66
110
|
/**
|
|
67
111
|
* Create server-ready route definitions from a REST contract.
|
|
68
112
|
*
|
|
@@ -87,48 +131,12 @@ function buildPathInfo(resolvedPath) {
|
|
|
87
131
|
* ```
|
|
88
132
|
*/
|
|
89
133
|
function createServer(contract) {
|
|
90
|
-
const routes = walkContract(contract, []).map(
|
|
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
|
-
});
|
|
134
|
+
const routes = walkContract(contract, []).map(routeFromEndpoint);
|
|
109
135
|
return {
|
|
110
136
|
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
|
-
}
|
|
137
|
+
match: (method, url, basePath = "") => matchRoute(routes, method, url, basePath)
|
|
130
138
|
};
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
export { createServer as t };
|
|
134
|
-
//# sourceMappingURL=server-
|
|
142
|
+
//# sourceMappingURL=server-SVHmvvEB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-SVHmvvEB.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\n/** Strip the query string and any configured base path from a request URL. */\nfunction relativeRequestPath(url: string, basePath: string): string {\n const pathname = url.split('?')[0];\n if (basePath && pathname.startsWith(basePath)) {\n return pathname.slice(basePath.length) || '/';\n }\n return pathname;\n}\n\n/** Build the params record from a regex match against a route. */\nfunction paramsFromMatch(\n route: RouteDefinition,\n match: RegExpExecArray,\n): Record<string, string> {\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 params;\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 relativePath = relativeRequestPath(url, basePath);\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 return { route, params: paramsFromMatch(route, match) };\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:
|
|
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,
|
|
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']],
|
|
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']],
|
|
202
|
-
export {
|
|
203
|
-
//# sourceMappingURL=types-
|
|
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 +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"}
|