schemantic 0.2.1 → 0.4.0
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 +196 -159
- 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 +186 -62
- 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 -6
- package/dist/core/schemantic.d.ts.map +1 -1
- package/dist/core/schemantic.js +194 -127
- package/dist/core/schemantic.js.map +1 -1
- package/dist/generators/api-client-generator.d.ts +9 -20
- package/dist/generators/api-client-generator.d.ts.map +1 -1
- package/dist/generators/api-client-generator.js +364 -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 +390 -268
- package/dist/plugins/zod-validation.js.map +1 -1
- package/dist/types/core.d.ts +20 -4
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js +2 -1
- 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 -600
- package/docs/AUDIT.md +49 -0
- package/docs/CONFIGURATION.md +81 -722
- 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 -563
- package/dist/__tests__/test-config.js.map +0 -1
- package/dist/lib/api/api-client.d.ts +0 -71
- package/dist/lib/api/api-client.d.ts.map +0 -1
- package/dist/lib/api/api-client.js +0 -238
- package/dist/lib/api/api-client.js.map +0 -1
- package/dist/lib/api/barrel.d.ts +0 -4
- package/dist/lib/api/barrel.d.ts.map +0 -1
- package/dist/lib/api/barrel.js +0 -26
- package/dist/lib/api/barrel.js.map +0 -1
- package/dist/lib/api/hooks.d.ts +0 -36
- package/dist/lib/api/hooks.d.ts.map +0 -1
- package/dist/lib/api/hooks.js +0 -120
- package/dist/lib/api/hooks.js.map +0 -1
- package/dist/lib/api/index.d.ts +0 -4
- package/dist/lib/api/index.d.ts.map +0 -1
- package/dist/lib/api/index.js +0 -8
- package/dist/lib/api/index.js.map +0 -1
- package/dist/lib/api/types.d.ts +0 -366
- package/dist/lib/api/types.d.ts.map +0 -1
- package/dist/lib/api/types.js +0 -315
- package/dist/lib/api/types.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') : '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
|
}
|
|
@@ -230,6 +327,8 @@ class ApiClientGenerator {
|
|
|
230
327
|
* Determine return type from responses
|
|
231
328
|
*/
|
|
232
329
|
determineReturnType(responses) {
|
|
330
|
+
// Non-success responses are thrown as ApiClientError by the runtime.
|
|
331
|
+
responses = responses.filter(response => /^2(?:\d{2}|XX)$/i.test(response.statusCode) || response.statusCode === 'default');
|
|
233
332
|
if (responses.length === 0) {
|
|
234
333
|
return "void";
|
|
235
334
|
}
|
|
@@ -237,7 +336,7 @@ class ApiClientGenerator {
|
|
|
237
336
|
return responses[0].type;
|
|
238
337
|
}
|
|
239
338
|
// Multiple responses - create union type
|
|
240
|
-
const types = responses.map((r) => r.type);
|
|
339
|
+
const types = [...new Set(responses.map((r) => r.type))];
|
|
241
340
|
return types.join(" | ");
|
|
242
341
|
}
|
|
243
342
|
/**
|
|
@@ -247,65 +346,35 @@ class ApiClientGenerator {
|
|
|
247
346
|
if (!schema) {
|
|
248
347
|
return "unknown";
|
|
249
348
|
}
|
|
250
|
-
return this.extractSchemaType(schema, context);
|
|
349
|
+
return this.extractSchemaType(schema, context, 'request');
|
|
251
350
|
}
|
|
252
351
|
/**
|
|
253
352
|
* Extract schema type
|
|
254
353
|
*/
|
|
255
|
-
extractSchemaType(schema, context) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
354
|
+
extractSchemaType(schema, context, direction, binary = false) {
|
|
355
|
+
return (0, schema_type_1.schemaType)(schema, ref => {
|
|
356
|
+
const original = (0, schema_type_1.referenceName)(ref);
|
|
357
|
+
const projection = context.projections?.[original];
|
|
358
|
+
const target = binary ? projection?.binary || projection?.request || original : direction ? projection?.[direction] || original : original;
|
|
359
|
+
const name = (0, schema_type_1.typeName)(target, context.config.typePrefix, context.config.typeSuffix);
|
|
360
|
+
this.referencedTypes.add(name);
|
|
361
|
+
const globals = new Set(['Blob', 'File', 'FormData', 'Headers', 'URL', 'URLSearchParams', 'Response', 'RequestInit', 'BodyInit', 'AbortController', 'Error', 'ApiClientConfig', 'ApiClientError', this.clientName]);
|
|
362
|
+
let alias = this.typeAliases.get(name) || name;
|
|
363
|
+
if (globals.has(name) && !this.typeAliases.has(name)) {
|
|
364
|
+
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()]);
|
|
365
|
+
const base = 'Schema' + name;
|
|
366
|
+
alias = base;
|
|
367
|
+
let index = 2;
|
|
368
|
+
while (occupied.has(alias))
|
|
369
|
+
alias = base + index++;
|
|
262
370
|
}
|
|
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";
|
|
371
|
+
this.typeAliases.set(name, alias);
|
|
372
|
+
return alias;
|
|
373
|
+
}, context.config.customTypeMappings, context.schemaResolver, undefined, { ...(direction && { direction }), binary });
|
|
374
|
+
}
|
|
375
|
+
mediaMode(contentType) {
|
|
376
|
+
const media = contentType.toLowerCase();
|
|
377
|
+
return /(?:\/json|\+json)(?:;|$)/.test(media) ? 'json' : media.startsWith('text/') || /(?:\/xml|\+xml)(?:;|$)/.test(media) || media === 'application/x-www-form-urlencoded' ? 'text' : 'blob';
|
|
309
378
|
}
|
|
310
379
|
/**
|
|
311
380
|
* Generate client class
|
|
@@ -320,7 +389,7 @@ class ApiClientGenerator {
|
|
|
320
389
|
content += "\n\n";
|
|
321
390
|
// Generate client class
|
|
322
391
|
content += `export class ${clientName} {\n`;
|
|
323
|
-
content += `
|
|
392
|
+
content += ` public readonly baseUrl: string;\n`;
|
|
324
393
|
content += ` private config: ApiClientConfig;\n\n`;
|
|
325
394
|
// Generate constructor
|
|
326
395
|
content += this.generateConstructor();
|
|
@@ -331,7 +400,7 @@ class ApiClientGenerator {
|
|
|
331
400
|
content += "\n\n";
|
|
332
401
|
}
|
|
333
402
|
// Generate utility methods
|
|
334
|
-
content += this.generateUtilityMethods();
|
|
403
|
+
content += this.generateUtilityMethods(endpoints);
|
|
335
404
|
content += "}\n";
|
|
336
405
|
return content;
|
|
337
406
|
}
|
|
@@ -339,41 +408,41 @@ class ApiClientGenerator {
|
|
|
339
408
|
* Generate configuration interface
|
|
340
409
|
*/
|
|
341
410
|
generateConfigInterface() {
|
|
342
|
-
return `export interface ApiClientConfig {
|
|
343
|
-
baseUrl: string;
|
|
344
|
-
timeout?: number;
|
|
345
|
-
headers?:
|
|
346
|
-
retries?: number;
|
|
347
|
-
retryDelay?: number;
|
|
411
|
+
return `export interface ApiClientConfig {
|
|
412
|
+
baseUrl: string;
|
|
413
|
+
timeout?: number;
|
|
414
|
+
headers?: RequestInit['headers'];
|
|
415
|
+
retries?: number;
|
|
416
|
+
retryDelay?: number;
|
|
348
417
|
}`;
|
|
349
418
|
}
|
|
350
419
|
/**
|
|
351
420
|
* Generate error class
|
|
352
421
|
*/
|
|
353
422
|
generateErrorClass() {
|
|
354
|
-
return `export class ApiClientError extends Error {
|
|
355
|
-
constructor(
|
|
356
|
-
message: string,
|
|
357
|
-
public status?: number,
|
|
358
|
-
public response?: Response
|
|
359
|
-
) {
|
|
360
|
-
super(message);
|
|
361
|
-
this.name = 'ApiClientError';
|
|
362
|
-
}
|
|
423
|
+
return `export class ApiClientError extends globalThis.Error {
|
|
424
|
+
constructor(
|
|
425
|
+
message: string,
|
|
426
|
+
public status?: number,
|
|
427
|
+
public response?: Response
|
|
428
|
+
) {
|
|
429
|
+
super(message);
|
|
430
|
+
this.name = 'ApiClientError';
|
|
431
|
+
}
|
|
363
432
|
}`;
|
|
364
433
|
}
|
|
365
434
|
/**
|
|
366
435
|
* Generate constructor
|
|
367
436
|
*/
|
|
368
437
|
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
|
-
};
|
|
438
|
+
return ` constructor(config: ApiClientConfig) {
|
|
439
|
+
this.baseUrl = config.baseUrl.replace(/\\/$/, '');
|
|
440
|
+
this.config = {
|
|
441
|
+
timeout: 30000,
|
|
442
|
+
retries: 3,
|
|
443
|
+
retryDelay: 1000,
|
|
444
|
+
...config,
|
|
445
|
+
};
|
|
377
446
|
}`;
|
|
378
447
|
}
|
|
379
448
|
/**
|
|
@@ -393,32 +462,17 @@ class ApiClientGenerator {
|
|
|
393
462
|
* Generate method parameters
|
|
394
463
|
*/
|
|
395
464
|
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
|
|
465
|
+
const parameters = ['path', 'query', 'header', 'cookie'].flatMap(location => endpoint.parameters.filter(p => p.location === location))
|
|
466
|
+
.map(p => ({ name: p.variableName || p.name, type: p.type, optional: p.isOptional }));
|
|
410
467
|
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);
|
|
468
|
+
const body = endpoint.requestBody;
|
|
469
|
+
parameters.push({ name: 'body', type: /^multipart\/form-data/i.test(body.contentType || '') ? body.type + ' | globalThis.FormData' : body.type, optional: body.isOptional });
|
|
418
470
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
471
|
+
const declarations = parameters.map((p, i) => {
|
|
472
|
+
const requiredFollows = parameters.slice(i + 1).some(next => !next.optional);
|
|
473
|
+
return p.name + (p.optional && !requiredFollows ? '?' : '') + ': ' + p.type + (p.optional && requiredFollows ? ' | undefined' : '');
|
|
474
|
+
});
|
|
475
|
+
return [...declarations, 'options?: RequestInit'].join(', ');
|
|
422
476
|
}
|
|
423
477
|
/**
|
|
424
478
|
* Generate method body
|
|
@@ -442,78 +496,49 @@ class ApiClientGenerator {
|
|
|
442
496
|
* Generate URL construction
|
|
443
497
|
*/
|
|
444
498
|
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";
|
|
499
|
+
const entries = endpoint.parameters.filter(p => p.location === 'path').map(p => JSON.stringify(p.name) + ': ' + this.serializeExpression(p.variableName || p.name, p.schema));
|
|
500
|
+
const metadata = Object.fromEntries(endpoint.parameters.filter(parameter => parameter.location === 'path').map(parameter => [parameter.name, { explode: parameter.explode ?? false }]));
|
|
501
|
+
let output = ' const url = new URL(this.baseUrl + this.buildPath(' + JSON.stringify(endpoint.path) + ', { ' + entries.join(', ') + ' }, ' + JSON.stringify(metadata) + '));\n';
|
|
502
|
+
for (const p of endpoint.parameters.filter(p => p.location === 'query')) {
|
|
503
|
+
const variable = p.variableName || p.name;
|
|
504
|
+
const key = JSON.stringify(p.name);
|
|
505
|
+
output += ' if (' + variable + ' !== undefined) {\n';
|
|
506
|
+
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';
|
|
507
|
+
output += ' }\n';
|
|
458
508
|
}
|
|
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;
|
|
509
|
+
return output;
|
|
479
510
|
}
|
|
480
511
|
/**
|
|
481
512
|
* Generate request options
|
|
482
513
|
*/
|
|
483
514
|
generateRequestOptions(endpoint) {
|
|
484
|
-
let
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
515
|
+
let output = ' const headers = new Headers(this.config.headers);\n';
|
|
516
|
+
const contentType = endpoint.requestBody?.contentType || '';
|
|
517
|
+
const json = /(?:application\/json|\+json)/i.test(contentType);
|
|
518
|
+
const multipart = /^multipart\/form-data/i.test(contentType);
|
|
519
|
+
if (endpoint.requestBody && !multipart && contentType)
|
|
520
|
+
output += ' headers.set("Content-Type", ' + JSON.stringify(contentType) + ');\n';
|
|
521
|
+
for (const p of endpoint.parameters.filter(p => p.location === 'header')) {
|
|
522
|
+
const variable = p.variableName || p.name;
|
|
523
|
+
output += ' if (' + variable + ' !== undefined) headers.set(' + JSON.stringify(p.name) + ', this.simpleHeader(' + this.serializeExpression(variable, p.schema) + ', ' + String(p.explode ?? false) + '));\n';
|
|
524
|
+
}
|
|
525
|
+
const cookies = endpoint.parameters.filter(p => p.location === 'cookie');
|
|
526
|
+
if (cookies.length) {
|
|
527
|
+
output += ' const cookies: string[] = [];\n';
|
|
528
|
+
for (const p of cookies) {
|
|
529
|
+
const variable = p.variableName || p.name;
|
|
530
|
+
output += ' if (' + variable + ' !== undefined) cookies.push(' + JSON.stringify(p.name + '=') + ' + encodeURIComponent(String(' + variable + ')));\n';
|
|
531
|
+
}
|
|
532
|
+
output += ' if (cookies.length) headers.set("Cookie", [headers.get("Cookie"), ...cookies].filter(Boolean).join("; "));\n';
|
|
493
533
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// Add request body
|
|
534
|
+
output += ' new Headers(options?.headers).forEach((value, key) => headers.set(key, value));\n';
|
|
535
|
+
output += ' const requestOptions: RequestInit = {\n ...options,\n method: ' + JSON.stringify(endpoint.method) + ',\n headers,\n';
|
|
497
536
|
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
|
-
}
|
|
537
|
+
const value = json || multipart || contentType === 'application/x-www-form-urlencoded' ? this.serializeExpression('body', endpoint.requestBody.schema) : 'body';
|
|
538
|
+
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']>";
|
|
539
|
+
output += ' ...(body !== undefined ? { body: ' + expression + ' } : {}),\n';
|
|
513
540
|
}
|
|
514
|
-
|
|
515
|
-
options += " };\n";
|
|
516
|
-
return options;
|
|
541
|
+
return output + ' };\n';
|
|
517
542
|
}
|
|
518
543
|
/**
|
|
519
544
|
* Generate fetch call
|
|
@@ -526,152 +551,151 @@ class ApiClientGenerator {
|
|
|
526
551
|
* Generate response handling
|
|
527
552
|
*/
|
|
528
553
|
generateResponseHandling(endpoint) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
return ` return this.request<${endpoint.returnType}>(url.toString(), requestOptions);\n`;
|
|
554
|
+
const metadata = endpoint.responses.map(response => ({ status: response.statusCode,
|
|
555
|
+
...(response.contentType && { contentType: response.contentType }), ...(response.schema && { schema: response.schema }), mode: response.type === 'void' ? 'empty' : this.mediaMode(response.contentType || 'application/json') }));
|
|
556
|
+
return ` return this.request<${endpoint.returnType}>(url.toString(), requestOptions, ${JSON.stringify(metadata)});\n`;
|
|
534
557
|
}
|
|
535
558
|
/**
|
|
536
559
|
* Generate utility methods
|
|
537
560
|
*/
|
|
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
|
-
|
|
561
|
+
generateUtilityMethods(endpoints) {
|
|
562
|
+
this.usesDeserialization = this.context.config.propertyNaming === 'camelCase' && endpoints.length > 0;
|
|
563
|
+
const headerHelper = endpoints.some(endpoint => endpoint.parameters.some(parameter => parameter.location === 'header')) ? ` private simpleHeader(value: unknown, explode: boolean): string {
|
|
564
|
+
const scalar = (item: unknown): string => {
|
|
565
|
+
if (item !== null && typeof item === 'object') throw new Error('Nested header parameter values are unsupported');
|
|
566
|
+
return String(item);
|
|
567
|
+
};
|
|
568
|
+
if (Array.isArray(value)) return value.map(scalar).join(',');
|
|
569
|
+
if (value !== null && typeof value === 'object') return Object.entries(value).filter(([, item]) => item !== undefined)
|
|
570
|
+
.map(([key, item]) => key + (explode ? '=' : ',') + scalar(item)).join(',');
|
|
571
|
+
return scalar(value);
|
|
572
|
+
}` : '';
|
|
573
|
+
const pathHelper = endpoints.length ? ` private buildPath(template: string, params: Record<string, unknown>, metadata: Record<string, { explode?: boolean }> = {}): string {
|
|
574
|
+
return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {
|
|
575
|
+
const value = params[key];
|
|
576
|
+
if (value === undefined) throw new Error('Missing required path parameter: ' + key);
|
|
577
|
+
if (Array.isArray(value)) return value.map(item => encodeURIComponent(String(item))).join(',');
|
|
578
|
+
if (value !== null && typeof value === 'object') return Object.entries(value).map(([name, item]) =>
|
|
579
|
+
encodeURIComponent(name) + (metadata[key]?.explode ? '=' : ',') + encodeURIComponent(String(item))).join(',');
|
|
580
|
+
return encodeURIComponent(String(value));
|
|
581
|
+
});
|
|
582
|
+
}` : '';
|
|
583
|
+
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 {
|
|
584
|
+
const scalar = (item: unknown): string => {
|
|
585
|
+
if (item !== null && typeof item === 'object') throw new Error('Nested query values require an explicit supported encoding: ' + name);
|
|
586
|
+
return String(item);
|
|
587
|
+
};
|
|
588
|
+
if (Array.isArray(value)) {
|
|
589
|
+
if (style === 'deepObject') throw new Error('deepObject requires an object: ' + name);
|
|
590
|
+
if (style === 'spaceDelimited' || style === 'pipeDelimited' || !explode) params.append(name, value.map(scalar).join(style === 'spaceDelimited' ? ' ' : style === 'pipeDelimited' ? '|' : ','));
|
|
591
|
+
else for (const item of value) params.append(name, scalar(item));
|
|
592
|
+
} else if (value !== null && typeof value === 'object') {
|
|
593
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined);
|
|
594
|
+
if (style === 'deepObject') for (const [key, item] of entries) params.append(name + '[' + key + ']', scalar(item));
|
|
595
|
+
else if (style !== 'form') throw new Error('Unsupported object query style: ' + style);
|
|
596
|
+
else if (explode) for (const [key, item] of entries) params.append(key, scalar(item));
|
|
597
|
+
else params.append(name, entries.flatMap(([key, item]) => [key, scalar(item)]).join(','));
|
|
598
|
+
} else params.append(name, scalar(value));
|
|
599
|
+
}` : '';
|
|
600
|
+
const encodedHelper = endpoints.some(endpoint => endpoint.requestBody?.contentType === 'application/x-www-form-urlencoded') ? ` private toUrlEncoded(input: unknown): URLSearchParams {
|
|
601
|
+
const form = new URLSearchParams();
|
|
602
|
+
if (input && typeof input === 'object') for (const [key, value] of Object.entries(input)) {
|
|
603
|
+
if (value === undefined || value === null) continue;
|
|
604
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
605
|
+
if (item !== null && typeof item === 'object') throw new Error('Nested URL-encoded form values are unsupported: ' + key);
|
|
606
|
+
form.append(key, String(item));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return form;
|
|
610
|
+
}` : '';
|
|
611
|
+
const multipartHelper = endpoints.some(endpoint => /^multipart\/form-data/i.test(endpoint.requestBody?.contentType || '')) ? ` private toFormData(input: unknown): globalThis.FormData {
|
|
612
|
+
const form = new globalThis.FormData();
|
|
613
|
+
if (input && typeof input === 'object') for (const [key, value] of Object.entries(input)) {
|
|
614
|
+
if (value === undefined || value === null) continue;
|
|
615
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
616
|
+
if (item instanceof globalThis.Blob) form.append(key, item);
|
|
617
|
+
else if (typeof item === 'object') form.append(key, new globalThis.Blob([JSON.stringify(item)], { type: 'application/json' }));
|
|
618
|
+
else form.append(key, String(item));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return form;
|
|
622
|
+
}` : '';
|
|
623
|
+
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> {
|
|
624
|
+
const retries = this.config.retries ?? 0;
|
|
625
|
+
const delayMs = this.config.retryDelay ?? 0;
|
|
626
|
+
const canRetry = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'].includes(options.method || 'GET');
|
|
627
|
+
for (let attempt = 0; ; attempt++) {
|
|
628
|
+
const controller = new AbortController();
|
|
629
|
+
const timeout = this.config.timeout ?? 0;
|
|
630
|
+
const timer = timeout > 0 ? setTimeout(() => controller.abort(), timeout) : undefined;
|
|
631
|
+
const onAbort = () => controller.abort();
|
|
632
|
+
if (options.signal?.aborted) controller.abort();
|
|
633
|
+
else options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
634
|
+
try {
|
|
635
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
636
|
+
if (!response.ok) throw new ApiClientError('Request failed: ' + response.status + ' ' + response.statusText, response.status, response);
|
|
637
|
+
if (response.status === 204 || response.status === 205 || options.method === 'HEAD') return undefined as T;
|
|
638
|
+
let candidates = responses.filter(item => item.status === String(response.status));
|
|
639
|
+
if (!candidates.length) candidates = responses.filter(item => item.status.toUpperCase() === Math.floor(response.status / 100) + 'XX');
|
|
640
|
+
if (!candidates.length) candidates = responses.filter(item => item.status === 'default');
|
|
641
|
+
const contentType = (response.headers.get('Content-Type') || '').split(';')[0]!.trim().toLowerCase();
|
|
642
|
+
const declared = candidates.find(item => item.contentType?.toLowerCase() === contentType) || candidates[0];
|
|
643
|
+
if (declared?.mode === 'empty') return undefined as T;
|
|
644
|
+
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';
|
|
645
|
+
if (mode === 'json') return ${this.usesDeserialization ? 'deserializeValue(await response.json(), declared?.schema)' : 'await response.json()'} as T;
|
|
646
|
+
if (mode === 'text') return await response.text() as T;
|
|
647
|
+
return await response.blob() as T;
|
|
648
|
+
} catch (error) {
|
|
649
|
+
const status = error instanceof ApiClientError ? error.status : undefined;
|
|
650
|
+
const transient = status === undefined || status === 408 || status === 429 || (status !== undefined && status >= 500);
|
|
651
|
+
if (!canRetry || !transient || attempt >= retries || options.signal?.aborted || controller.signal.aborted || error instanceof SyntaxError) throw error;
|
|
652
|
+
} finally {
|
|
653
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
654
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
655
|
+
}
|
|
656
|
+
if (delayMs > 0) await new Promise<void>((resolve, reject) => {
|
|
657
|
+
const abort = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', abort); reject(options.signal?.reason || new Error('Request aborted')); };
|
|
658
|
+
const timer = setTimeout(() => { options.signal?.removeEventListener('abort', abort); resolve(); }, delayMs);
|
|
659
|
+
if (options.signal?.aborted) abort(); else options.signal?.addEventListener('abort', abort, { once: true });
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}` : '';
|
|
663
|
+
const authHelpers = ` public setDefaultHeader(name: string, value: string): void {
|
|
664
|
+
const headers = new globalThis.Headers(this.config.headers); headers.set(name, value); this.config.headers = headers;
|
|
665
|
+
}
|
|
666
|
+
public removeDefaultHeader(name: string): void {
|
|
667
|
+
const headers = new globalThis.Headers(this.config.headers); headers.delete(name); this.config.headers = headers;
|
|
668
|
+
}
|
|
669
|
+
public setAuthToken(token: string): void { this.setDefaultHeader('Authorization', 'Bearer ' + token); }
|
|
670
|
+
public clearAuthToken(): void { this.removeDefaultHeader('Authorization'); }`;
|
|
671
|
+
return [pathHelper, queryHelper, headerHelper, encodedHelper, multipartHelper, requestHelper, authHelpers].filter(Boolean).join('\n\n') + '\n';
|
|
613
672
|
}
|
|
614
673
|
/**
|
|
615
674
|
* Generate imports
|
|
616
675
|
*/
|
|
617
676
|
generateImports(endpoints) {
|
|
618
677
|
const imports = [];
|
|
678
|
+
const converters = [...(this.usesSerialization ? ['serializeValue'] : []), ...(this.usesDeserialization ? ['deserializeValue'] : [])];
|
|
679
|
+
if (converters.length)
|
|
680
|
+
imports.push(`import { ${converters.join(', ')} } from './conversion.js';`);
|
|
619
681
|
// Add type imports. Endpoint types are already formatted; don't re-apply prefix/suffix.
|
|
620
682
|
const typeNames = this.getTypeNamesFromEndpoints(endpoints);
|
|
621
683
|
if (typeNames.length > 0) {
|
|
622
|
-
imports.push(`import { ${typeNames.join(", ")} } from './types';`);
|
|
684
|
+
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
685
|
}
|
|
624
686
|
return imports.length > 0 ? imports.join("\n") + "\n\n" : "";
|
|
625
687
|
}
|
|
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);
|
|
688
|
+
serializeExpression(value, schema) {
|
|
689
|
+
if (this.context.config.propertyNaming !== 'camelCase' || !schema)
|
|
690
|
+
return value;
|
|
691
|
+
this.usesSerialization = true;
|
|
692
|
+
return `serializeValue(${value}, ${JSON.stringify(schema)})`;
|
|
647
693
|
}
|
|
648
694
|
/**
|
|
649
|
-
*
|
|
695
|
+
* Get type names from endpoints
|
|
650
696
|
*/
|
|
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));
|
|
697
|
+
getTypeNamesFromEndpoints(_endpoints) {
|
|
698
|
+
return [...this.referencedTypes];
|
|
675
699
|
}
|
|
676
700
|
/**
|
|
677
701
|
* Get dependencies from endpoints
|
|
@@ -679,35 +703,6 @@ class ApiClientGenerator {
|
|
|
679
703
|
getDependencies(endpoints) {
|
|
680
704
|
return this.getTypeNamesFromEndpoints(endpoints);
|
|
681
705
|
}
|
|
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
706
|
/**
|
|
712
707
|
* Get client name
|
|
713
708
|
*/
|
|
@@ -727,7 +722,7 @@ class ApiClientGenerator {
|
|
|
727
722
|
const pascal = tokens
|
|
728
723
|
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
|
729
724
|
.join("");
|
|
730
|
-
return (pascal || "
|
|
725
|
+
return (0, schema_type_1.typeName)((pascal || "") + "ApiClient");
|
|
731
726
|
}
|
|
732
727
|
return "ApiClient";
|
|
733
728
|
}
|
|
@@ -807,7 +802,8 @@ class ApiClientGenerator {
|
|
|
807
802
|
if (/^[0-9]/.test(name)) {
|
|
808
803
|
name = "op" + name;
|
|
809
804
|
}
|
|
810
|
-
return
|
|
805
|
+
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']);
|
|
806
|
+
return reserved.has(name) ? `${name}Operation` : name;
|
|
811
807
|
}
|
|
812
808
|
/**
|
|
813
809
|
* Capitalize string
|
|
@@ -815,62 +811,6 @@ class ApiClientGenerator {
|
|
|
815
811
|
capitalize(str) {
|
|
816
812
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
817
813
|
}
|
|
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
814
|
}
|
|
875
815
|
exports.ApiClientGenerator = ApiClientGenerator;
|
|
876
816
|
//# sourceMappingURL=api-client-generator.js.map
|