@abapify/speci 0.4.0 → 0.4.1
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/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -0
- package/dist/rest/index.d.mts +188 -0
- package/dist/rest/index.d.mts.map +1 -0
- package/dist/rest/index.mjs +277 -0
- package/dist/rest/index.mjs.map +1 -0
- package/dist/types-wn9lAKzN.d.mts +203 -0
- package/dist/types-wn9lAKzN.d.mts.map +1 -0
- package/package.json +1 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as ExtractDescriptor, a as Inferrable, b as OperationFunction, g as Contract, p as Serializable, v as ExtractParams, x as Schema, y as OperationDescriptor } from "./types-wn9lAKzN.mjs";
|
|
2
|
+
export { Contract, ExtractDescriptor, ExtractParams, type Inferrable, OperationDescriptor, OperationFunction, Schema, type Serializable };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { _ as ExtractDescriptor, a as Inferrable, b as OperationFunction, c as RestEndpointDescriptor, d as RestOperationFunction, f as SchemaLike, h as schema, i as InferSuccessResponse, l as RestMetadata, m as createInferrable, n as InferErrorResponse, o as ResponseMap, p as Serializable, r as InferSchema, s as RestContract, t as ExtractResponse, u as RestMethod, v as ExtractParams, x as Schema } from "../types-wn9lAKzN.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/rest/helpers.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Options for REST endpoint helpers
|
|
6
|
+
*/
|
|
7
|
+
interface RestEndpointOptions<TBodySchema = unknown, TResponses extends ResponseMap = ResponseMap> {
|
|
8
|
+
/** Request body schema - can be Inferrable schema or plain type */
|
|
9
|
+
body?: TBodySchema;
|
|
10
|
+
/** Query parameters schema */
|
|
11
|
+
query?: Schema;
|
|
12
|
+
/** Headers schema */
|
|
13
|
+
headers?: Schema;
|
|
14
|
+
/** Response schemas by status code (optional when using shortcut syntax) */
|
|
15
|
+
responses?: TResponses;
|
|
16
|
+
/** Optional REST metadata */
|
|
17
|
+
metadata?: RestMetadata;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* HTTP helper object with optional global responses
|
|
21
|
+
*/
|
|
22
|
+
type Http<TGlobalResponses extends ResponseMap = {}> = {
|
|
23
|
+
get: {
|
|
24
|
+
<TSuccess = unknown>(path: string): RestEndpointDescriptor<'GET', string, never, {
|
|
25
|
+
200: TSuccess;
|
|
26
|
+
} & TGlobalResponses>;
|
|
27
|
+
<TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>): RestEndpointDescriptor<'GET', TPath, never, TResponses & TGlobalResponses>;
|
|
28
|
+
};
|
|
29
|
+
post: <TPath extends string, TBody = unknown, TResponses extends ResponseMap = ResponseMap>(path: TPath, options: RestEndpointOptions<TBody, TResponses>) => RestEndpointDescriptor<'POST', TPath, TBody, TResponses & TGlobalResponses>;
|
|
30
|
+
put: <TPath extends string, TBody = unknown, TResponses extends ResponseMap = ResponseMap>(path: TPath, options: RestEndpointOptions<TBody, TResponses>) => RestEndpointDescriptor<'PUT', TPath, TBody, TResponses & TGlobalResponses>;
|
|
31
|
+
patch: <TPath extends string, TBody = unknown, TResponses extends ResponseMap = ResponseMap>(path: TPath, options: RestEndpointOptions<TBody, TResponses>) => RestEndpointDescriptor<'PATCH', TPath, TBody, TResponses & TGlobalResponses>;
|
|
32
|
+
delete: {
|
|
33
|
+
(path: string): RestEndpointDescriptor<'DELETE', string, never, {
|
|
34
|
+
204: undefined;
|
|
35
|
+
} & TGlobalResponses>;
|
|
36
|
+
<TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>): RestEndpointDescriptor<'DELETE', TPath, never, TResponses & TGlobalResponses>;
|
|
37
|
+
};
|
|
38
|
+
head: <TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>) => RestEndpointDescriptor<'HEAD', TPath, never, TResponses & TGlobalResponses>;
|
|
39
|
+
options: <TPath extends string, TResponses extends ResponseMap>(path: TPath, options: Omit<RestEndpointOptions<never, TResponses>, 'body'>) => RestEndpointDescriptor<'OPTIONS', TPath, never, TResponses & TGlobalResponses>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Create HTTP helpers for defining REST endpoints
|
|
43
|
+
*
|
|
44
|
+
* @param globalResponses - Optional: Global response map to merge with all endpoint responses
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Without global responses
|
|
48
|
+
* const http = createHttp();
|
|
49
|
+
* http.get('/users', {
|
|
50
|
+
* responses: { 200: User[], 404: NotFoundError }
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // With global error responses
|
|
55
|
+
* const globalErrors = {
|
|
56
|
+
* 400: {} as ApiError,
|
|
57
|
+
* 401: {} as ApiError,
|
|
58
|
+
* 500: {} as ApiError
|
|
59
|
+
* };
|
|
60
|
+
*
|
|
61
|
+
* const api = createHttp(globalErrors);
|
|
62
|
+
* api.get('/users', {
|
|
63
|
+
* responses: { 200: [] as User[] }
|
|
64
|
+
* // 400, 401, 500 are automatically added
|
|
65
|
+
* })
|
|
66
|
+
*/
|
|
67
|
+
declare function createHttp<TGlobalResponses extends ResponseMap = {}>(globalResponses?: TGlobalResponses): Http<TGlobalResponses>;
|
|
68
|
+
/**
|
|
69
|
+
* Default HTTP helper object
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* import { http } from '@abapify/speci/rest'
|
|
73
|
+
*
|
|
74
|
+
* http.get('/users', {
|
|
75
|
+
* responses: { 200: User[], 404: NotFoundError }
|
|
76
|
+
* })
|
|
77
|
+
*
|
|
78
|
+
* http.delete('/users/123', {
|
|
79
|
+
* responses: { 204: undefined, 404: NotFoundError }
|
|
80
|
+
* })
|
|
81
|
+
*/
|
|
82
|
+
declare const http: Http<{}>;
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/rest/client/types.d.ts
|
|
85
|
+
/**
|
|
86
|
+
* HTTP error with typed payload
|
|
87
|
+
*/
|
|
88
|
+
declare class HttpError<TPayload = unknown> extends Error {
|
|
89
|
+
readonly status: number;
|
|
90
|
+
readonly payload: TPayload;
|
|
91
|
+
constructor(status: number, payload: TPayload, message?: string);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* HTTP request options
|
|
95
|
+
*/
|
|
96
|
+
interface HttpRequestOptions {
|
|
97
|
+
method: string;
|
|
98
|
+
url: string;
|
|
99
|
+
body?: unknown;
|
|
100
|
+
query?: Record<string, any>;
|
|
101
|
+
headers?: Record<string, string>;
|
|
102
|
+
bodySchema?: unknown;
|
|
103
|
+
responses?: Record<number, unknown>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* HTTP adapter interface - consumer provides their own implementation
|
|
107
|
+
*
|
|
108
|
+
* @template TDefaultResponse - Default response type for all requests (useful for testing)
|
|
109
|
+
*/
|
|
110
|
+
interface HttpAdapter<TDefaultResponse = unknown> {
|
|
111
|
+
/**
|
|
112
|
+
* Execute an HTTP request
|
|
113
|
+
*/
|
|
114
|
+
request<TResponse = TDefaultResponse>(options?: HttpRequestOptions): Promise<TResponse>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Client configuration
|
|
118
|
+
*/
|
|
119
|
+
interface ClientConfig {
|
|
120
|
+
/** Base URL for all requests */
|
|
121
|
+
baseUrl: string;
|
|
122
|
+
/** HTTP adapter implementation */
|
|
123
|
+
adapter: HttpAdapter;
|
|
124
|
+
/** Default headers for all requests */
|
|
125
|
+
headers?: Record<string, string>;
|
|
126
|
+
/** Request interceptor */
|
|
127
|
+
onRequest?: (options: any) => any | Promise<any>;
|
|
128
|
+
/** Response interceptor */
|
|
129
|
+
onResponse?: (response: any) => any | Promise<any>;
|
|
130
|
+
/** Error interceptor */
|
|
131
|
+
onError?: (error: any) => any | Promise<any>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if body is an Inferrable schema (has _infer property)
|
|
135
|
+
*/
|
|
136
|
+
type IsInferrableBody<TBody> = TBody extends {
|
|
137
|
+
_infer?: any;
|
|
138
|
+
} ? true : false;
|
|
139
|
+
/**
|
|
140
|
+
* Check if method is PUT or PATCH (update operations that typically send partial data)
|
|
141
|
+
*/
|
|
142
|
+
type IsUpdateMethod<TMethod> = TMethod extends 'PUT' | 'PATCH' ? true : false;
|
|
143
|
+
/**
|
|
144
|
+
* Extract body type from a REST endpoint descriptor
|
|
145
|
+
* Only extracts if the body is an Inferrable schema
|
|
146
|
+
* For PUT/PATCH methods, returns Partial<T> since updates typically send partial data
|
|
147
|
+
*/
|
|
148
|
+
type ExtractBodyType<TDescriptor> = TDescriptor extends RestEndpointDescriptor<infer TMethod, any, infer TBody, any> ? IsInferrableBody<TBody> extends true ? TBody extends {
|
|
149
|
+
_infer?: infer U;
|
|
150
|
+
} ? IsUpdateMethod<TMethod> extends true ? Partial<U> : U : never : never : never;
|
|
151
|
+
/**
|
|
152
|
+
* Build parameter list for a REST client method
|
|
153
|
+
* - If descriptor has a body with Inferrable schema, append the inferred type as a parameter
|
|
154
|
+
* - Otherwise, use the function's declared parameters
|
|
155
|
+
*/
|
|
156
|
+
type BuildParams<T extends OperationFunction> = ExtractDescriptor<T> extends RestEndpointDescriptor ? ExtractBodyType<ExtractDescriptor<T>> extends never ? ExtractParams<T> : ExtractParams<T> extends [] ? [ExtractBodyType<ExtractDescriptor<T>>] : [...ExtractParams<T>, ExtractBodyType<ExtractDescriptor<T>>] : ExtractParams<T>;
|
|
157
|
+
/**
|
|
158
|
+
* Convert a REST operation function to a client method
|
|
159
|
+
* Only returns success response types (2xx) - errors are thrown as HttpError
|
|
160
|
+
* Includes typed error property for error response payloads
|
|
161
|
+
*
|
|
162
|
+
* Automatically infers parameter types from body schema if present
|
|
163
|
+
*/
|
|
164
|
+
type RestClientMethod<T extends OperationFunction> = {
|
|
165
|
+
(...params: BuildParams<T>): Promise<ExtractDescriptor<T> extends RestEndpointDescriptor ? InferSuccessResponse<ExtractDescriptor<T>> : never>; /** Typed error response payload - use with HttpError */
|
|
166
|
+
error: ExtractDescriptor<T> extends RestEndpointDescriptor ? InferErrorResponse<ExtractDescriptor<T>> : never; /** Check if error is from this endpoint */
|
|
167
|
+
isError(error: unknown): error is HttpError<ExtractDescriptor<T> extends RestEndpointDescriptor ? InferErrorResponse<ExtractDescriptor<T>> : never>;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Convert a REST contract to a typed client.
|
|
171
|
+
* Uses Record<string, any> instead of Contract to allow concrete types without index signature.
|
|
172
|
+
*/
|
|
173
|
+
type RestClient<T extends Record<string, any>> = { [K in keyof T]: T[K] extends OperationFunction ? RestClientMethod<T[K]> : T[K] extends Record<string, any> ? RestClient<T[K]> : never };
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/rest/client/create-client.d.ts
|
|
176
|
+
/**
|
|
177
|
+
* Create a typed REST client from a contract
|
|
178
|
+
*/
|
|
179
|
+
declare function createClient<T extends Record<string, any>>(contract: T, config: ClientConfig): RestClient<T>;
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/rest/client/fetch-adapter.d.ts
|
|
182
|
+
/**
|
|
183
|
+
* Create a fetch-based HTTP adapter
|
|
184
|
+
*/
|
|
185
|
+
declare function createFetchAdapter(options?: RequestInit): HttpAdapter;
|
|
186
|
+
//#endregion
|
|
187
|
+
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 SchemaLike, type Serializable, createClient, createFetchAdapter, createHttp, createInferrable, http, schema };
|
|
188
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/rest/helpers.ts","../../src/rest/client/types.ts","../../src/rest/client/create-client.ts","../../src/rest/client/fetch-adapter.ts"],"mappings":"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"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
//#region src/rest/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create an Inferrable schema with automatic type inference
|
|
4
|
+
* Cleaner than the manual _infer pattern
|
|
5
|
+
*/
|
|
6
|
+
function createInferrable() {
|
|
7
|
+
return { _infer: void 0 };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Helper to wrap a schema with type information
|
|
11
|
+
* Use this if your schema doesn't implement Inferrable
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* responses: { 200: schema(UserSchema, {} as User) }
|
|
15
|
+
*/
|
|
16
|
+
function schema(schemaObject, _type) {
|
|
17
|
+
return schemaObject;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/rest/helpers.ts
|
|
22
|
+
/**
|
|
23
|
+
* Create HTTP helpers for defining REST endpoints
|
|
24
|
+
*
|
|
25
|
+
* @param globalResponses - Optional: Global response map to merge with all endpoint responses
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Without global responses
|
|
29
|
+
* const http = createHttp();
|
|
30
|
+
* http.get('/users', {
|
|
31
|
+
* responses: { 200: User[], 404: NotFoundError }
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // With global error responses
|
|
36
|
+
* const globalErrors = {
|
|
37
|
+
* 400: {} as ApiError,
|
|
38
|
+
* 401: {} as ApiError,
|
|
39
|
+
* 500: {} as ApiError
|
|
40
|
+
* };
|
|
41
|
+
*
|
|
42
|
+
* const api = createHttp(globalErrors);
|
|
43
|
+
* api.get('/users', {
|
|
44
|
+
* responses: { 200: [] as User[] }
|
|
45
|
+
* // 400, 401, 500 are automatically added
|
|
46
|
+
* })
|
|
47
|
+
*/
|
|
48
|
+
function createHttp(globalResponses) {
|
|
49
|
+
const mergeResponses = (responses) => {
|
|
50
|
+
if (!globalResponses) return responses;
|
|
51
|
+
return {
|
|
52
|
+
...globalResponses,
|
|
53
|
+
...responses
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
get: (path, options) => {
|
|
58
|
+
if (!options) return {
|
|
59
|
+
method: "GET",
|
|
60
|
+
path,
|
|
61
|
+
responses: mergeResponses({ 200: void 0 })
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
method: "GET",
|
|
65
|
+
path,
|
|
66
|
+
...options,
|
|
67
|
+
responses: mergeResponses(options.responses || { 200: void 0 })
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
post: (path, options) => {
|
|
71
|
+
const { body, responses, ...rest } = options;
|
|
72
|
+
return {
|
|
73
|
+
method: "POST",
|
|
74
|
+
path,
|
|
75
|
+
body,
|
|
76
|
+
...rest,
|
|
77
|
+
responses: mergeResponses(responses)
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
put: (path, options) => {
|
|
81
|
+
const { body, responses, ...rest } = options;
|
|
82
|
+
return {
|
|
83
|
+
method: "PUT",
|
|
84
|
+
path,
|
|
85
|
+
body,
|
|
86
|
+
...rest,
|
|
87
|
+
responses: mergeResponses(responses)
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
patch: (path, options) => {
|
|
91
|
+
const { body, responses, ...rest } = options;
|
|
92
|
+
return {
|
|
93
|
+
method: "PATCH",
|
|
94
|
+
path,
|
|
95
|
+
body,
|
|
96
|
+
...rest,
|
|
97
|
+
responses: mergeResponses(responses)
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
delete: (path, options) => {
|
|
101
|
+
if (!options) return {
|
|
102
|
+
method: "DELETE",
|
|
103
|
+
path,
|
|
104
|
+
responses: mergeResponses({ 204: void 0 })
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
method: "DELETE",
|
|
108
|
+
path,
|
|
109
|
+
...options,
|
|
110
|
+
responses: mergeResponses(options.responses || { 204: void 0 })
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
head: (path, options) => ({
|
|
114
|
+
method: "HEAD",
|
|
115
|
+
path,
|
|
116
|
+
...options,
|
|
117
|
+
responses: mergeResponses(options.responses)
|
|
118
|
+
}),
|
|
119
|
+
options: (path, options) => ({
|
|
120
|
+
method: "OPTIONS",
|
|
121
|
+
path,
|
|
122
|
+
...options,
|
|
123
|
+
responses: mergeResponses(options.responses)
|
|
124
|
+
})
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Default HTTP helper object
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* import { http } from '@abapify/speci/rest'
|
|
132
|
+
*
|
|
133
|
+
* http.get('/users', {
|
|
134
|
+
* responses: { 200: User[], 404: NotFoundError }
|
|
135
|
+
* })
|
|
136
|
+
*
|
|
137
|
+
* http.delete('/users/123', {
|
|
138
|
+
* responses: { 204: undefined, 404: NotFoundError }
|
|
139
|
+
* })
|
|
140
|
+
*/
|
|
141
|
+
const http = createHttp();
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/rest/client/types.ts
|
|
145
|
+
/**
|
|
146
|
+
* HTTP error with typed payload
|
|
147
|
+
*/
|
|
148
|
+
var HttpError = class extends Error {
|
|
149
|
+
constructor(status, payload, message) {
|
|
150
|
+
super(message || `HTTP ${status}`);
|
|
151
|
+
this.status = status;
|
|
152
|
+
this.payload = payload;
|
|
153
|
+
this.name = "HttpError";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/rest/client/create-client.ts
|
|
159
|
+
/**
|
|
160
|
+
* Extract path parameters from a path template
|
|
161
|
+
* Example: "/users/${id}/posts/${postId}" -> ["id", "postId"]
|
|
162
|
+
*/
|
|
163
|
+
function extractPathParams(path) {
|
|
164
|
+
const matches = path.matchAll(/\$\{(\w+)\}/g);
|
|
165
|
+
return Array.from(matches, (m) => m[1]);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Replace path parameters with actual values
|
|
169
|
+
* Example: "/users/${id}" with {id: "123"} -> "/users/123"
|
|
170
|
+
*/
|
|
171
|
+
function replacePath(path, params) {
|
|
172
|
+
return path.replaceAll(/\$\{(\w+)\}/g, (_, key) => {
|
|
173
|
+
const value = params[key];
|
|
174
|
+
if (value === void 0) throw new Error(`Missing path parameter: ${key}`);
|
|
175
|
+
return encodeURIComponent(String(value));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check if a value is an Inferrable schema (has _infer property)
|
|
180
|
+
*/
|
|
181
|
+
function isInferrableSchema(value) {
|
|
182
|
+
return value && typeof value === "object" && "_infer" in value;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Create a client method from an operation function
|
|
186
|
+
*/
|
|
187
|
+
function createMethod(config, operationFn) {
|
|
188
|
+
const method = async (...args) => {
|
|
189
|
+
const descriptor = operationFn(...args);
|
|
190
|
+
const pathParamNames = descriptor.path ? extractPathParams(descriptor.path) : [];
|
|
191
|
+
const pathParams = {};
|
|
192
|
+
const firstArg = args[0];
|
|
193
|
+
if (firstArg && typeof firstArg === "object" && !Array.isArray(firstArg)) pathParamNames.forEach((name) => {
|
|
194
|
+
if (firstArg[name] !== void 0) pathParams[name] = firstArg[name];
|
|
195
|
+
});
|
|
196
|
+
else pathParamNames.forEach((name, index) => {
|
|
197
|
+
if (args[index] !== void 0) pathParams[name] = args[index];
|
|
198
|
+
});
|
|
199
|
+
let bodyData = descriptor.body;
|
|
200
|
+
let bodySchema = void 0;
|
|
201
|
+
const declaredParamCount = operationFn.length;
|
|
202
|
+
if (isInferrableSchema(descriptor.body)) {
|
|
203
|
+
bodySchema = descriptor.body;
|
|
204
|
+
bodyData = args[declaredParamCount];
|
|
205
|
+
}
|
|
206
|
+
const url = descriptor.path ? config.baseUrl + replacePath(descriptor.path, pathParams) : config.baseUrl;
|
|
207
|
+
let requestOptions = {
|
|
208
|
+
method: descriptor.method || "GET",
|
|
209
|
+
url,
|
|
210
|
+
body: bodyData,
|
|
211
|
+
bodySchema,
|
|
212
|
+
query: descriptor.query,
|
|
213
|
+
headers: {
|
|
214
|
+
...config.headers,
|
|
215
|
+
...descriptor.headers
|
|
216
|
+
},
|
|
217
|
+
responses: descriptor.responses
|
|
218
|
+
};
|
|
219
|
+
if (config.onRequest) requestOptions = await config.onRequest(requestOptions);
|
|
220
|
+
try {
|
|
221
|
+
let response = await config.adapter.request(requestOptions);
|
|
222
|
+
if (config.onResponse) response = await config.onResponse(response);
|
|
223
|
+
return response;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (config.onError) return await config.onError(error);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
method.error = void 0;
|
|
230
|
+
method.isError = (error) => {
|
|
231
|
+
return error instanceof Error && error.name === "HttpError";
|
|
232
|
+
};
|
|
233
|
+
return method;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Create a typed REST client from a contract
|
|
237
|
+
*/
|
|
238
|
+
function createClient(contract, config) {
|
|
239
|
+
const client = {};
|
|
240
|
+
for (const [key, value] of Object.entries(contract)) if (typeof value === "function") client[key] = createMethod(config, value);
|
|
241
|
+
else if (typeof value === "object" && value !== null) client[key] = createClient(value, config);
|
|
242
|
+
return client;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/rest/client/fetch-adapter.ts
|
|
247
|
+
/**
|
|
248
|
+
* Create a fetch-based HTTP adapter
|
|
249
|
+
*/
|
|
250
|
+
function createFetchAdapter(options) {
|
|
251
|
+
return { async request(opts) {
|
|
252
|
+
const { method, url, body, query, headers } = opts;
|
|
253
|
+
const fullUrl = url + (query ? "?" + new URLSearchParams(query).toString() : "");
|
|
254
|
+
const response = await fetch(fullUrl, {
|
|
255
|
+
...options,
|
|
256
|
+
method,
|
|
257
|
+
headers: {
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
...options?.headers,
|
|
260
|
+
...headers
|
|
261
|
+
},
|
|
262
|
+
body: body ? JSON.stringify(body) : void 0
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
const errorData = await response.json().catch(() => ({
|
|
266
|
+
error: "HTTP Error",
|
|
267
|
+
message: response.statusText
|
|
268
|
+
}));
|
|
269
|
+
throw new HttpError(response.status, errorData);
|
|
270
|
+
}
|
|
271
|
+
return await response.json();
|
|
272
|
+
} };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
export { HttpError, createClient, createFetchAdapter, createHttp, createInferrable, http, schema };
|
|
277
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/rest/types.ts","../../src/rest/helpers.ts","../../src/rest/client/types.ts","../../src/rest/client/create-client.ts","../../src/rest/client/fetch-adapter.ts"],"sourcesContent":["/**\n * Speci REST - REST-specific types\n */\n\nimport type { OperationDescriptor, Schema } from '../core/types';\n\n/**\n * HTTP methods for REST APIs\n */\nexport type RestMethod =\n | 'GET'\n | 'POST'\n | 'PUT'\n | 'PATCH'\n | 'DELETE'\n | 'HEAD'\n | 'OPTIONS';\n\n/**\n * Inferrable schema interface (explicit _infer property)\n *\n * Schemas that implement this interface will have their types automatically inferred.\n *\n * @example\n * const UserSchema = {\n * ...yourSchemaDefinition,\n * _infer: undefined as unknown as User\n * } as const;\n *\n * responses: { 200: UserSchema } // Type is User\n */\nexport interface Inferrable<T = unknown> {\n /** Type inference marker - never accessed at runtime */\n _infer?: T;\n}\n\n/**\n * Serializable schema interface (parse/build methods + Inferrable)\n *\n * Extends Inferrable so schemas with parse() automatically get _infer marker.\n * This is the preferred pattern for schema libraries like ts-xsd.\n *\n * @example\n * const UserSchema = {\n * parse: (raw: string): User => JSON.parse(raw),\n * build: (data: User): string => JSON.stringify(data),\n * };\n *\n * responses: { 200: UserSchema } // Type is User (inferred via _infer from Inferrable)\n */\nexport interface Serializable<T = unknown> extends Inferrable<T> {\n /** Parse raw string to typed object */\n parse(raw: string): T;\n /** Build typed object to string */\n build?(data: T): string;\n}\n\n/**\n * Create an Inferrable schema with automatic type inference\n * Cleaner than the manual _infer pattern\n */\nexport function createInferrable<T>(): Inferrable<T> {\n return { _infer: undefined as unknown as T };\n}\n\n/**\n * Infer type from a schema\n *\n * Uses _infer property from Inferrable<T> (which Serializable extends).\n * Falls back to original type if no _infer property.\n *\n * @example\n * type A = InferSchema<{ _infer?: User }> // User\n * type B = InferSchema<string> // string (fallback)\n */\nexport type InferSchema<T> = T extends { _infer?: infer U }\n ? NonNullable<U>\n : T;\n\n/**\n * Schema-like object - can be any schema library (Zod, JSON Schema, custom, etc.)\n * Adapters interpret these based on their capabilities.\n */\nexport type SchemaLike = unknown;\n\n/**\n * Helper to wrap a schema with type information\n * Use this if your schema doesn't implement Inferrable\n *\n * @example\n * responses: { 200: schema(UserSchema, {} as User) }\n */\nexport function schema<T>(schemaObject: SchemaLike, _type: T): Inferrable<T> {\n return schemaObject as Inferrable<T>;\n}\n\n/**\n * Response map - maps status codes to response types or schemas\n *\n * Values can be:\n * - Type assertions: `undefined as unknown as MyType`\n * - Schema objects: `MyZodSchema`, `MyElementSchema`, etc.\n * - Schema with type: `MySchema as SchemaWithType<MyType>`\n * - The adapter will interpret them appropriately\n */\nexport type ResponseMap = Record<number, SchemaLike>;\n\n/**\n * REST endpoint descriptor\n * Extends the protocol-agnostic OperationDescriptor with REST-specific fields\n */\nexport interface RestEndpointDescriptor<\n TMethod extends RestMethod = RestMethod,\n TPath extends string = string,\n TBodySchema = unknown,\n TResponses extends ResponseMap = ResponseMap,\n> extends OperationDescriptor {\n /** HTTP method */\n method: TMethod;\n\n /** URL path with optional template variables */\n path: TPath;\n\n /** Request body schema - type will be inferred from Inferrable schemas */\n body?: InferSchema<TBodySchema>;\n\n /** Query parameters schema */\n query?: Schema;\n\n /** Headers schema */\n headers?: Schema;\n\n /** Response schemas by HTTP status code */\n responses: TResponses;\n\n /** REST-specific metadata */\n metadata?: RestMetadata;\n}\n\n/**\n * REST-specific metadata\n */\nexport interface RestMetadata {\n /** Endpoint description */\n description?: string;\n\n /** Tags for grouping */\n tags?: string[];\n\n /** Deprecation notice */\n deprecated?: boolean;\n\n /** Rate limit information */\n rateLimit?: {\n requests: number;\n window: string;\n };\n\n /** Cache control */\n cache?: {\n maxAge?: number;\n private?: boolean;\n };\n\n /** Any additional metadata */\n [key: string]: any;\n}\n\n/**\n * REST operation function type\n */\nexport type RestOperationFunction<\n TParams extends any[] = any[],\n TDescriptor extends RestEndpointDescriptor = RestEndpointDescriptor,\n> = (...params: TParams) => TDescriptor;\n\n/**\n * REST Contract - enforces that all properties are either:\n * - RestOperationFunction (endpoint)\n * - Nested RestContract (namespace)\n */\nexport type RestContract = {\n [key: string]: RestOperationFunction | RestContract;\n};\n\n/**\n * Extract response type for a specific HTTP status code\n * Automatically infers types from Inferrable schemas\n */\nexport type ExtractResponse<\n T extends RestEndpointDescriptor,\n Status extends keyof T['responses'],\n> = InferSchema<T['responses'][Status]>;\n\n/**\n * Infer the success response type (2xx status codes only)\n * Filters out error responses (4xx, 5xx) and only returns 2xx response types\n * Automatically infers types from Inferrable schemas\n */\nexport type InferSuccessResponse<T extends RestEndpointDescriptor> = Exclude<\n {\n [K in keyof T['responses']]: K extends\n | 200\n | 201\n | 202\n | 203\n | 204\n | 205\n | 206\n | 207\n | 208\n | 226\n ? InferSchema<T['responses'][K]>\n : never;\n }[keyof T['responses']],\n never | undefined\n>;\n\n/**\n * Infer the error response type (non-2xx status codes)\n * Captures all error responses (4xx, 5xx) as a union type\n */\nexport type InferErrorResponse<T extends RestEndpointDescriptor> = Exclude<\n {\n [K in keyof T['responses']]: K extends\n | 200\n | 201\n | 202\n | 203\n | 204\n | 205\n | 206\n | 207\n | 208\n | 226\n ? never\n : T['responses'][K];\n }[keyof T['responses']],\n never | undefined\n>;\n","/**\n * Speci REST - Helper functions for REST endpoints\n */\n\nimport type { Schema } from '../core/types';\nimport type {\n RestEndpointDescriptor,\n RestMetadata,\n ResponseMap,\n} from './types';\n\n/**\n * Options for REST endpoint helpers\n */\nexport interface RestEndpointOptions<\n TBodySchema = unknown,\n TResponses extends ResponseMap = ResponseMap,\n> {\n /** Request body schema - can be Inferrable schema or plain type */\n body?: TBodySchema;\n\n /** Query parameters schema */\n query?: Schema;\n\n /** Headers schema */\n headers?: Schema;\n\n /** Response schemas by status code (optional when using shortcut syntax) */\n responses?: TResponses;\n\n /** Optional REST metadata */\n metadata?: RestMetadata;\n}\n\n/**\n * HTTP helper object with optional global responses\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\ntype Http<TGlobalResponses extends ResponseMap = {}> = {\n // GET - with shortcut syntax\n get: {\n <TSuccess = unknown>(\n path: string,\n ): RestEndpointDescriptor<\n 'GET',\n string,\n never,\n { 200: TSuccess } & TGlobalResponses\n >;\n\n <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ): RestEndpointDescriptor<\n 'GET',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n };\n\n // POST - always use options (no shortcut)\n post: <\n TPath extends string,\n TBody = unknown,\n TResponses extends ResponseMap = ResponseMap,\n >(\n path: TPath,\n options: RestEndpointOptions<TBody, TResponses>,\n ) => RestEndpointDescriptor<\n 'POST',\n TPath,\n TBody,\n TResponses & TGlobalResponses\n >;\n\n // PUT - always use options (no shortcut)\n put: <\n TPath extends string,\n TBody = unknown,\n TResponses extends ResponseMap = ResponseMap,\n >(\n path: TPath,\n options: RestEndpointOptions<TBody, TResponses>,\n ) => RestEndpointDescriptor<\n 'PUT',\n TPath,\n TBody,\n TResponses & TGlobalResponses\n >;\n\n // PATCH - always use options (no shortcut)\n patch: <\n TPath extends string,\n TBody = unknown,\n TResponses extends ResponseMap = ResponseMap,\n >(\n path: TPath,\n options: RestEndpointOptions<TBody, TResponses>,\n ) => RestEndpointDescriptor<\n 'PATCH',\n TPath,\n TBody,\n TResponses & TGlobalResponses\n >;\n\n // DELETE - with shortcut syntax\n delete: {\n (\n path: string,\n ): RestEndpointDescriptor<\n 'DELETE',\n string,\n never,\n { 204: undefined } & TGlobalResponses\n >;\n\n <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ): RestEndpointDescriptor<\n 'DELETE',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n };\n\n head: <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ) => RestEndpointDescriptor<\n 'HEAD',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n\n options: <TPath extends string, TResponses extends ResponseMap>(\n path: TPath,\n options: Omit<RestEndpointOptions<never, TResponses>, 'body'>,\n ) => RestEndpointDescriptor<\n 'OPTIONS',\n TPath,\n never,\n TResponses & TGlobalResponses\n >;\n};\n\n/**\n * Create HTTP helpers for defining REST endpoints\n *\n * @param globalResponses - Optional: Global response map to merge with all endpoint responses\n *\n * @example\n * // Without global responses\n * const http = createHttp();\n * http.get('/users', {\n * responses: { 200: User[], 404: NotFoundError }\n * })\n *\n * @example\n * // With global error responses\n * const globalErrors = {\n * 400: {} as ApiError,\n * 401: {} as ApiError,\n * 500: {} as ApiError\n * };\n *\n * const api = createHttp(globalErrors);\n * api.get('/users', {\n * responses: { 200: [] as User[] }\n * // 400, 401, 500 are automatically added\n * })\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport function createHttp<TGlobalResponses extends ResponseMap = {}>(\n globalResponses?: TGlobalResponses,\n): Http<TGlobalResponses> {\n const mergeResponses = (responses: any) => {\n if (!globalResponses) return responses;\n return { ...globalResponses, ...responses };\n };\n\n return {\n get: (path: string, options?: any) => {\n // Shortcut: api.get<User[]>('/path')\n if (!options) {\n return {\n method: 'GET' as const,\n path,\n responses: mergeResponses({ 200: undefined }),\n };\n }\n // Full: api.get('/path', { responses: {...} })\n return {\n method: 'GET' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses || { 200: undefined }),\n };\n },\n post: (path, options) => {\n const { body, responses, ...rest } = options;\n return {\n method: 'POST' as const,\n path,\n body,\n ...rest,\n responses: mergeResponses(responses),\n };\n },\n\n put: (path, options) => {\n const { body, responses, ...rest } = options;\n return {\n method: 'PUT' as const,\n path,\n body,\n ...rest,\n responses: mergeResponses(responses),\n };\n },\n\n patch: (path, options) => {\n const { body, responses, ...rest } = options;\n return {\n method: 'PATCH' as const,\n path,\n body,\n ...rest,\n responses: mergeResponses(responses),\n };\n },\n delete: (path: string, options?: any) => {\n // Shortcut: api.delete('/path')\n if (!options) {\n return {\n method: 'DELETE' as const,\n path,\n responses: mergeResponses({ 204: undefined }),\n };\n }\n // Full: api.delete('/path', { responses: {...} })\n return {\n method: 'DELETE' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses || { 204: undefined }),\n };\n },\n head: (path: string, options: any) => ({\n method: 'HEAD' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses),\n }),\n options: (path: string, options: any) => ({\n method: 'OPTIONS' as const,\n path,\n ...options,\n responses: mergeResponses(options.responses),\n }),\n } as Http<TGlobalResponses>;\n}\n\n/**\n * Default HTTP helper object\n *\n * @example\n * import { http } from '@abapify/speci/rest'\n *\n * http.get('/users', {\n * responses: { 200: User[], 404: NotFoundError }\n * })\n *\n * http.delete('/users/123', {\n * responses: { 204: undefined, 404: NotFoundError }\n * })\n */\nexport const http = createHttp();\n","/**\n * Speci REST - Client Types\n *\n * REST-specific client interface types.\n */\n\nimport type {\n OperationFunction,\n ExtractParams,\n ExtractDescriptor,\n} from '../../core/types';\nimport type {\n RestEndpointDescriptor,\n InferSuccessResponse,\n InferErrorResponse,\n} from '../types';\n\n/**\n * HTTP error with typed payload\n */\nexport class HttpError<TPayload = unknown> extends Error {\n constructor(\n public readonly status: number,\n public readonly payload: TPayload,\n message?: string,\n ) {\n super(message || `HTTP ${status}`);\n this.name = 'HttpError';\n }\n}\n\n/**\n * HTTP request options\n */\nexport interface HttpRequestOptions {\n method: string;\n url: string;\n body?: unknown;\n query?: Record<string, any>;\n headers?: Record<string, string>;\n bodySchema?: unknown; // Schema for serializing body (passed when body is Inferrable)\n responses?: Record<number, unknown>; // Response schemas by status code\n}\n\n/**\n * HTTP adapter interface - consumer provides their own implementation\n *\n * @template TDefaultResponse - Default response type for all requests (useful for testing)\n */\nexport interface HttpAdapter<TDefaultResponse = unknown> {\n /**\n * Execute an HTTP request\n */\n request<TResponse = TDefaultResponse>(\n options?: HttpRequestOptions,\n ): Promise<TResponse>;\n}\n\n/**\n * Client configuration\n */\nexport interface ClientConfig {\n /** Base URL for all requests */\n baseUrl: string;\n\n /** HTTP adapter implementation */\n adapter: HttpAdapter;\n\n /** Default headers for all requests */\n headers?: Record<string, string>;\n\n /** Request interceptor */\n onRequest?: (options: any) => any | Promise<any>;\n\n /** Response interceptor */\n onResponse?: (response: any) => any | Promise<any>;\n\n /** Error interceptor */\n onError?: (error: any) => any | Promise<any>;\n}\n\n/**\n * Check if body is an Inferrable schema (has _infer property)\n */\ntype IsInferrableBody<TBody> = TBody extends { _infer?: any } ? true : false;\n\n/**\n * Check if method is PUT or PATCH (update operations that typically send partial data)\n */\ntype IsUpdateMethod<TMethod> = TMethod extends 'PUT' | 'PATCH' ? true : false;\n\n/**\n * Extract body type from a REST endpoint descriptor\n * Only extracts if the body is an Inferrable schema\n * For PUT/PATCH methods, returns Partial<T> since updates typically send partial data\n */\ntype ExtractBodyType<TDescriptor> =\n TDescriptor extends RestEndpointDescriptor<\n infer TMethod,\n any,\n infer TBody,\n any\n >\n ? IsInferrableBody<TBody> extends true\n ? TBody extends { _infer?: infer U }\n ? IsUpdateMethod<TMethod> extends true\n ? Partial<U> // PUT/PATCH: allow partial body\n : U // POST: require full body\n : never\n : never\n : never;\n\n/**\n * Build parameter list for a REST client method\n * - If descriptor has a body with Inferrable schema, append the inferred type as a parameter\n * - Otherwise, use the function's declared parameters\n */\ntype BuildParams<T extends OperationFunction> =\n ExtractDescriptor<T> extends RestEndpointDescriptor\n ? ExtractBodyType<ExtractDescriptor<T>> extends never\n ? ExtractParams<T> // No body - use declared params\n : ExtractParams<T> extends []\n ? [ExtractBodyType<ExtractDescriptor<T>>] // Empty params + body - add body param\n : [...ExtractParams<T>, ExtractBodyType<ExtractDescriptor<T>>] // Has params + body - append body\n : ExtractParams<T>;\n\n/**\n * Convert a REST operation function to a client method\n * Only returns success response types (2xx) - errors are thrown as HttpError\n * Includes typed error property for error response payloads\n *\n * Automatically infers parameter types from body schema if present\n */\nexport type RestClientMethod<T extends OperationFunction> = {\n (\n ...params: BuildParams<T>\n ): Promise<\n ExtractDescriptor<T> extends RestEndpointDescriptor\n ? InferSuccessResponse<ExtractDescriptor<T>>\n : never\n >;\n /** Typed error response payload - use with HttpError */\n error: ExtractDescriptor<T> extends RestEndpointDescriptor\n ? InferErrorResponse<ExtractDescriptor<T>>\n : never;\n /** Check if error is from this endpoint */\n isError(\n error: unknown,\n ): error is HttpError<\n ExtractDescriptor<T> extends RestEndpointDescriptor\n ? InferErrorResponse<ExtractDescriptor<T>>\n : never\n >;\n};\n\n/**\n * Convert a REST contract to a typed client.\n * Uses Record<string, any> instead of Contract to allow concrete types without index signature.\n */\nexport type RestClient<T extends Record<string, any>> = {\n [K in keyof T]: T[K] extends OperationFunction\n ? RestClientMethod<T[K]>\n : T[K] extends Record<string, any>\n ? RestClient<T[K]>\n : never;\n};\n","/**\n * Speci v0.1 - Client Generator\n *\n * Creates a typed client from a contract specification.\n */\n\nimport type { OperationFunction } from '../../core/types';\nimport type { RestClient, ClientConfig } from './types';\n\n/**\n * Extract path parameters from a path template\n * Example: \"/users/${id}/posts/${postId}\" -> [\"id\", \"postId\"]\n */\nfunction extractPathParams(path: string): string[] {\n const matches = path.matchAll(/\\$\\{(\\w+)\\}/g);\n return Array.from(matches, (m) => m[1]);\n}\n\n/**\n * Replace path parameters with actual values\n * Example: \"/users/${id}\" with {id: \"123\"} -> \"/users/123\"\n */\nfunction replacePath(path: string, params: Record<string, any>): string {\n return path.replaceAll(/\\$\\{(\\w+)\\}/g, (_, key) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: ${key}`);\n }\n return encodeURIComponent(String(value));\n });\n}\n\n/**\n * Check if a value is an Inferrable schema (has _infer property)\n */\nfunction isInferrableSchema(value: any): boolean {\n return value && typeof value === 'object' && '_infer' in value;\n}\n\n/**\n * Create a client method from an operation function\n */\nfunction createMethod(\n config: ClientConfig,\n operationFn: OperationFunction,\n): any {\n const method = async (...args: any[]) => {\n // Execute the operation function with the provided args\n // Contract functions receive params (object or individual), we pass them through\n const descriptor: any = operationFn(...args);\n\n // For REST endpoints, handle path parameters\n const pathParamNames = descriptor.path\n ? extractPathParams(descriptor.path)\n : [];\n\n // Build path params object\n // If first arg is an object with path param keys, use it; otherwise use positional args\n const pathParams: Record<string, any> = {};\n const firstArg = args[0];\n\n if (firstArg && typeof firstArg === 'object' && !Array.isArray(firstArg)) {\n // Object-based parameters - extract from first arg\n pathParamNames.forEach((name) => {\n if (firstArg[name] !== undefined) {\n pathParams[name] = firstArg[name];\n }\n });\n } else {\n // Positional parameters - use arg indices\n pathParamNames.forEach((name, index) => {\n if (args[index] !== undefined) {\n pathParams[name] = args[index];\n }\n });\n }\n\n // Handle body parameter\n // If descriptor.body is an Inferrable schema, body data comes after path params\n let bodyData = descriptor.body;\n let bodySchema = undefined;\n\n // Use operationFn.length to determine how many path params the function declared\n // This works because template strings consume the params before we see them\n const declaredParamCount = operationFn.length;\n\n if (isInferrableSchema(descriptor.body)) {\n // Body is a schema - body data comes after all declared parameters\n bodySchema = descriptor.body;\n // Body is at position: number of declared params (path params consumed by template)\n bodyData = args[declaredParamCount];\n }\n\n // Replace path parameters if path exists\n const url = descriptor.path\n ? config.baseUrl + replacePath(descriptor.path, pathParams)\n : config.baseUrl;\n\n // Prepare request options (REST-specific, but works generically)\n let requestOptions = {\n method: descriptor.method || 'GET',\n url,\n body: bodyData, // Actual data (not schema)\n bodySchema, // Schema for adapter to use\n query: descriptor.query,\n headers: {\n ...config.headers,\n ...descriptor.headers,\n },\n responses: descriptor.responses, // Pass responses for adapter\n };\n\n // Apply request interceptor\n if (config.onRequest) {\n requestOptions = await config.onRequest(requestOptions);\n }\n\n try {\n // Execute request\n let response = await config.adapter.request(requestOptions);\n\n // Apply response interceptor\n if (config.onResponse) {\n response = await config.onResponse(response);\n }\n\n return response;\n } catch (error) {\n // Apply error interceptor\n if (config.onError) {\n return await config.onError(error);\n }\n throw error;\n }\n };\n\n // Add error type property and isError method for type checking\n (method as any).error = undefined;\n (method as any).isError = (error: unknown): error is any => {\n return error instanceof Error && error.name === 'HttpError';\n };\n\n return method;\n}\n\n/**\n * Create a typed REST client from a contract\n */\nexport function createClient<T extends Record<string, any>>(\n contract: T,\n config: ClientConfig,\n): RestClient<T> {\n const client: any = {};\n\n for (const [key, value] of Object.entries(contract)) {\n if (typeof value === 'function') {\n // It's an operation function\n client[key] = createMethod(config, value as OperationFunction);\n } else if (typeof value === 'object' && value !== null) {\n // It's a nested contract\n client[key] = createClient(value as Record<string, any>, config);\n }\n }\n\n return client as RestClient<T>;\n}\n","/**\n * Speci v0.1 - Fetch Adapter\n *\n * Native fetch API adapter for Speci clients.\n */\n\nimport type { HttpAdapter } from './types';\nimport { HttpError } from './types';\n\n/**\n * Create a fetch-based HTTP adapter\n */\nexport function createFetchAdapter(options?: RequestInit): HttpAdapter {\n return {\n async request<TResponse = unknown>(opts: {\n method: string;\n url: string;\n body?: unknown;\n query?: Record<string, any>;\n headers?: Record<string, string>;\n }): Promise<TResponse> {\n const { method, url, body, query, headers } = opts;\n // Build query string\n const queryString = query\n ? '?' + new URLSearchParams(query).toString()\n : '';\n\n const fullUrl = url + queryString;\n\n // Execute fetch\n const response = await fetch(fullUrl, {\n ...options,\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...options?.headers,\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n\n // Check if response is successful (2xx)\n if (!response.ok) {\n // Parse error response\n const errorData = await response.json().catch(() => ({\n error: 'HTTP Error',\n message: response.statusText,\n }));\n throw new HttpError(response.status, errorData);\n }\n\n // Parse successful response\n const data = await response.json();\n return data as TResponse;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
//#region src/core/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Speci v0.1 - Core Types (Protocol-Agnostic)
|
|
4
|
+
*
|
|
5
|
+
* These types work across all protocols: REST, GraphQL, gRPC, WebSocket, etc.
|
|
6
|
+
* Protocol-specific types are in their respective modules.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Schema type - can be any type that describes data structure
|
|
10
|
+
* Works with: Zod, JSON Schema, TypeScript types, Protobuf, GraphQL types, etc.
|
|
11
|
+
*/
|
|
12
|
+
type Schema<T = any> = T;
|
|
13
|
+
/**
|
|
14
|
+
* Generic operation descriptor
|
|
15
|
+
* Protocol-specific descriptors extend this base interface
|
|
16
|
+
*/
|
|
17
|
+
interface OperationDescriptor {
|
|
18
|
+
/** Optional metadata */
|
|
19
|
+
metadata?: {
|
|
20
|
+
/** Operation description */description?: string; /** Tags for grouping */
|
|
21
|
+
tags?: string[]; /** Deprecation notice */
|
|
22
|
+
deprecated?: boolean; /** Any additional metadata */
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Arrow function that defines an operation contract
|
|
28
|
+
* Generic across all protocols
|
|
29
|
+
*/
|
|
30
|
+
type OperationFunction<TParams extends any[] = any[], TDescriptor extends OperationDescriptor = OperationDescriptor> = (...params: TParams) => TDescriptor;
|
|
31
|
+
/**
|
|
32
|
+
* Contract - a collection of operation functions
|
|
33
|
+
* Works for REST, GraphQL, gRPC, etc.
|
|
34
|
+
*/
|
|
35
|
+
type Contract = {
|
|
36
|
+
[key: string]: OperationFunction | Contract;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Extract parameter types from an operation function
|
|
40
|
+
*/
|
|
41
|
+
type ExtractParams<T> = T extends ((...args: infer P) => any) ? P : never;
|
|
42
|
+
/**
|
|
43
|
+
* Extract descriptor from an operation function
|
|
44
|
+
*/
|
|
45
|
+
type ExtractDescriptor<T> = T extends ((...args: any[]) => infer D) ? D : never;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/rest/types.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* HTTP methods for REST APIs
|
|
50
|
+
*/
|
|
51
|
+
type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
52
|
+
/**
|
|
53
|
+
* Inferrable schema interface (explicit _infer property)
|
|
54
|
+
*
|
|
55
|
+
* Schemas that implement this interface will have their types automatically inferred.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const UserSchema = {
|
|
59
|
+
* ...yourSchemaDefinition,
|
|
60
|
+
* _infer: undefined as unknown as User
|
|
61
|
+
* } as const;
|
|
62
|
+
*
|
|
63
|
+
* responses: { 200: UserSchema } // Type is User
|
|
64
|
+
*/
|
|
65
|
+
interface Inferrable<T = unknown> {
|
|
66
|
+
/** Type inference marker - never accessed at runtime */
|
|
67
|
+
_infer?: T;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Serializable schema interface (parse/build methods + Inferrable)
|
|
71
|
+
*
|
|
72
|
+
* Extends Inferrable so schemas with parse() automatically get _infer marker.
|
|
73
|
+
* This is the preferred pattern for schema libraries like ts-xsd.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const UserSchema = {
|
|
77
|
+
* parse: (raw: string): User => JSON.parse(raw),
|
|
78
|
+
* build: (data: User): string => JSON.stringify(data),
|
|
79
|
+
* };
|
|
80
|
+
*
|
|
81
|
+
* responses: { 200: UserSchema } // Type is User (inferred via _infer from Inferrable)
|
|
82
|
+
*/
|
|
83
|
+
interface Serializable<T = unknown> extends Inferrable<T> {
|
|
84
|
+
/** Parse raw string to typed object */
|
|
85
|
+
parse(raw: string): T;
|
|
86
|
+
/** Build typed object to string */
|
|
87
|
+
build?(data: T): string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create an Inferrable schema with automatic type inference
|
|
91
|
+
* Cleaner than the manual _infer pattern
|
|
92
|
+
*/
|
|
93
|
+
declare function createInferrable<T>(): Inferrable<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Infer type from a schema
|
|
96
|
+
*
|
|
97
|
+
* Uses _infer property from Inferrable<T> (which Serializable extends).
|
|
98
|
+
* Falls back to original type if no _infer property.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* type A = InferSchema<{ _infer?: User }> // User
|
|
102
|
+
* type B = InferSchema<string> // string (fallback)
|
|
103
|
+
*/
|
|
104
|
+
type InferSchema<T> = T extends {
|
|
105
|
+
_infer?: infer U;
|
|
106
|
+
} ? NonNullable<U> : T;
|
|
107
|
+
/**
|
|
108
|
+
* Schema-like object - can be any schema library (Zod, JSON Schema, custom, etc.)
|
|
109
|
+
* Adapters interpret these based on their capabilities.
|
|
110
|
+
*/
|
|
111
|
+
type SchemaLike = unknown;
|
|
112
|
+
/**
|
|
113
|
+
* Helper to wrap a schema with type information
|
|
114
|
+
* Use this if your schema doesn't implement Inferrable
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* responses: { 200: schema(UserSchema, {} as User) }
|
|
118
|
+
*/
|
|
119
|
+
declare function schema<T>(schemaObject: SchemaLike, _type: T): Inferrable<T>;
|
|
120
|
+
/**
|
|
121
|
+
* Response map - maps status codes to response types or schemas
|
|
122
|
+
*
|
|
123
|
+
* Values can be:
|
|
124
|
+
* - Type assertions: `undefined as unknown as MyType`
|
|
125
|
+
* - Schema objects: `MyZodSchema`, `MyElementSchema`, etc.
|
|
126
|
+
* - Schema with type: `MySchema as SchemaWithType<MyType>`
|
|
127
|
+
* - The adapter will interpret them appropriately
|
|
128
|
+
*/
|
|
129
|
+
type ResponseMap = Record<number, SchemaLike>;
|
|
130
|
+
/**
|
|
131
|
+
* REST endpoint descriptor
|
|
132
|
+
* Extends the protocol-agnostic OperationDescriptor with REST-specific fields
|
|
133
|
+
*/
|
|
134
|
+
interface RestEndpointDescriptor<TMethod extends RestMethod = RestMethod, TPath extends string = string, TBodySchema = unknown, TResponses extends ResponseMap = ResponseMap> extends OperationDescriptor {
|
|
135
|
+
/** HTTP method */
|
|
136
|
+
method: TMethod;
|
|
137
|
+
/** URL path with optional template variables */
|
|
138
|
+
path: TPath;
|
|
139
|
+
/** Request body schema - type will be inferred from Inferrable schemas */
|
|
140
|
+
body?: InferSchema<TBodySchema>;
|
|
141
|
+
/** Query parameters schema */
|
|
142
|
+
query?: Schema;
|
|
143
|
+
/** Headers schema */
|
|
144
|
+
headers?: Schema;
|
|
145
|
+
/** Response schemas by HTTP status code */
|
|
146
|
+
responses: TResponses;
|
|
147
|
+
/** REST-specific metadata */
|
|
148
|
+
metadata?: RestMetadata;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* REST-specific metadata
|
|
152
|
+
*/
|
|
153
|
+
interface RestMetadata {
|
|
154
|
+
/** Endpoint description */
|
|
155
|
+
description?: string;
|
|
156
|
+
/** Tags for grouping */
|
|
157
|
+
tags?: string[];
|
|
158
|
+
/** Deprecation notice */
|
|
159
|
+
deprecated?: boolean;
|
|
160
|
+
/** Rate limit information */
|
|
161
|
+
rateLimit?: {
|
|
162
|
+
requests: number;
|
|
163
|
+
window: string;
|
|
164
|
+
};
|
|
165
|
+
/** Cache control */
|
|
166
|
+
cache?: {
|
|
167
|
+
maxAge?: number;
|
|
168
|
+
private?: boolean;
|
|
169
|
+
};
|
|
170
|
+
/** Any additional metadata */
|
|
171
|
+
[key: string]: any;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* REST operation function type
|
|
175
|
+
*/
|
|
176
|
+
type RestOperationFunction<TParams extends any[] = any[], TDescriptor extends RestEndpointDescriptor = RestEndpointDescriptor> = (...params: TParams) => TDescriptor;
|
|
177
|
+
/**
|
|
178
|
+
* REST Contract - enforces that all properties are either:
|
|
179
|
+
* - RestOperationFunction (endpoint)
|
|
180
|
+
* - Nested RestContract (namespace)
|
|
181
|
+
*/
|
|
182
|
+
type RestContract = {
|
|
183
|
+
[key: string]: RestOperationFunction | RestContract;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Extract response type for a specific HTTP status code
|
|
187
|
+
* Automatically infers types from Inferrable schemas
|
|
188
|
+
*/
|
|
189
|
+
type ExtractResponse<T extends RestEndpointDescriptor, Status extends keyof T['responses']> = InferSchema<T['responses'][Status]>;
|
|
190
|
+
/**
|
|
191
|
+
* Infer the success response type (2xx status codes only)
|
|
192
|
+
* Filters out error responses (4xx, 5xx) and only returns 2xx response types
|
|
193
|
+
* Automatically infers types from Inferrable schemas
|
|
194
|
+
*/
|
|
195
|
+
type InferSuccessResponse<T extends RestEndpointDescriptor> = Exclude<{ [K in keyof T['responses']]: K extends 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 ? InferSchema<T['responses'][K]> : never }[keyof T['responses']], never | undefined>;
|
|
196
|
+
/**
|
|
197
|
+
* Infer the error response type (non-2xx status codes)
|
|
198
|
+
* Captures all error responses (4xx, 5xx) as a union type
|
|
199
|
+
*/
|
|
200
|
+
type InferErrorResponse<T extends RestEndpointDescriptor> = Exclude<{ [K in keyof T['responses']]: K extends 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 ? never : T['responses'][K] }[keyof T['responses']], never | undefined>;
|
|
201
|
+
//#endregion
|
|
202
|
+
export { ExtractDescriptor as _, Inferrable as a, OperationFunction as b, RestEndpointDescriptor as c, RestOperationFunction as d, SchemaLike as f, Contract as g, schema as h, InferSuccessResponse as i, RestMetadata as l, createInferrable as m, InferErrorResponse as n, ResponseMap as o, Serializable as p, InferSchema as r, RestContract as s, ExtractResponse as t, RestMethod as u, ExtractParams as v, Schema as x, OperationDescriptor as y };
|
|
203
|
+
//# sourceMappingURL=types-wn9lAKzN.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-wn9lAKzN.d.mts","names":[],"sources":["../src/core/types.ts","../src/rest/types.ts"],"mappings":";;AAWA;;;;;AAMA;;;;AAAA,KANY,MAAA,YAAkB,CAAA;;;;;UAMb,mBAAA;EAaD;EAXd,QAAA;IAmB2B,4BAjBzB,WAAA,WAmBkB;IAhBlB,IAAA,aAiBY;IAdZ,UAAA,YAcmC;IAAA,CAXlC,GAAA;EAAA;AAAA;;;;;KAQO,iBAAA,oDAEU,mBAAA,GAAsB,mBAAA,QACpC,MAAA,EAAQ,OAAA,KAAY,WAAA;;;AAM5B;;KAAY,QAAA;EAAA,CACT,GAAA,WAAc,iBAAA,GAAoB,QAAA;AAAA;;;;KAMzB,aAAA,MAAmB,CAAA,cAAc,IAAA,qBAAwB,CAAA;AAArE;;;AAAA,KAKY,iBAAA,MAAuB,CAAA,cAAc,IAAA,uBAC7C,CAAA;;;;;AA3CJ;KCRY,UAAA;;;;;;;;;;AD6BZ;;;;UCPiB,UAAA;EDUD;ECRd,MAAA,GAAS,CAAA;AAAA;;;;;;;;;;;ADcX;;;;UCGiB,YAAA,sBAAkC,UAAA,CAAW,CAAA;EDF7C;ECIf,KAAA,CAAM,GAAA,WAAc,CAAA;EDJuB;ECM3C,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA;;;;;iBAOC,gBAAA,GAAA,CAAA,GAAuB,UAAA,CAAW,CAAA;;;;;;ADFlD;;;;;KCgBY,WAAA,MAAiB,CAAA;EAAY,MAAA;AAAA,IACrC,WAAA,CAAY,CAAA,IACZ,CAAA;;;;;KAMQ,UAAA;;AA1EZ;;;;;AAsBA;iBA6DgB,MAAA,GAAA,CAAU,YAAA,EAAc,UAAA,EAAY,KAAA,EAAO,CAAA,GAAI,UAAA,CAAW,CAAA;;;;;;;;AA1C1E;;KAuDY,WAAA,GAAc,MAAA,SAAe,UAAA;;;;;UAMxB,sBAAA,iBACC,UAAA,GAAa,UAAA,2EAGV,WAAA,GAAc,WAAA,UACzB,mBAAA;EAlEmD;EAoE3D,MAAA,EAAQ,OAAA;EApEyC;EAuEjD,IAAA,EAAM,KAAA;EArEN;EAwEA,IAAA,GAAO,WAAA,CAAY,WAAA;EAxEC;EA2EpB,KAAA,GAAQ,MAAA;EAzEK;EA4Eb,OAAA,GAAU,MAAA;EA5EI;EA+Ed,SAAA,EAAW,UAAA;EAxEG;EA2Ed,QAAA,GAAW,YAAA;AAAA;;;;UAMI,YAAA;EAjFkC;EAmFjD,WAAA;EArEU;EAwEV,IAAA;EAxEqB;EA2ErB,UAAA;EA1Ec;EA6Ed,SAAA;IACE,QAAA;IACA,MAAA;EAAA;EAhFoB;EAoFtB,KAAA;IACE,MAAA;IACA,OAAA;EAAA;EArFY;EAAA,CAyFb,GAAA;AAAA;;AAlFH;;KAwFY,qBAAA,oDAEU,sBAAA,GAAyB,sBAAA,QACvC,MAAA,EAAQ,OAAA,KAAY,WAAA;;;AAlF5B;;;KAyFY,YAAA;EAAA,CACT,GAAA,WAAc,qBAAA,GAAwB,YAAA;AAAA;;;;;KAO7B,eAAA,WACA,sBAAA,uBACW,CAAA,iBACnB,WAAA,CAAY,CAAA,cAAe,MAAA;;;;;;KAOnB,oBAAA,WAA+B,sBAAA,IAA0B,OAAA,eAErD,CAAA,gBAAiB,CAAA,qEAWzB,WAAA,CAAY,CAAA,cAAe,CAAA,mBAEzB,CAAA;;;;;KAQE,kBAAA,WAA6B,sBAAA,IAA0B,OAAA,eAEnD,CAAA,gBAAiB,CAAA,6EAYzB,CAAA,cAAe,CAAA,UACb,CAAA"}
|