schemantic 0.3.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/CHANGELOG.md +204 -174
- package/LICENSE +21 -21
- package/README.md +88 -830
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +185 -61
- package/dist/cli/index.js.map +1 -1
- package/dist/core/configuration.d.ts +6 -0
- package/dist/core/configuration.d.ts.map +1 -0
- package/dist/core/configuration.js +64 -0
- package/dist/core/configuration.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/output-writer.d.ts +7 -0
- package/dist/core/output-writer.d.ts.map +1 -0
- package/dist/core/output-writer.js +255 -0
- package/dist/core/output-writer.js.map +1 -0
- package/dist/core/schemantic.d.ts +8 -2
- package/dist/core/schemantic.d.ts.map +1 -1
- package/dist/core/schemantic.js +196 -71
- package/dist/core/schemantic.js.map +1 -1
- package/dist/generators/api-client-generator.d.ts +10 -20
- package/dist/generators/api-client-generator.d.ts.map +1 -1
- package/dist/generators/api-client-generator.js +374 -424
- package/dist/generators/api-client-generator.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +7 -25
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/enum-generator.d.ts.map +1 -1
- package/dist/generators/enum-generator.js +30 -3
- package/dist/generators/enum-generator.js.map +1 -1
- package/dist/generators/hook-generator.d.ts +3 -17
- package/dist/generators/hook-generator.d.ts.map +1 -1
- package/dist/generators/hook-generator.js +51 -445
- package/dist/generators/hook-generator.js.map +1 -1
- package/dist/generators/naming.d.ts +10 -0
- package/dist/generators/naming.d.ts.map +1 -0
- package/dist/generators/naming.js +331 -0
- package/dist/generators/naming.js.map +1 -0
- package/dist/generators/object-generator.d.ts +0 -4
- package/dist/generators/object-generator.d.ts.map +1 -1
- package/dist/generators/object-generator.js +13 -101
- package/dist/generators/object-generator.js.map +1 -1
- package/dist/generators/primitive-generator.d.ts +1 -13
- package/dist/generators/primitive-generator.d.ts.map +1 -1
- package/dist/generators/primitive-generator.js +11 -76
- package/dist/generators/primitive-generator.js.map +1 -1
- package/dist/generators/projections.d.ts +10 -0
- package/dist/generators/projections.d.ts.map +1 -0
- package/dist/generators/projections.js +65 -0
- package/dist/generators/projections.js.map +1 -0
- package/dist/generators/schema-type.d.ts +23 -0
- package/dist/generators/schema-type.d.ts.map +1 -0
- package/dist/generators/schema-type.js +233 -0
- package/dist/generators/schema-type.js.map +1 -0
- package/dist/parsers/base.d.ts +2 -0
- package/dist/parsers/base.d.ts.map +1 -1
- package/dist/parsers/base.js +12 -2
- package/dist/parsers/base.js.map +1 -1
- package/dist/parsers/factory.d.ts.map +1 -1
- package/dist/parsers/factory.js +2 -1
- package/dist/parsers/factory.js.map +1 -1
- package/dist/parsers/openapi.d.ts +4 -0
- package/dist/parsers/openapi.d.ts.map +1 -1
- package/dist/parsers/openapi.js +107 -20
- package/dist/parsers/openapi.js.map +1 -1
- package/dist/plugins/branded-types.d.ts.map +1 -1
- package/dist/plugins/branded-types.js +231 -365
- package/dist/plugins/branded-types.js.map +1 -1
- package/dist/plugins/builtin.d.ts.map +1 -1
- package/dist/plugins/builtin.js +6 -141
- package/dist/plugins/builtin.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +17 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/loader.d.ts +5 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +42 -5
- package/dist/plugins/loader.js.map +1 -1
- package/dist/plugins/manager.d.ts.map +1 -1
- package/dist/plugins/manager.js +7 -7
- package/dist/plugins/manager.js.map +1 -1
- package/dist/plugins/performance-monitoring.d.ts +31 -1
- package/dist/plugins/performance-monitoring.d.ts.map +1 -1
- package/dist/plugins/performance-monitoring.js +146 -207
- package/dist/plugins/performance-monitoring.js.map +1 -1
- package/dist/plugins/request-deduplication.d.ts.map +1 -1
- package/dist/plugins/request-deduplication.js +151 -586
- package/dist/plugins/request-deduplication.js.map +1 -1
- package/dist/plugins/zod-validation.d.ts +23 -15
- package/dist/plugins/zod-validation.d.ts.map +1 -1
- package/dist/plugins/zod-validation.js +392 -268
- package/dist/plugins/zod-validation.js.map +1 -1
- package/dist/types/core.d.ts +20 -3
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js +2 -0
- package/dist/types/core.js.map +1 -1
- package/dist/types/openapi.d.ts +14 -1
- package/dist/types/openapi.d.ts.map +1 -1
- package/docs/ADVANCED_PLUGINS_IMPLEMENTATION.md +24 -355
- package/docs/ADVANCED_PLUGIN_DEVELOPMENT.md +33 -309
- package/docs/API.md +80 -601
- package/docs/AUDIT.md +49 -0
- package/docs/CONFIGURATION.md +81 -715
- package/docs/FASTAPI_TUTORIAL.md +109 -1497
- package/docs/NAMING_MIGRATION.md +59 -0
- package/docs/PLUGINS.md +49 -687
- package/docs/PLUGIN_DEVELOPMENT.md +53 -720
- package/docs/REAUDIT.md +399 -0
- package/docs/RELEASING.md +88 -0
- package/docs/REMEDIATION.md +58 -0
- package/docs/superpowers/plans/2026-10-10-audit-remediation.md +113 -0
- package/package.json +114 -95
- package/dist/__tests__/setup.d.ts +0 -6
- package/dist/__tests__/setup.d.ts.map +0 -1
- package/dist/__tests__/setup.js +0 -30
- package/dist/__tests__/setup.js.map +0 -1
- package/dist/__tests__/test-config.d.ts +0 -36
- package/dist/__tests__/test-config.d.ts.map +0 -1
- package/dist/__tests__/test-config.js +0 -562
- package/dist/__tests__/test-config.js.map +0 -1
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.ApiClientGenerator = void 0;
|
|
8
|
+
const schema_type_1 = require("./schema-type");
|
|
9
|
+
const parsers_1 = require("../parsers");
|
|
8
10
|
// Removed unused TypeGeneratorFactory import
|
|
9
11
|
/**
|
|
10
12
|
* API client generator implementation
|
|
11
13
|
*/
|
|
12
14
|
class ApiClientGenerator {
|
|
13
15
|
constructor(context) {
|
|
16
|
+
this.referencedTypes = new Set();
|
|
17
|
+
this.typeAliases = new Map();
|
|
18
|
+
this.clientName = '';
|
|
19
|
+
this.usesSerialization = false;
|
|
20
|
+
this.usesDeserialization = false;
|
|
14
21
|
// Store context for access to config (namingConvention, etc.)
|
|
15
22
|
this.context = context;
|
|
16
23
|
}
|
|
@@ -18,8 +25,14 @@ class ApiClientGenerator {
|
|
|
18
25
|
* Generate API client from OpenAPI schema
|
|
19
26
|
*/
|
|
20
27
|
generate(context) {
|
|
28
|
+
this.context = context;
|
|
29
|
+
this.referencedTypes.clear();
|
|
30
|
+
this.typeAliases.clear();
|
|
31
|
+
this.usesSerialization = false;
|
|
32
|
+
this.usesDeserialization = false;
|
|
21
33
|
const schema = context.schema;
|
|
22
34
|
const clientName = this.getClientName(schema);
|
|
35
|
+
this.clientName = clientName;
|
|
23
36
|
// Extract endpoints from paths
|
|
24
37
|
const endpoints = this.extractEndpoints(schema.paths, context);
|
|
25
38
|
// Generate client class
|
|
@@ -39,12 +52,36 @@ class ApiClientGenerator {
|
|
|
39
52
|
/**
|
|
40
53
|
* Extract endpoints from OpenAPI paths
|
|
41
54
|
*/
|
|
55
|
+
resolveComponent(value) {
|
|
56
|
+
const visited = new Set();
|
|
57
|
+
while (value && typeof value === 'object' && '$ref' in value) {
|
|
58
|
+
const ref = value.$ref;
|
|
59
|
+
if (visited.has(ref))
|
|
60
|
+
throw new Error(`Circular component reference: ${ref}`);
|
|
61
|
+
visited.add(ref);
|
|
62
|
+
const resolved = this.context.schemaResolver(ref);
|
|
63
|
+
if (!resolved)
|
|
64
|
+
throw new Error(`Unresolved component reference: ${ref}`);
|
|
65
|
+
value = resolved;
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
mergeParameters(inherited, own) {
|
|
70
|
+
const parameters = new Map();
|
|
71
|
+
for (const value of [...inherited, ...own]) {
|
|
72
|
+
const parameter = this.resolveComponent(value);
|
|
73
|
+
parameters.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
74
|
+
}
|
|
75
|
+
return [...parameters.values()];
|
|
76
|
+
}
|
|
42
77
|
extractEndpoints(paths, context) {
|
|
43
78
|
const endpoints = [];
|
|
44
|
-
|
|
45
|
-
|
|
79
|
+
const usedMethodNames = new Set(['request', 'buildPath', 'appendQuery', 'simpleHeader', 'toFormData', 'toUrlEncoded', 'setDefaultHeader', 'removeDefaultHeader', 'setAuthToken', 'clearAuthToken', 'baseUrl', 'config', 'constructor']);
|
|
80
|
+
for (const [path, rawPathItem] of Object.entries(paths)) {
|
|
81
|
+
if (!rawPathItem || typeof rawPathItem !== "object") {
|
|
46
82
|
continue;
|
|
47
83
|
}
|
|
84
|
+
const pathItem = this.resolveComponent(rawPathItem);
|
|
48
85
|
// Extract operations from path item
|
|
49
86
|
const operations = [
|
|
50
87
|
"get",
|
|
@@ -59,8 +96,13 @@ class ApiClientGenerator {
|
|
|
59
96
|
for (const method of operations) {
|
|
60
97
|
const operation = pathItem[method];
|
|
61
98
|
if (operation) {
|
|
62
|
-
const endpoint = this.extractEndpoint(path, method.toUpperCase(), operation, context);
|
|
99
|
+
const endpoint = this.extractEndpoint(path, method.toUpperCase(), { ...operation, parameters: this.mergeParameters(pathItem.parameters || [], operation.parameters || []) }, context);
|
|
63
100
|
if (endpoint) {
|
|
101
|
+
const baseName = endpoint.functionName;
|
|
102
|
+
let suffix = 2;
|
|
103
|
+
while (usedMethodNames.has(endpoint.functionName))
|
|
104
|
+
endpoint.functionName = `${baseName}${suffix++}`;
|
|
105
|
+
usedMethodNames.add(endpoint.functionName);
|
|
64
106
|
endpoints.push(endpoint);
|
|
65
107
|
}
|
|
66
108
|
}
|
|
@@ -81,6 +123,48 @@ class ApiClientGenerator {
|
|
|
81
123
|
const requestBody = this.extractRequestBody(operation.requestBody, context);
|
|
82
124
|
// Extract responses
|
|
83
125
|
const responses = this.extractResponses(operation.responses, context);
|
|
126
|
+
// Keep original Schema Objects for boundary conversion; declarations use application names.
|
|
127
|
+
if (context.wireSchema) {
|
|
128
|
+
const wireResolver = parsers_1.ParserFactory.autoCreateParser({ data: context.wireSchema }, context.config).createResolver(context.wireSchema);
|
|
129
|
+
const directional = (value, direction, binary = false) => (0, schema_type_1.projectSchema)(value, { direction, binary }, wireResolver, ref => {
|
|
130
|
+
if (!/^#\/components\/schemas\/[^/]+$/.test(ref))
|
|
131
|
+
return ref;
|
|
132
|
+
const projection = context.projections?.[(0, schema_type_1.referenceName)(ref)];
|
|
133
|
+
const target = binary ? projection?.binary || projection?.request : projection?.[direction];
|
|
134
|
+
return target ? '#/components/schemas/' + encodeURIComponent(target.replace(/~/g, '~0').replace(/\//g, '~1')) : ref;
|
|
135
|
+
});
|
|
136
|
+
const resolve = (input) => {
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
while (input && typeof input === 'object' && '$ref' in input) {
|
|
139
|
+
const ref = input.$ref;
|
|
140
|
+
if (seen.has(ref))
|
|
141
|
+
throw new Error(`Circular component reference: ${ref}`);
|
|
142
|
+
seen.add(ref);
|
|
143
|
+
input = wireResolver(ref);
|
|
144
|
+
}
|
|
145
|
+
return input;
|
|
146
|
+
};
|
|
147
|
+
const item = resolve(context.wireSchema.paths[path]);
|
|
148
|
+
const wireOperation = item[method.toLowerCase()];
|
|
149
|
+
const wireParameters = [...(item.parameters || []), ...(wireOperation.parameters || [])].map(value => resolve(value));
|
|
150
|
+
for (const parameter of parameters) {
|
|
151
|
+
const wire = wireParameters.filter(value => value.name === parameter.name && value.in === parameter.location).pop();
|
|
152
|
+
if (wire?.schema)
|
|
153
|
+
parameter.schema = directional(wire.schema, 'request');
|
|
154
|
+
}
|
|
155
|
+
if (requestBody && wireOperation.requestBody) {
|
|
156
|
+
const body = resolve(wireOperation.requestBody);
|
|
157
|
+
const bodySchema = body.content[requestBody.contentType]?.schema;
|
|
158
|
+
if (bodySchema)
|
|
159
|
+
requestBody.schema = directional(bodySchema, 'request', this.mediaMode(requestBody.contentType) === 'blob');
|
|
160
|
+
}
|
|
161
|
+
for (const response of responses) {
|
|
162
|
+
const wire = resolve(wireOperation.responses[response.statusCode]);
|
|
163
|
+
const responseSchema = wire.content?.[response.contentType]?.schema;
|
|
164
|
+
if (responseSchema)
|
|
165
|
+
response.schema = directional(responseSchema, 'response');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
84
168
|
// Determine return type
|
|
85
169
|
const returnType = this.determineReturnType(responses);
|
|
86
170
|
const endpoint = {
|
|
@@ -102,19 +186,45 @@ class ApiClientGenerator {
|
|
|
102
186
|
*/
|
|
103
187
|
extractParameters(parameters, context) {
|
|
104
188
|
const validParameters = [];
|
|
105
|
-
|
|
189
|
+
const usedNames = new Set(['body', 'options', 'url', 'headers', 'requestOptions', 'cookies', 'queryValue', 'serializeValue', 'deserializeValue', 'URL', 'Headers', 'FormData', 'JSON', 'String', 'Array', 'Blob', 'File']);
|
|
190
|
+
for (const parameter of parameters) {
|
|
191
|
+
const param = this.resolveComponent(parameter);
|
|
106
192
|
if (typeof param !== "object" || param === null) {
|
|
107
193
|
continue;
|
|
108
194
|
}
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
195
|
+
const supportedStyles = { query: ['form', 'deepObject', 'spaceDelimited', 'pipeDelimited'], path: ['simple'], header: ['simple'], cookie: ['form'] };
|
|
196
|
+
if (param.style && !supportedStyles[param.in]?.includes(param.style))
|
|
197
|
+
throw new Error(`Unsupported ${param.in} parameter style ${param.style}: ${param.name}`);
|
|
198
|
+
if (param.content)
|
|
199
|
+
throw new Error(`Content-encoded parameter ${param.name} is unsupported; use schema/style/explode parameters`);
|
|
113
200
|
const type = this.extractParameterType(param.schema, context);
|
|
114
|
-
const
|
|
201
|
+
const complex = (value, seen = new Set()) => {
|
|
202
|
+
if ('$ref' in value && typeof value.$ref === 'string') {
|
|
203
|
+
if (seen.has(value.$ref))
|
|
204
|
+
return false;
|
|
205
|
+
const target = context.schemaResolver(value.$ref);
|
|
206
|
+
return target ? complex(target, new Set([...seen, value.$ref])) : false;
|
|
207
|
+
}
|
|
208
|
+
const object = value;
|
|
209
|
+
return object.type === 'object' || object.type === 'array' || Array.isArray(object.type) && object.type.some(type => type === 'object' || type === 'array')
|
|
210
|
+
|| !!object.properties || object.additionalProperties !== undefined || [...(object.oneOf || []), ...(object.anyOf || []), ...(object.allOf || [])].some(branch => complex(branch, seen));
|
|
211
|
+
};
|
|
212
|
+
if (param.in === 'cookie' && param.schema && complex(param.schema))
|
|
213
|
+
throw new Error(`Complex cookie parameter ${param.name} is unsupported; use scalar cookies or explicit request headers`);
|
|
214
|
+
const isRequired = param.in === 'path' || !!param.required;
|
|
215
|
+
const baseName = this.convertToFunctionName(param.name);
|
|
216
|
+
let variableName = baseName;
|
|
217
|
+
let suffix = 2;
|
|
218
|
+
while (usedNames.has(variableName))
|
|
219
|
+
variableName = `${baseName}${suffix++}`;
|
|
220
|
+
usedNames.add(variableName);
|
|
115
221
|
validParameters.push({
|
|
116
222
|
name: param.name,
|
|
223
|
+
variableName,
|
|
224
|
+
...(param.style !== undefined && { style: param.style }),
|
|
225
|
+
...(param.explode !== undefined && { explode: param.explode }),
|
|
117
226
|
type,
|
|
227
|
+
...(param.schema && { schema: param.schema }),
|
|
118
228
|
isOptional: !isRequired,
|
|
119
229
|
isRequired,
|
|
120
230
|
location: param.in,
|
|
@@ -130,29 +240,29 @@ class ApiClientGenerator {
|
|
|
130
240
|
if (!requestBody || typeof requestBody !== "object") {
|
|
131
241
|
return undefined;
|
|
132
242
|
}
|
|
133
|
-
//
|
|
243
|
+
// Resolve the Request Body Object, then inspect its content and required flag.
|
|
134
244
|
if ("$ref" in requestBody) {
|
|
135
|
-
|
|
136
|
-
if (resolvedSchema) {
|
|
137
|
-
const type = this.extractSchemaType(resolvedSchema, context);
|
|
138
|
-
return {
|
|
139
|
-
type,
|
|
140
|
-
isOptional: true, // Refs don't have required field directly
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
return undefined;
|
|
245
|
+
return this.extractRequestBody(this.resolveComponent(requestBody), context);
|
|
144
246
|
}
|
|
145
247
|
// Handle inline request body
|
|
146
248
|
if ("content" in requestBody && requestBody.content) {
|
|
147
|
-
const contentType = Object.keys(requestBody.content)[0];
|
|
249
|
+
const contentType = requestBody.content['application/json'] ? 'application/json' : Object.keys(requestBody.content)[0];
|
|
148
250
|
if (!contentType)
|
|
149
251
|
return undefined;
|
|
150
252
|
const mediaType = requestBody.content[contentType];
|
|
151
|
-
if (mediaType && mediaType.
|
|
152
|
-
|
|
253
|
+
if (mediaType?.encoding && Object.keys(mediaType.encoding).length)
|
|
254
|
+
throw new Error(`Per-property request encoding is unsupported for ${contentType}; supply encoded request data through an explicit transport adapter`);
|
|
255
|
+
if (mediaType) {
|
|
256
|
+
const binary = this.mediaMode(contentType) === 'blob';
|
|
257
|
+
if (contentType.includes('*'))
|
|
258
|
+
throw new Error(`Wildcard request media ${contentType} is unsupported; declare a concrete request media type`);
|
|
259
|
+
const type = mediaType.schema ? this.extractSchemaType(mediaType.schema, context, 'request', binary)
|
|
260
|
+
: this.mediaMode(contentType) === 'json' ? 'unknown' : this.mediaMode(contentType) === 'text' && contentType !== 'application/x-www-form-urlencoded' ? 'string'
|
|
261
|
+
: /^(?:multipart\/form-data|application\/x-www-form-urlencoded)$/i.test(contentType) ? 'Record<string, unknown>' : "NonNullable<RequestInit['body']>";
|
|
153
262
|
const body = {
|
|
154
263
|
type,
|
|
155
264
|
isOptional: !requestBody.required,
|
|
265
|
+
...(mediaType.schema && { schema: mediaType.schema }),
|
|
156
266
|
...(requestBody.description && {
|
|
157
267
|
description: requestBody.description,
|
|
158
268
|
}),
|
|
@@ -174,26 +284,11 @@ class ApiClientGenerator {
|
|
|
174
284
|
if (typeof response !== "object" || response === null) {
|
|
175
285
|
continue;
|
|
176
286
|
}
|
|
177
|
-
const responseObj = response;
|
|
178
|
-
// Handle reference
|
|
179
|
-
if ("$ref" in responseObj) {
|
|
180
|
-
const resolvedSchema = context.schemaResolver(responseObj.$ref);
|
|
181
|
-
if (resolvedSchema) {
|
|
182
|
-
const type = this.extractSchemaType(resolvedSchema, context);
|
|
183
|
-
responseList.push({
|
|
184
|
-
statusCode,
|
|
185
|
-
type,
|
|
186
|
-
isOptional: false,
|
|
187
|
-
description: ("description" in resolvedSchema && resolvedSchema.description) ||
|
|
188
|
-
"",
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
287
|
+
const responseObj = this.resolveComponent(response);
|
|
193
288
|
// Handle inline response
|
|
194
289
|
if ("content" in responseObj && responseObj.content) {
|
|
195
|
-
const
|
|
196
|
-
if (!
|
|
290
|
+
const media = Object.entries(responseObj.content);
|
|
291
|
+
if (!media.length) {
|
|
197
292
|
// Response without content type
|
|
198
293
|
responseList.push({
|
|
199
294
|
statusCode,
|
|
@@ -203,14 +298,16 @@ class ApiClientGenerator {
|
|
|
203
298
|
});
|
|
204
299
|
continue;
|
|
205
300
|
}
|
|
206
|
-
const mediaType
|
|
207
|
-
|
|
208
|
-
const type = this.extractSchemaType(mediaType.schema, context);
|
|
301
|
+
for (const [contentType, mediaType] of media) {
|
|
302
|
+
const mode = this.mediaMode(contentType);
|
|
303
|
+
const type = contentType.includes('*') ? 'unknown' : mode === 'text' ? 'string' : mode === 'blob' ? 'globalThis.Blob' : mediaType.schema ? this.extractSchemaType(mediaType.schema, context, 'response', false, this.isReturnStatus(statusCode)) : 'unknown';
|
|
209
304
|
responseList.push({
|
|
210
305
|
statusCode,
|
|
211
306
|
type,
|
|
307
|
+
contentType,
|
|
212
308
|
isOptional: false,
|
|
213
309
|
description: responseObj.description || "",
|
|
310
|
+
...(mediaType.schema && { schema: mediaType.schema }),
|
|
214
311
|
});
|
|
215
312
|
}
|
|
216
313
|
}
|
|
@@ -226,10 +323,15 @@ class ApiClientGenerator {
|
|
|
226
323
|
}
|
|
227
324
|
return responseList;
|
|
228
325
|
}
|
|
326
|
+
isReturnStatus(statusCode) {
|
|
327
|
+
return /^2(?:\d{2}|XX)$/i.test(statusCode) || statusCode === 'default';
|
|
328
|
+
}
|
|
229
329
|
/**
|
|
230
330
|
* Determine return type from responses
|
|
231
331
|
*/
|
|
232
332
|
determineReturnType(responses) {
|
|
333
|
+
// Non-success responses are thrown as ApiClientError by the runtime.
|
|
334
|
+
responses = responses.filter(response => this.isReturnStatus(response.statusCode));
|
|
233
335
|
if (responses.length === 0) {
|
|
234
336
|
return "void";
|
|
235
337
|
}
|
|
@@ -237,7 +339,7 @@ class ApiClientGenerator {
|
|
|
237
339
|
return responses[0].type;
|
|
238
340
|
}
|
|
239
341
|
// Multiple responses - create union type
|
|
240
|
-
const types = responses.map((r) => r.type);
|
|
342
|
+
const types = [...new Set(responses.map((r) => r.type))];
|
|
241
343
|
return types.join(" | ");
|
|
242
344
|
}
|
|
243
345
|
/**
|
|
@@ -247,65 +349,37 @@ class ApiClientGenerator {
|
|
|
247
349
|
if (!schema) {
|
|
248
350
|
return "unknown";
|
|
249
351
|
}
|
|
250
|
-
return this.extractSchemaType(schema, context);
|
|
352
|
+
return this.extractSchemaType(schema, context, 'request');
|
|
251
353
|
}
|
|
252
354
|
/**
|
|
253
355
|
* Extract schema type
|
|
254
356
|
*/
|
|
255
|
-
extractSchemaType(schema, context) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
357
|
+
extractSchemaType(schema, context, direction, binary = false, importReferences = true) {
|
|
358
|
+
return (0, schema_type_1.schemaType)(schema, ref => {
|
|
359
|
+
const original = (0, schema_type_1.referenceName)(ref);
|
|
360
|
+
const projection = context.projections?.[original];
|
|
361
|
+
const target = binary ? projection?.binary || projection?.request || original : direction ? projection?.[direction] || original : original;
|
|
362
|
+
const name = (0, schema_type_1.typeName)(target, context.config.typePrefix, context.config.typeSuffix);
|
|
363
|
+
// Error response metadata is retained, but only emitted signatures need type imports.
|
|
364
|
+
if (importReferences)
|
|
365
|
+
this.referencedTypes.add(name);
|
|
366
|
+
const globals = new Set(['Blob', 'File', 'FormData', 'Headers', 'URL', 'URLSearchParams', 'Response', 'RequestInit', 'BodyInit', 'AbortController', 'Error', 'ApiClientConfig', 'ApiClientError', this.clientName]);
|
|
367
|
+
let alias = this.typeAliases.get(name) || name;
|
|
368
|
+
if (globals.has(name) && !this.typeAliases.has(name)) {
|
|
369
|
+
const occupied = new Set([...Object.keys(context.schema.components?.schemas || {}).map(value => (0, schema_type_1.typeName)(value, context.config.typePrefix, context.config.typeSuffix)), ...this.typeAliases.values()]);
|
|
370
|
+
const base = 'Schema' + name;
|
|
371
|
+
alias = base;
|
|
372
|
+
let index = 2;
|
|
373
|
+
while (occupied.has(alias))
|
|
374
|
+
alias = base + index++;
|
|
262
375
|
}
|
|
263
|
-
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
? this.extractSchemaType(items, context)
|
|
271
|
-
: "unknown";
|
|
272
|
-
return `${itemType}[]`;
|
|
273
|
-
}
|
|
274
|
-
// Handle objects with properties or additionalProperties
|
|
275
|
-
if ("type" in schema && schema.type === "object") {
|
|
276
|
-
const hasProps = !!schema.properties && Object.keys(schema.properties).length > 0;
|
|
277
|
-
if (hasProps) {
|
|
278
|
-
// Prefer named types if nested refs exist; otherwise inline object type
|
|
279
|
-
const lines = [];
|
|
280
|
-
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
281
|
-
const t = this.extractSchemaType(propSchema, context);
|
|
282
|
-
lines.push(`${propName}: ${t}`);
|
|
283
|
-
}
|
|
284
|
-
return `{ ${lines.join("; ")} }`;
|
|
285
|
-
}
|
|
286
|
-
if (schema.additionalProperties !== undefined) {
|
|
287
|
-
if (schema.additionalProperties === true)
|
|
288
|
-
return "Record<string, unknown>";
|
|
289
|
-
if (typeof schema.additionalProperties === "object") {
|
|
290
|
-
const vt = this.extractSchemaType(schema.additionalProperties, context);
|
|
291
|
-
return `Record<string, ${vt}>`;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return "Record<string, unknown>";
|
|
295
|
-
}
|
|
296
|
-
// Handle basic types
|
|
297
|
-
if ("type" in schema && schema.type) {
|
|
298
|
-
return this.mapSchemaTypeToTypeScript(schema.type, schema.format);
|
|
299
|
-
}
|
|
300
|
-
// Handle enums
|
|
301
|
-
if ("enum" in schema && schema.enum) {
|
|
302
|
-
return schema.enum.map((val) => JSON.stringify(val)).join(" | ");
|
|
303
|
-
}
|
|
304
|
-
// Handle const
|
|
305
|
-
if ("const" in schema && schema.const !== undefined) {
|
|
306
|
-
return JSON.stringify(schema.const);
|
|
307
|
-
}
|
|
308
|
-
return "unknown";
|
|
376
|
+
this.typeAliases.set(name, alias);
|
|
377
|
+
return alias;
|
|
378
|
+
}, context.config.customTypeMappings, context.schemaResolver, undefined, { ...(direction && { direction }), binary });
|
|
379
|
+
}
|
|
380
|
+
mediaMode(contentType) {
|
|
381
|
+
const media = contentType.toLowerCase();
|
|
382
|
+
return /(?:\/json|\+json)(?:;|$)/.test(media) ? 'json' : media.startsWith('text/') || /(?:\/xml|\+xml)(?:;|$)/.test(media) || media === 'application/x-www-form-urlencoded' ? 'text' : 'blob';
|
|
309
383
|
}
|
|
310
384
|
/**
|
|
311
385
|
* Generate client class
|
|
@@ -320,7 +394,7 @@ class ApiClientGenerator {
|
|
|
320
394
|
content += "\n\n";
|
|
321
395
|
// Generate client class
|
|
322
396
|
content += `export class ${clientName} {\n`;
|
|
323
|
-
content += `
|
|
397
|
+
content += ` public readonly baseUrl: string;\n`;
|
|
324
398
|
content += ` private config: ApiClientConfig;\n\n`;
|
|
325
399
|
// Generate constructor
|
|
326
400
|
content += this.generateConstructor();
|
|
@@ -331,7 +405,7 @@ class ApiClientGenerator {
|
|
|
331
405
|
content += "\n\n";
|
|
332
406
|
}
|
|
333
407
|
// Generate utility methods
|
|
334
|
-
content += this.generateUtilityMethods();
|
|
408
|
+
content += this.generateUtilityMethods(endpoints);
|
|
335
409
|
content += "}\n";
|
|
336
410
|
return content;
|
|
337
411
|
}
|
|
@@ -339,41 +413,46 @@ class ApiClientGenerator {
|
|
|
339
413
|
* Generate configuration interface
|
|
340
414
|
*/
|
|
341
415
|
generateConfigInterface() {
|
|
342
|
-
return `export interface ApiClientConfig {
|
|
343
|
-
baseUrl: string;
|
|
344
|
-
timeout?: number;
|
|
345
|
-
headers?:
|
|
346
|
-
retries?: number;
|
|
347
|
-
retryDelay?: number;
|
|
416
|
+
return `export interface ApiClientConfig {
|
|
417
|
+
baseUrl: string;
|
|
418
|
+
timeout?: number;
|
|
419
|
+
headers?: RequestInit['headers'];
|
|
420
|
+
retries?: number;
|
|
421
|
+
retryDelay?: number;
|
|
348
422
|
}`;
|
|
349
423
|
}
|
|
350
424
|
/**
|
|
351
425
|
* Generate error class
|
|
352
426
|
*/
|
|
353
427
|
generateErrorClass() {
|
|
354
|
-
return `export class ApiClientError extends Error {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
428
|
+
return `export class ApiClientError extends globalThis.Error {
|
|
429
|
+
public status?: number | undefined;
|
|
430
|
+
public response?: Response | undefined;
|
|
431
|
+
|
|
432
|
+
constructor(
|
|
433
|
+
message: string,
|
|
434
|
+
status?: number,
|
|
435
|
+
response?: Response
|
|
436
|
+
) {
|
|
437
|
+
super(message);
|
|
438
|
+
this.status = status;
|
|
439
|
+
this.response = response;
|
|
440
|
+
this.name = 'ApiClientError';
|
|
441
|
+
}
|
|
363
442
|
}`;
|
|
364
443
|
}
|
|
365
444
|
/**
|
|
366
445
|
* Generate constructor
|
|
367
446
|
*/
|
|
368
447
|
generateConstructor() {
|
|
369
|
-
return ` constructor(config: ApiClientConfig) {
|
|
370
|
-
this.baseUrl = config.baseUrl.replace(/\\/$/, '');
|
|
371
|
-
this.config = {
|
|
372
|
-
timeout: 30000,
|
|
373
|
-
retries: 3,
|
|
374
|
-
retryDelay: 1000,
|
|
375
|
-
...config,
|
|
376
|
-
};
|
|
448
|
+
return ` constructor(config: ApiClientConfig) {
|
|
449
|
+
this.baseUrl = config.baseUrl.replace(/\\/$/, '');
|
|
450
|
+
this.config = {
|
|
451
|
+
timeout: 30000,
|
|
452
|
+
retries: 3,
|
|
453
|
+
retryDelay: 1000,
|
|
454
|
+
...config,
|
|
455
|
+
};
|
|
377
456
|
}`;
|
|
378
457
|
}
|
|
379
458
|
/**
|
|
@@ -393,32 +472,17 @@ class ApiClientGenerator {
|
|
|
393
472
|
* Generate method parameters
|
|
394
473
|
*/
|
|
395
474
|
generateMethodParameters(endpoint) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
const pathParams = endpoint.parameters.filter((p) => p.location === "path");
|
|
399
|
-
if (pathParams.length > 0) {
|
|
400
|
-
const pathParamTypes = pathParams.map((p) => `${p.name}: ${p.type}`);
|
|
401
|
-
params.push(...pathParamTypes);
|
|
402
|
-
}
|
|
403
|
-
// Add query parameters
|
|
404
|
-
const queryParams = endpoint.parameters.filter((p) => p.location === "query");
|
|
405
|
-
if (queryParams.length > 0) {
|
|
406
|
-
const queryParamTypes = queryParams.map((p) => `${p.name}${p.isOptional ? "?" : ""}: ${p.type}`);
|
|
407
|
-
params.push(...queryParamTypes);
|
|
408
|
-
}
|
|
409
|
-
// Add request body
|
|
475
|
+
const parameters = ['path', 'query', 'header', 'cookie'].flatMap(location => endpoint.parameters.filter(p => p.location === location))
|
|
476
|
+
.map(p => ({ name: p.variableName || p.name, type: p.type, optional: p.isOptional }));
|
|
410
477
|
if (endpoint.requestBody) {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
const bodyType = isMultipart
|
|
414
|
-
? `${endpoint.requestBody.type} | FormData`
|
|
415
|
-
: endpoint.requestBody.type;
|
|
416
|
-
const bodyParam = `body${endpoint.requestBody.isOptional ? "?" : ""}: ${bodyType}`;
|
|
417
|
-
params.push(bodyParam);
|
|
478
|
+
const body = endpoint.requestBody;
|
|
479
|
+
parameters.push({ name: 'body', type: /^multipart\/form-data/i.test(body.contentType || '') ? body.type + ' | globalThis.FormData' : body.type, optional: body.isOptional });
|
|
418
480
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
481
|
+
const declarations = parameters.map((p, i) => {
|
|
482
|
+
const requiredFollows = parameters.slice(i + 1).some(next => !next.optional);
|
|
483
|
+
return p.name + (p.optional && !requiredFollows ? '?' : '') + ': ' + p.type + (p.optional && requiredFollows ? ' | undefined' : '');
|
|
484
|
+
});
|
|
485
|
+
return [...declarations, 'options?: RequestInit'].join(', ');
|
|
422
486
|
}
|
|
423
487
|
/**
|
|
424
488
|
* Generate method body
|
|
@@ -442,78 +506,49 @@ class ApiClientGenerator {
|
|
|
442
506
|
* Generate URL construction
|
|
443
507
|
*/
|
|
444
508
|
generateUrlConstruction(endpoint) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (pathParams.length > 0) {
|
|
455
|
-
const pathParamEntries = pathParams.map((p) => `${p.name}`).join(", ");
|
|
456
|
-
// Close buildPath first argument backtick, add params object, close buildPath, close ${}, close template, then close new URL
|
|
457
|
-
urlConstruction += "`, { " + pathParamEntries + " })}`);\n";
|
|
509
|
+
const entries = endpoint.parameters.filter(p => p.location === 'path').map(p => JSON.stringify(p.name) + ': ' + this.serializeExpression(p.variableName || p.name, p.schema));
|
|
510
|
+
const metadata = Object.fromEntries(endpoint.parameters.filter(parameter => parameter.location === 'path').map(parameter => [parameter.name, { explode: parameter.explode ?? false }]));
|
|
511
|
+
let output = ' const url = new URL(this.baseUrl + this.buildPath(' + JSON.stringify(endpoint.path) + ', { ' + entries.join(', ') + ' }, ' + JSON.stringify(metadata) + '));\n';
|
|
512
|
+
for (const p of endpoint.parameters.filter(p => p.location === 'query')) {
|
|
513
|
+
const variable = p.variableName || p.name;
|
|
514
|
+
const key = JSON.stringify(p.name);
|
|
515
|
+
output += ' if (' + variable + ' !== undefined) {\n';
|
|
516
|
+
output += ' this.appendQuery(url.searchParams, ' + key + ', ' + this.serializeExpression(variable, p.schema) + ', ' + JSON.stringify(p.style || 'form') + ', ' + String(p.explode ?? (p.style === undefined || p.style === 'form' || p.style === 'deepObject')) + ');\n';
|
|
517
|
+
output += ' }\n';
|
|
458
518
|
}
|
|
459
|
-
|
|
460
|
-
// Close buildPath first argument backtick, add empty params, close buildPath, close ${}, close template, then close new URL
|
|
461
|
-
urlConstruction += "`, {})}`);\n";
|
|
462
|
-
}
|
|
463
|
-
// Add query parameters
|
|
464
|
-
const queryParams = endpoint.parameters.filter((p) => p.location === "query");
|
|
465
|
-
if (queryParams.length > 0) {
|
|
466
|
-
urlConstruction += "\n // Add query parameters\n";
|
|
467
|
-
for (const param of queryParams) {
|
|
468
|
-
urlConstruction += " if (" + param.name + " !== undefined) {\n";
|
|
469
|
-
urlConstruction +=
|
|
470
|
-
" url.searchParams.set('" +
|
|
471
|
-
param.name +
|
|
472
|
-
"', String(" +
|
|
473
|
-
param.name +
|
|
474
|
-
"));\n";
|
|
475
|
-
urlConstruction += " }\n";
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
return urlConstruction;
|
|
519
|
+
return output;
|
|
479
520
|
}
|
|
480
521
|
/**
|
|
481
522
|
* Generate request options
|
|
482
523
|
*/
|
|
483
524
|
generateRequestOptions(endpoint) {
|
|
484
|
-
let
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
525
|
+
let output = ' const headers = new Headers(this.config.headers);\n';
|
|
526
|
+
const contentType = endpoint.requestBody?.contentType || '';
|
|
527
|
+
const json = /(?:application\/json|\+json)/i.test(contentType);
|
|
528
|
+
const multipart = /^multipart\/form-data/i.test(contentType);
|
|
529
|
+
if (endpoint.requestBody && !multipart && contentType)
|
|
530
|
+
output += ' headers.set("Content-Type", ' + JSON.stringify(contentType) + ');\n';
|
|
531
|
+
for (const p of endpoint.parameters.filter(p => p.location === 'header')) {
|
|
532
|
+
const variable = p.variableName || p.name;
|
|
533
|
+
output += ' if (' + variable + ' !== undefined) headers.set(' + JSON.stringify(p.name) + ', this.simpleHeader(' + this.serializeExpression(variable, p.schema) + ', ' + String(p.explode ?? false) + '));\n';
|
|
534
|
+
}
|
|
535
|
+
const cookies = endpoint.parameters.filter(p => p.location === 'cookie');
|
|
536
|
+
if (cookies.length) {
|
|
537
|
+
output += ' const cookies: string[] = [];\n';
|
|
538
|
+
for (const p of cookies) {
|
|
539
|
+
const variable = p.variableName || p.name;
|
|
540
|
+
output += ' if (' + variable + ' !== undefined) cookies.push(' + JSON.stringify(p.name + '=') + ' + encodeURIComponent(String(' + variable + ')));\n';
|
|
541
|
+
}
|
|
542
|
+
output += ' if (cookies.length) headers.set("Cookie", [headers.get("Cookie"), ...cookies].filter(Boolean).join("; "));\n';
|
|
493
543
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// Add request body
|
|
544
|
+
output += ' new Headers(options?.headers).forEach((value, key) => headers.set(key, value));\n';
|
|
545
|
+
output += ' const requestOptions: RequestInit = {\n ...options,\n method: ' + JSON.stringify(endpoint.method) + ',\n headers,\n';
|
|
497
546
|
if (endpoint.requestBody) {
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
/^multipart\/form-data/i.test(endpoint.requestBody.contentType);
|
|
502
|
-
if (isJson) {
|
|
503
|
-
options +=
|
|
504
|
-
" body: body !== undefined ? JSON.stringify(body) : undefined,\n";
|
|
505
|
-
}
|
|
506
|
-
else if (isMultipart) {
|
|
507
|
-
options +=
|
|
508
|
-
" body: (body instanceof FormData ? body : (body !== undefined ? this.toFormData(body as any) : undefined)) as any,\n";
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
options += " body: body as any,\n";
|
|
512
|
-
}
|
|
547
|
+
const value = json || multipart || contentType === 'application/x-www-form-urlencoded' ? this.serializeExpression('body', endpoint.requestBody.schema) : 'body';
|
|
548
|
+
const expression = json ? 'JSON.stringify(' + value + ')' : multipart ? '(body instanceof globalThis.FormData ? body : this.toFormData(' + value + '))' : contentType === 'application/x-www-form-urlencoded' ? 'this.toUrlEncoded(' + value + ')' : "body as NonNullable<RequestInit['body']>";
|
|
549
|
+
output += ' ...(body !== undefined ? { body: ' + expression + ' } : {}),\n';
|
|
513
550
|
}
|
|
514
|
-
|
|
515
|
-
options += " };\n";
|
|
516
|
-
return options;
|
|
551
|
+
return output + ' };\n';
|
|
517
552
|
}
|
|
518
553
|
/**
|
|
519
554
|
* Generate fetch call
|
|
@@ -526,152 +561,151 @@ class ApiClientGenerator {
|
|
|
526
561
|
* Generate response handling
|
|
527
562
|
*/
|
|
528
563
|
generateResponseHandling(endpoint) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
return ` return this.request<${endpoint.returnType}>(url.toString(), requestOptions);\n`;
|
|
564
|
+
const metadata = endpoint.responses.map(response => ({ status: response.statusCode,
|
|
565
|
+
...(response.contentType && { contentType: response.contentType }), ...(response.schema && { schema: response.schema }), mode: response.type === 'void' ? 'empty' : this.mediaMode(response.contentType || 'application/json') }));
|
|
566
|
+
return ` return this.request<${endpoint.returnType}>(url.toString(), requestOptions, ${JSON.stringify(metadata)});\n`;
|
|
534
567
|
}
|
|
535
568
|
/**
|
|
536
569
|
* Generate utility methods
|
|
537
570
|
*/
|
|
538
|
-
generateUtilityMethods() {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
571
|
+
generateUtilityMethods(endpoints) {
|
|
572
|
+
this.usesDeserialization = this.context.config.propertyNaming === 'camelCase' && endpoints.length > 0;
|
|
573
|
+
const headerHelper = endpoints.some(endpoint => endpoint.parameters.some(parameter => parameter.location === 'header')) ? ` private simpleHeader(value: unknown, explode: boolean): string {
|
|
574
|
+
const scalar = (item: unknown): string => {
|
|
575
|
+
if (item !== null && typeof item === 'object') throw new Error('Nested header parameter values are unsupported');
|
|
576
|
+
return String(item);
|
|
577
|
+
};
|
|
578
|
+
if (Array.isArray(value)) return value.map(scalar).join(',');
|
|
579
|
+
if (value !== null && typeof value === 'object') return Object.entries(value).filter(([, item]) => item !== undefined)
|
|
580
|
+
.map(([key, item]) => key + (explode ? '=' : ',') + scalar(item)).join(',');
|
|
581
|
+
return scalar(value);
|
|
582
|
+
}` : '';
|
|
583
|
+
const pathHelper = endpoints.length ? ` private buildPath(template: string, params: Record<string, unknown>, metadata: Record<string, { explode?: boolean }> = {}): string {
|
|
584
|
+
return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {
|
|
585
|
+
const value = params[key];
|
|
586
|
+
if (value === undefined) throw new Error('Missing required path parameter: ' + key);
|
|
587
|
+
if (Array.isArray(value)) return value.map(item => encodeURIComponent(String(item))).join(',');
|
|
588
|
+
if (value !== null && typeof value === 'object') return Object.entries(value).map(([name, item]) =>
|
|
589
|
+
encodeURIComponent(name) + (metadata[key]?.explode ? '=' : ',') + encodeURIComponent(String(item))).join(',');
|
|
590
|
+
return encodeURIComponent(String(value));
|
|
591
|
+
});
|
|
592
|
+
}` : '';
|
|
593
|
+
const queryHelper = endpoints.some(endpoint => endpoint.parameters.some(parameter => parameter.location === 'query')) ? ` private appendQuery(params: URLSearchParams, name: string, value: unknown, style = 'form', explode = true): void {
|
|
594
|
+
const scalar = (item: unknown): string => {
|
|
595
|
+
if (item !== null && typeof item === 'object') throw new Error('Nested query values require an explicit supported encoding: ' + name);
|
|
596
|
+
return String(item);
|
|
597
|
+
};
|
|
598
|
+
if (Array.isArray(value)) {
|
|
599
|
+
if (style === 'deepObject') throw new Error('deepObject requires an object: ' + name);
|
|
600
|
+
if (style === 'spaceDelimited' || style === 'pipeDelimited' || !explode) params.append(name, value.map(scalar).join(style === 'spaceDelimited' ? ' ' : style === 'pipeDelimited' ? '|' : ','));
|
|
601
|
+
else for (const item of value) params.append(name, scalar(item));
|
|
602
|
+
} else if (value !== null && typeof value === 'object') {
|
|
603
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined);
|
|
604
|
+
if (style === 'deepObject') for (const [key, item] of entries) params.append(name + '[' + key + ']', scalar(item));
|
|
605
|
+
else if (style !== 'form') throw new Error('Unsupported object query style: ' + style);
|
|
606
|
+
else if (explode) for (const [key, item] of entries) params.append(key, scalar(item));
|
|
607
|
+
else params.append(name, entries.flatMap(([key, item]) => [key, scalar(item)]).join(','));
|
|
608
|
+
} else params.append(name, scalar(value));
|
|
609
|
+
}` : '';
|
|
610
|
+
const encodedHelper = endpoints.some(endpoint => endpoint.requestBody?.contentType === 'application/x-www-form-urlencoded') ? ` private toUrlEncoded(input: unknown): URLSearchParams {
|
|
611
|
+
const form = new URLSearchParams();
|
|
612
|
+
if (input && typeof input === 'object') for (const [key, value] of Object.entries(input)) {
|
|
613
|
+
if (value === undefined || value === null) continue;
|
|
614
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
615
|
+
if (item !== null && typeof item === 'object') throw new Error('Nested URL-encoded form values are unsupported: ' + key);
|
|
616
|
+
form.append(key, String(item));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return form;
|
|
620
|
+
}` : '';
|
|
621
|
+
const multipartHelper = endpoints.some(endpoint => /^multipart\/form-data/i.test(endpoint.requestBody?.contentType || '')) ? ` private toFormData(input: unknown): globalThis.FormData {
|
|
622
|
+
const form = new globalThis.FormData();
|
|
623
|
+
if (input && typeof input === 'object') for (const [key, value] of Object.entries(input)) {
|
|
624
|
+
if (value === undefined || value === null) continue;
|
|
625
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
626
|
+
if (item instanceof globalThis.Blob) form.append(key, item);
|
|
627
|
+
else if (typeof item === 'object') form.append(key, new globalThis.Blob([JSON.stringify(item)], { type: 'application/json' }));
|
|
628
|
+
else form.append(key, String(item));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return form;
|
|
632
|
+
}` : '';
|
|
633
|
+
const requestHelper = endpoints.length ? ` private async request<T>(url: string, options: RequestInit, responses: Array<{ status: string; contentType?: string; schema?: unknown; mode: 'json' | 'text' | 'blob' | 'empty' }>): Promise<T> {
|
|
634
|
+
const retries = this.config.retries ?? 0;
|
|
635
|
+
const delayMs = this.config.retryDelay ?? 0;
|
|
636
|
+
const canRetry = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'].includes(options.method || 'GET');
|
|
637
|
+
for (let attempt = 0; ; attempt++) {
|
|
638
|
+
const controller = new AbortController();
|
|
639
|
+
const timeout = this.config.timeout ?? 0;
|
|
640
|
+
const timer = timeout > 0 ? setTimeout(() => controller.abort(), timeout) : undefined;
|
|
641
|
+
const onAbort = () => controller.abort();
|
|
642
|
+
if (options.signal?.aborted) controller.abort();
|
|
643
|
+
else options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
644
|
+
try {
|
|
645
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
646
|
+
if (!response.ok) throw new ApiClientError('Request failed: ' + response.status + ' ' + response.statusText, response.status, response);
|
|
647
|
+
if (response.status === 204 || response.status === 205 || options.method === 'HEAD') return undefined as T;
|
|
648
|
+
let candidates = responses.filter(item => item.status === String(response.status));
|
|
649
|
+
if (!candidates.length) candidates = responses.filter(item => item.status.toUpperCase() === Math.floor(response.status / 100) + 'XX');
|
|
650
|
+
if (!candidates.length) candidates = responses.filter(item => item.status === 'default');
|
|
651
|
+
const contentType = (response.headers.get('Content-Type') || '').split(';')[0]!.trim().toLowerCase();
|
|
652
|
+
const declared = candidates.find(item => item.contentType?.toLowerCase() === contentType) || candidates[0];
|
|
653
|
+
if (declared?.mode === 'empty') return undefined as T;
|
|
654
|
+
const mode = contentType ? /(?:\\/json|\\+json)$/.test(contentType) ? 'json' : contentType.startsWith('text/') || /(?:\\/xml|\\+xml)$/.test(contentType) || contentType === 'application/x-www-form-urlencoded' ? 'text' : 'blob' : declared?.mode || 'json';
|
|
655
|
+
if (mode === 'json') return ${this.usesDeserialization ? 'deserializeValue(await response.json(), declared?.schema)' : 'await response.json()'} as T;
|
|
656
|
+
if (mode === 'text') return await response.text() as T;
|
|
657
|
+
return await response.blob() as T;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
const status = error instanceof ApiClientError ? error.status : undefined;
|
|
660
|
+
const transient = status === undefined || status === 408 || status === 429 || (status !== undefined && status >= 500);
|
|
661
|
+
if (!canRetry || !transient || attempt >= retries || options.signal?.aborted || controller.signal.aborted || error instanceof SyntaxError) throw error;
|
|
662
|
+
} finally {
|
|
663
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
664
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
665
|
+
}
|
|
666
|
+
if (delayMs > 0) await new Promise<void>((resolve, reject) => {
|
|
667
|
+
const abort = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', abort); reject(options.signal?.reason || new Error('Request aborted')); };
|
|
668
|
+
const timer = setTimeout(() => { options.signal?.removeEventListener('abort', abort); resolve(); }, delayMs);
|
|
669
|
+
if (options.signal?.aborted) abort(); else options.signal?.addEventListener('abort', abort, { once: true });
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}` : '';
|
|
673
|
+
const authHelpers = ` public setDefaultHeader(name: string, value: string): void {
|
|
674
|
+
const headers = new globalThis.Headers(this.config.headers); headers.set(name, value); this.config.headers = headers;
|
|
675
|
+
}
|
|
676
|
+
public removeDefaultHeader(name: string): void {
|
|
677
|
+
const headers = new globalThis.Headers(this.config.headers); headers.delete(name); this.config.headers = headers;
|
|
678
|
+
}
|
|
679
|
+
public setAuthToken(token: string): void { this.setDefaultHeader('Authorization', 'Bearer ' + token); }
|
|
680
|
+
public clearAuthToken(): void { this.removeDefaultHeader('Authorization'); }`;
|
|
681
|
+
return [pathHelper, queryHelper, headerHelper, encodedHelper, multipartHelper, requestHelper, authHelpers].filter(Boolean).join('\n\n') + '\n';
|
|
613
682
|
}
|
|
614
683
|
/**
|
|
615
684
|
* Generate imports
|
|
616
685
|
*/
|
|
617
686
|
generateImports(endpoints) {
|
|
618
687
|
const imports = [];
|
|
688
|
+
const converters = [...(this.usesSerialization ? ['serializeValue'] : []), ...(this.usesDeserialization ? ['deserializeValue'] : [])];
|
|
689
|
+
if (converters.length)
|
|
690
|
+
imports.push(`import { ${converters.join(', ')} } from './conversion.js';`);
|
|
619
691
|
// Add type imports. Endpoint types are already formatted; don't re-apply prefix/suffix.
|
|
620
692
|
const typeNames = this.getTypeNamesFromEndpoints(endpoints);
|
|
621
693
|
if (typeNames.length > 0) {
|
|
622
|
-
imports.push(`import { ${typeNames.join(", ")} } from './types';`);
|
|
694
|
+
imports.push(`import type { ${typeNames.map(name => this.typeAliases.get(name) && this.typeAliases.get(name) !== name ? name + ' as ' + this.typeAliases.get(name) : name).join(", ")} } from './types.js';`);
|
|
623
695
|
}
|
|
624
696
|
return imports.length > 0 ? imports.join("\n") + "\n\n" : "";
|
|
625
697
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
for (const endpoint of endpoints) {
|
|
632
|
-
// Extract type names from return type
|
|
633
|
-
const returnTypeNames = this.extractTypeNamesFromType(endpoint.returnType);
|
|
634
|
-
returnTypeNames.forEach((name) => typeNames.add(name));
|
|
635
|
-
// Extract type names from request body
|
|
636
|
-
if (endpoint.requestBody) {
|
|
637
|
-
const bodyTypeNames = this.extractTypeNamesFromType(endpoint.requestBody.type);
|
|
638
|
-
bodyTypeNames.forEach((name) => typeNames.add(name));
|
|
639
|
-
}
|
|
640
|
-
// Extract type names from parameters
|
|
641
|
-
for (const param of endpoint.parameters) {
|
|
642
|
-
const paramTypeNames = this.extractTypeNamesFromType(param.type);
|
|
643
|
-
paramTypeNames.forEach((name) => typeNames.add(name));
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return Array.from(typeNames);
|
|
698
|
+
serializeExpression(value, schema) {
|
|
699
|
+
if (this.context.config.propertyNaming !== 'camelCase' || !schema)
|
|
700
|
+
return value;
|
|
701
|
+
this.usesSerialization = true;
|
|
702
|
+
return `serializeValue(${value}, ${JSON.stringify(schema)})`;
|
|
647
703
|
}
|
|
648
704
|
/**
|
|
649
|
-
*
|
|
705
|
+
* Get type names from endpoints
|
|
650
706
|
*/
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
// known TypeScript built-in/global generics (Record, Promise, Array, etc.)
|
|
654
|
-
const matches = type.match(/\b[A-Z][A-Za-z0-9_]*\b/g) || [];
|
|
655
|
-
if (matches.length === 0)
|
|
656
|
-
return [];
|
|
657
|
-
const builtin = new Set([
|
|
658
|
-
// Common TS/global generics and types we should not import
|
|
659
|
-
"Record",
|
|
660
|
-
"Array",
|
|
661
|
-
"Promise",
|
|
662
|
-
"Readonly",
|
|
663
|
-
"Partial",
|
|
664
|
-
"Required",
|
|
665
|
-
"Pick",
|
|
666
|
-
"Omit",
|
|
667
|
-
"Date",
|
|
668
|
-
"Map",
|
|
669
|
-
"Set",
|
|
670
|
-
"BigInt",
|
|
671
|
-
"Uint8Array",
|
|
672
|
-
"Object",
|
|
673
|
-
]);
|
|
674
|
-
return matches.filter((m) => !builtin.has(m));
|
|
707
|
+
getTypeNamesFromEndpoints(_endpoints) {
|
|
708
|
+
return [...this.referencedTypes];
|
|
675
709
|
}
|
|
676
710
|
/**
|
|
677
711
|
* Get dependencies from endpoints
|
|
@@ -679,35 +713,6 @@ class ApiClientGenerator {
|
|
|
679
713
|
getDependencies(endpoints) {
|
|
680
714
|
return this.getTypeNamesFromEndpoints(endpoints);
|
|
681
715
|
}
|
|
682
|
-
/**
|
|
683
|
-
* Apply configured prefix/suffix to a type name
|
|
684
|
-
*/
|
|
685
|
-
/**
|
|
686
|
-
* Normalize a type name using the same rules as BaseTypeGenerator and apply prefix/suffix
|
|
687
|
-
*/
|
|
688
|
-
formatTypeName(name, context) {
|
|
689
|
-
// Sanitize and convert to PascalCase for type identifiers
|
|
690
|
-
const sanitized = name
|
|
691
|
-
.replace(/\[/g, "<")
|
|
692
|
-
.replace(/\]/g, ">")
|
|
693
|
-
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
694
|
-
.replace(/^_+|_+$/g, "")
|
|
695
|
-
.replace(/_+/g, "_");
|
|
696
|
-
const converted = this.toPascalCase(sanitized);
|
|
697
|
-
const { typePrefix, typeSuffix } = context.config;
|
|
698
|
-
let result = converted;
|
|
699
|
-
if (typePrefix)
|
|
700
|
-
result = `${typePrefix}${result}`;
|
|
701
|
-
if (typeSuffix)
|
|
702
|
-
result = `${result}${typeSuffix}`;
|
|
703
|
-
return result;
|
|
704
|
-
}
|
|
705
|
-
toPascalCase(str) {
|
|
706
|
-
if (!str)
|
|
707
|
-
return str;
|
|
708
|
-
const tokens = str.split(/_+/).filter(Boolean);
|
|
709
|
-
return tokens.map((t) => t.charAt(0).toUpperCase() + t.slice(1)).join("");
|
|
710
|
-
}
|
|
711
716
|
/**
|
|
712
717
|
* Get client name
|
|
713
718
|
*/
|
|
@@ -727,7 +732,7 @@ class ApiClientGenerator {
|
|
|
727
732
|
const pascal = tokens
|
|
728
733
|
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
|
729
734
|
.join("");
|
|
730
|
-
return (pascal || "
|
|
735
|
+
return (0, schema_type_1.typeName)((pascal || "") + "ApiClient");
|
|
731
736
|
}
|
|
732
737
|
return "ApiClient";
|
|
733
738
|
}
|
|
@@ -807,7 +812,8 @@ class ApiClientGenerator {
|
|
|
807
812
|
if (/^[0-9]/.test(name)) {
|
|
808
813
|
name = "op" + name;
|
|
809
814
|
}
|
|
810
|
-
return
|
|
815
|
+
const reserved = new Set(['class', 'default', 'delete', 'new', 'function', 'var', 'let', 'const', 'return', 'switch', 'case', 'this', 'super', 'import', 'export', 'await', 'yield', 'constructor', 'break', 'catch', 'continue', 'debugger', 'do', 'else', 'enum', 'extends', 'finally', 'for', 'if', 'in', 'instanceof', 'throw', 'try', 'typeof', 'void', 'while', 'with', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'static', 'true', 'false', 'null', 'eval', 'arguments']);
|
|
816
|
+
return reserved.has(name) ? `${name}Operation` : name;
|
|
811
817
|
}
|
|
812
818
|
/**
|
|
813
819
|
* Capitalize string
|
|
@@ -815,62 +821,6 @@ class ApiClientGenerator {
|
|
|
815
821
|
capitalize(str) {
|
|
816
822
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
817
823
|
}
|
|
818
|
-
/**
|
|
819
|
-
* Extract type name from reference
|
|
820
|
-
*/
|
|
821
|
-
extractTypeNameFromRef(ref) {
|
|
822
|
-
const parts = ref.split("/");
|
|
823
|
-
return parts[parts.length - 1] || "Unknown";
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Map schema type to TypeScript
|
|
827
|
-
*/
|
|
828
|
-
mapSchemaTypeToTypeScript(type, format) {
|
|
829
|
-
const types = Array.isArray(type) ? type : [type];
|
|
830
|
-
// Handle format-specific mappings
|
|
831
|
-
if (format) {
|
|
832
|
-
const formatMappings = {
|
|
833
|
-
date: "string",
|
|
834
|
-
"date-time": "string",
|
|
835
|
-
time: "string",
|
|
836
|
-
email: "string",
|
|
837
|
-
hostname: "string",
|
|
838
|
-
ipv4: "string",
|
|
839
|
-
ipv6: "string",
|
|
840
|
-
uri: "string",
|
|
841
|
-
"uri-reference": "string",
|
|
842
|
-
"uri-template": "string",
|
|
843
|
-
url: "string",
|
|
844
|
-
uuid: "string",
|
|
845
|
-
password: "string",
|
|
846
|
-
byte: "string",
|
|
847
|
-
binary: "string",
|
|
848
|
-
int32: "number",
|
|
849
|
-
int64: "number",
|
|
850
|
-
float: "number",
|
|
851
|
-
double: "number",
|
|
852
|
-
decimal: "number",
|
|
853
|
-
};
|
|
854
|
-
if (formatMappings[format]) {
|
|
855
|
-
return formatMappings[format];
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
// Handle basic type mappings
|
|
859
|
-
const typeMappings = {
|
|
860
|
-
string: "string",
|
|
861
|
-
number: "number",
|
|
862
|
-
integer: "number",
|
|
863
|
-
boolean: "boolean",
|
|
864
|
-
array: "unknown[]",
|
|
865
|
-
object: "Record<string, unknown>",
|
|
866
|
-
null: "null",
|
|
867
|
-
};
|
|
868
|
-
const mappedTypes = types.map((t) => typeMappings[t] || "unknown");
|
|
869
|
-
if (mappedTypes.length === 1) {
|
|
870
|
-
return mappedTypes[0];
|
|
871
|
-
}
|
|
872
|
-
return mappedTypes.join(" | ");
|
|
873
|
-
}
|
|
874
824
|
}
|
|
875
825
|
exports.ApiClientGenerator = ApiClientGenerator;
|
|
876
826
|
//# sourceMappingURL=api-client-generator.js.map
|