@abapify/speci 0.0.0 β†’ 0.3.5

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.
Files changed (2) hide show
  1. package/README.md +463 -2
  2. package/package.json +36 -3
package/README.md CHANGED
@@ -1,3 +1,464 @@
1
- # @abapify/speci
1
+ # speci
2
2
 
3
- Placeholder. The real release is published via CI/CD.
3
+ [![version](https://img.shields.io/github/package-json/v/abapify/adt-cli?filename=packages/speci/package.json)](https://github.com/abapify/adt-cli/pkgs/npm/%40abapify%2Fspeci)
4
+
5
+ **Minimal arrow-function-based contract specification system for TypeScript**
6
+
7
+ Zero decorators. Zero DSL. Zero dependencies. Just TypeScript arrow functions.
8
+
9
+ Part of the **ADT Toolkit** foundation - see [main README](../../README.md) for architecture overview.
10
+
11
+ ## Philosophy
12
+
13
+ An endpoint = an arrow function whose parameters define the contract and whose return value defines the operation.
14
+
15
+ ```typescript
16
+ import { http } from '@abapify/speci/rest';
17
+
18
+ // Shortcut syntax - super clean!
19
+ const updateUser = (id: string, user: UserInput) =>
20
+ http.put<User>(`/users/${id}`, user);
21
+ ```
22
+
23
+ This is extremely expressive while staying minimal and TypeScript-native. Choose between shortcut syntax for simplicity or full syntax for control.
24
+
25
+ ## Developed with ts-xsd
26
+
27
+ Speci was developed in tight combination with [ts-xsd](../ts-xsd) to support **XML-based contracts**. While most REST libraries assume JSON, Speci's `Serializable<T>` interface enables seamless integration with XML schemas:
28
+
29
+ ```typescript
30
+ import { parse, build, type XsdSchema, type InferXsd } from 'ts-xsd';
31
+ import { http } from '@abapify/speci/rest';
32
+
33
+ // ts-xsd schema with parse/build
34
+ const TransportSchema = {
35
+ /* ... */
36
+ } as const satisfies XsdSchema;
37
+ type Transport = InferXsd<typeof TransportSchema>;
38
+
39
+ const transportApi = {
40
+ // Speci infers types from ts-xsd's Serializable interface
41
+ create: (transport: Transport) =>
42
+ http.post<Transport>('/transports', transport),
43
+ };
44
+ ```
45
+
46
+ This makes Speci ideal for enterprise APIs that use XML (SAP ADT, SOAP services, etc.) while remaining fully compatible with JSON-based schemas like Zod.
47
+
48
+ ## Modular Architecture
49
+
50
+ Speci is organized into protocol-specific modules:
51
+
52
+ - **`speci`** - Core types and utilities (protocol-agnostic)
53
+ - **`speci/rest`** - REST API (helpers, types, client generation)
54
+ - **`speci/openapi`** - OpenAPI generation (planned)
55
+ - **`speci/cli`** - CLI generation (planned)
56
+ - **`speci/graphql`** - GraphQL (planned)
57
+ - **`speci/grpc`** - gRPC (planned)
58
+
59
+ Each protocol module is self-contained with its own types, helpers, and client generation.
60
+
61
+ ## Installation
62
+
63
+ ```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;
125
+ ```
126
+
127
+ ### 2. Generate a Typed Client
128
+
129
+ ```typescript
130
+ import {
131
+ createClient,
132
+ createFetchAdapter,
133
+ HttpError,
134
+ } from '@abapify/speci/rest';
135
+
136
+ const client = createClient(api, {
137
+ baseUrl: 'https://api.example.com',
138
+ adapter: createFetchAdapter(),
139
+ });
140
+
141
+ // Use it with full type safety - returns only success types
142
+ const users = await client.users.list(); // Type: User[]
143
+ const user = await client.users.get('123'); // Type: User
144
+
145
+ // Errors are thrown as HttpError with typed payloads
146
+ try {
147
+ const user = await client.users.get('999');
148
+ } catch (error) {
149
+ if (client.users.get.isError(error)) {
150
+ // error.payload is typed as ErrorResponse!
151
+ console.error(error.status, error.payload.error);
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## Core Concepts
157
+
158
+ ### Endpoint Descriptors
159
+
160
+ Every endpoint is defined by an arrow function that returns a descriptor:
161
+
162
+ ```typescript
163
+ const endpoint = (...params) => ({
164
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS',
165
+ path: '/path/with/${params}',
166
+ body: any, // Request body schema
167
+ query: any, // Query parameters schema
168
+ headers: any, // Headers schema
169
+ responses: {
170
+ // Response schemas by status code
171
+ 200: SuccessSchema,
172
+ 400: ErrorSchema,
173
+ },
174
+ metadata: {
175
+ // Optional metadata
176
+ description: 'Endpoint description',
177
+ tags: ['user', 'admin'],
178
+ deprecated: false,
179
+ },
180
+ });
181
+ ```
182
+
183
+ ### HTTP Helper Object
184
+
185
+ The `@abapify/speci/rest` module provides an `http` object with all HTTP methods:
186
+
187
+ ```typescript
188
+ import { http } from '@abapify/speci/rest';
189
+
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
+ const getUser = (id: string) => http.get<User>(`/users/${id}`);
203
+ const createUser = (user: CreateUserInput) => http.post<User>('/users', user);
204
+ ```
205
+
206
+ **Why `http` object?** The `delete` keyword is reserved in JavaScript, so we use `http.delete` instead of a standalone `delete` function.
207
+
208
+ ### Schema Support
209
+
210
+ Speci supports **any schema library** - Zod, JSON Schema, custom schemas, etc. Use the `schema()` helper for clean syntax:
211
+
212
+ ```typescript
213
+ import { z } from 'zod';
214
+ import { schema } from '@abapify/speci/rest';
215
+
216
+ // Zod schemas
217
+ const UserSchema = z.object({
218
+ id: z.string(),
219
+ name: z.string(),
220
+ email: z.string().email(),
221
+ });
222
+
223
+ type User = z.infer<typeof UserSchema>;
224
+
225
+ const api = {
226
+ getUser: (id: string) =>
227
+ http.get(`/users/${id}`, {
228
+ responses: { 200: schema(UserSchema, {} as User) }, // βœ… Schema + type
229
+ }),
230
+ };
231
+
232
+ // Custom schemas (e.g., XML schemas for ADT)
233
+ const ClassSchema = {
234
+ element: 'class',
235
+ attributes: ['name', 'type'],
236
+ // ... your schema definition
237
+ };
238
+
239
+ interface ClassXml {
240
+ name: string;
241
+ type: string;
242
+ // ... type definition
243
+ }
244
+
245
+ const adtApi = {
246
+ getClass: (name: string) =>
247
+ http.get(`/classes/${name}`, {
248
+ responses: { 200: schema(ClassSchema, {} as ClassXml) }, // βœ… Schema + type
249
+ }),
250
+ };
251
+ ```
252
+
253
+ **How it works:**
254
+
255
+ - `schema(schemaObject, {} as Type)` provides both runtime schema and compile-time type
256
+ - Your **adapter** sees the schema object and uses it to parse/validate
257
+ - TypeScript sees the type for full type safety
258
+ - Type-only assertions still work: `undefined as unknown as Type`
259
+
260
+ This makes Speci **schema-agnostic** - use whatever validation library you prefer!
261
+
262
+ ### Global Error Responses
263
+
264
+ Avoid repeating error types across all endpoints:
265
+
266
+ ```typescript
267
+ import { createHttp } from '@abapify/speci/rest';
268
+
269
+ // 1. Define global error responses
270
+ const globalErrors = {
271
+ 400: undefined as unknown as ApiError,
272
+ 401: undefined as unknown as ApiError,
273
+ 403: undefined as unknown as ApiError,
274
+ 404: undefined as unknown as ApiError,
275
+ 500: undefined as unknown as ApiError,
276
+ } as const;
277
+
278
+ // 2. Create http instance with global errors
279
+ const api = createHttp(globalErrors);
280
+
281
+ // 3. Now only specify success responses!
282
+ const userApi = {
283
+ list: () => api.get<User[]>('/users'),
284
+ // 400, 401, 403, 404, 500 automatically added!
285
+
286
+ get: (id: string) => api.get<User>(`/users/${id}`),
287
+ // Global errors merged automatically
288
+
289
+ create: (user: CreateUserInput) => api.post<User>('/users', user),
290
+ // All endpoints get global errors
291
+ };
292
+ ```
293
+
294
+ ### Path Parameters
295
+
296
+ Path parameters are automatically extracted from template literals:
297
+
298
+ ```typescript
299
+ const getPost = (userId: string, postId: string) =>
300
+ http.get<Post>(`/users/${userId}/posts/${postId}`);
301
+
302
+ // Speci automatically maps:
303
+ // - First param (userId) β†’ ${userId} in path
304
+ // - Second param (postId) β†’ ${postId} in path
305
+ ```
306
+
307
+ ### Error Handling
308
+
309
+ Errors are thrown as `HttpError` with typed payloads:
310
+
311
+ ```typescript
312
+ import { HttpError } from '@abapify/speci/rest';
313
+
314
+ try {
315
+ const user = await client.users.get('123');
316
+ // user is typed as User (not User | ErrorResponse)
317
+ } catch (error) {
318
+ // Option 1: Use endpoint-specific type guard
319
+ if (client.users.get.isError(error)) {
320
+ // error.payload is typed as ErrorResponse
321
+ console.error(`HTTP ${error.status}:`, error.payload.error);
322
+ }
323
+
324
+ // Option 2: Generic HttpError check
325
+ else if (error instanceof HttpError) {
326
+ console.error(`HTTP ${error.status}:`, error.payload);
327
+ }
328
+
329
+ // Option 3: Network or other errors
330
+ else {
331
+ console.error('Network error:', error);
332
+ }
333
+ }
334
+ ```
335
+
336
+ ### Custom HTTP Adapters
337
+
338
+ Speci is adapter-agnostic. Bring your own HTTP client:
339
+
340
+ ```typescript
341
+ import type { HttpAdapter } from '@abapify/speci/rest';
342
+
343
+ const myAdapter: HttpAdapter = {
344
+ async request({ method, url, body, query, headers }) {
345
+ // Use any HTTP client: axios, got, ky, wretch, etc.
346
+ return await yourHttpClient.request({ method, url, body, query, headers });
347
+ },
348
+ };
349
+
350
+ const client = createClient(api, {
351
+ baseUrl: 'https://api.example.com',
352
+ adapter: myAdapter,
353
+ });
354
+ ```
355
+
356
+ ### Interceptors
357
+
358
+ Add request/response/error interceptors:
359
+
360
+ ```typescript
361
+ const client = createClient(api, {
362
+ baseUrl: 'https://api.example.com',
363
+ adapter: createFetchAdapter(),
364
+
365
+ // Add auth token to all requests
366
+ onRequest: async (options) => ({
367
+ ...options,
368
+ headers: {
369
+ ...options.headers,
370
+ Authorization: `Bearer ${getToken()}`,
371
+ },
372
+ }),
373
+
374
+ // Transform responses
375
+ onResponse: async (response) => {
376
+ console.log('Response:', response);
377
+ return response;
378
+ },
379
+
380
+ // Handle errors
381
+ onError: async (error) => {
382
+ console.error('Error:', error);
383
+ throw error;
384
+ },
385
+ });
386
+ ```
387
+
388
+ ## What Speci Can Generate
389
+
390
+ From your arrow-function contracts, Speci can generate:
391
+
392
+ - βœ… **Typed clients** (implemented)
393
+ - 🚧 **Server routing** (planned)
394
+ - 🚧 **OpenAPI specs** (planned)
395
+ - 🚧 **Mock servers** (planned)
396
+ - 🚧 **Test fixtures** (planned)
397
+ - 🚧 **CLI tools** (planned)
398
+ - 🚧 **GraphQL schemas** (planned)
399
+ - 🚧 **gRPC definitions** (planned)
400
+
401
+ ## Why Arrow Functions?
402
+
403
+ βœ… **No TypeScript AST parsing** - TS gives you function types natively
404
+ βœ… **No decorators** - They're optional syntactic sugar
405
+ βœ… **No template-literal parsing** - Simple variable extraction
406
+ βœ… **Perfectly readable** - Looks like ordinary domain code
407
+ βœ… **Fully expressible** - All REST/HATEOAS/gRPC concepts can be wrapped
408
+ βœ… **Zero framework coupling** - Pure TypeScript
409
+
410
+ ## Coming from ts-rest?
411
+
412
+ Speci uses the same contract-first philosophy, but with arrow functions:
413
+
414
+ ```typescript
415
+ // ts-rest
416
+ const contract = c.router({
417
+ getUser: {
418
+ method: 'GET',
419
+ path: '/users/:id',
420
+ responses: { 200: UserSchema },
421
+ },
422
+ });
423
+
424
+ // Speci
425
+ const contract = {
426
+ getUser: (id: string) => http.get<User>(`/users/${id}`),
427
+ };
428
+ ```
429
+
430
+ Same type safety, less boilerplate. Choose ts-rest for maturity, Speci for simplicity and XML support.
431
+
432
+ ## Comparison with ts-rest
433
+
434
+ [ts-rest](https://ts-rest.com/) is an excellent, mature library for type-safe REST contracts and we recommend it for most use cases. Speci covers specific scenarios where you need: **dynamic routing as functions** (endpoints defined as arrow functions with parameters), **schema flexibility** (any schema library via adapters, not just Zod), and a **flexible plugin system** for custom extensions. If ts-rest's contract-first approach works for you, use itβ€”it's battle-tested and production-ready.
435
+
436
+ ## Comparison with Other Tools
437
+
438
+ | Feature | Speci | ts-rest | tRPC | OpenAPI |
439
+ | ------------------ | --------------- | ----------- | ---------- | ---------- |
440
+ | **Syntax** | Arrow functions | Builder API | Procedures | YAML/JSON |
441
+ | **Type Safety** | βœ… Full | βœ… Full | βœ… Full | ⚠️ Codegen |
442
+ | **Dependencies** | 0 | Few | Many | Many |
443
+ | **Learning Curve** | Minimal | Low | Medium | High |
444
+ | **Client** | βœ… Yes | βœ… Yes | βœ… Yes | βœ… Yes |
445
+ | **Server** | 🚧 Planned | βœ… Yes | βœ… Yes | ⚠️ Partial |
446
+ | **Validation** | 🚧 Planned | βœ… Zod | βœ… Zod | ⚠️ Varies |
447
+
448
+ ## Documentation
449
+
450
+ - [Body Parameter Inference](./docs/body-inference.md) - Automatic body type inference from schemas
451
+
452
+ ## License
453
+
454
+ MIT
455
+
456
+ ## Contributing
457
+
458
+ Contributions welcome! This is v0.1 - the minimal viable core. Future versions will add:
459
+
460
+ - Server adapters (Express, Fastify, Hono, etc.)
461
+ - OpenAPI generation
462
+ - Mock server generation
463
+ - Schema validation (Zod, JSON Schema, etc.)
464
+ - And more!
package/package.json CHANGED
@@ -1,5 +1,38 @@
1
1
  {
2
2
  "name": "@abapify/speci",
3
- "version": "0.0.0",
4
- "description": "Placeholder β€” real package is published via CI/CD (trusted publishing)."
5
- }
3
+ "version": "0.3.5",
4
+ "description": "Minimal arrow-function-based contract specification system for TypeScript",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "types": "./dist/index.d.mts",
11
+ "exports": {
12
+ ".": "./dist/index.mjs",
13
+ "./rest": "./dist/rest/index.mjs",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "api",
22
+ "contract",
23
+ "specification",
24
+ "typescript",
25
+ "client-generation",
26
+ "openapi"
27
+ ],
28
+ "dependencies": {},
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/abapify/adt-cli.git",
32
+ "directory": "packages/speci"
33
+ },
34
+ "homepage": "https://github.com/abapify/adt-cli/tree/main/packages/speci#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/abapify/adt-cli/issues"
37
+ }
38
+ }