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.
Files changed (124) hide show
  1. package/CHANGELOG.md +204 -174
  2. package/LICENSE +21 -21
  3. package/README.md +88 -830
  4. package/dist/cli/index.d.ts +4 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +185 -61
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/core/configuration.d.ts +6 -0
  9. package/dist/core/configuration.d.ts.map +1 -0
  10. package/dist/core/configuration.js +64 -0
  11. package/dist/core/configuration.js.map +1 -0
  12. package/dist/core/index.d.ts +1 -0
  13. package/dist/core/index.d.ts.map +1 -1
  14. package/dist/core/index.js +1 -0
  15. package/dist/core/index.js.map +1 -1
  16. package/dist/core/output-writer.d.ts +7 -0
  17. package/dist/core/output-writer.d.ts.map +1 -0
  18. package/dist/core/output-writer.js +255 -0
  19. package/dist/core/output-writer.js.map +1 -0
  20. package/dist/core/schemantic.d.ts +8 -2
  21. package/dist/core/schemantic.d.ts.map +1 -1
  22. package/dist/core/schemantic.js +196 -71
  23. package/dist/core/schemantic.js.map +1 -1
  24. package/dist/generators/api-client-generator.d.ts +10 -20
  25. package/dist/generators/api-client-generator.d.ts.map +1 -1
  26. package/dist/generators/api-client-generator.js +374 -424
  27. package/dist/generators/api-client-generator.js.map +1 -1
  28. package/dist/generators/base.d.ts.map +1 -1
  29. package/dist/generators/base.js +7 -25
  30. package/dist/generators/base.js.map +1 -1
  31. package/dist/generators/enum-generator.d.ts.map +1 -1
  32. package/dist/generators/enum-generator.js +30 -3
  33. package/dist/generators/enum-generator.js.map +1 -1
  34. package/dist/generators/hook-generator.d.ts +3 -17
  35. package/dist/generators/hook-generator.d.ts.map +1 -1
  36. package/dist/generators/hook-generator.js +51 -445
  37. package/dist/generators/hook-generator.js.map +1 -1
  38. package/dist/generators/naming.d.ts +10 -0
  39. package/dist/generators/naming.d.ts.map +1 -0
  40. package/dist/generators/naming.js +331 -0
  41. package/dist/generators/naming.js.map +1 -0
  42. package/dist/generators/object-generator.d.ts +0 -4
  43. package/dist/generators/object-generator.d.ts.map +1 -1
  44. package/dist/generators/object-generator.js +13 -101
  45. package/dist/generators/object-generator.js.map +1 -1
  46. package/dist/generators/primitive-generator.d.ts +1 -13
  47. package/dist/generators/primitive-generator.d.ts.map +1 -1
  48. package/dist/generators/primitive-generator.js +11 -76
  49. package/dist/generators/primitive-generator.js.map +1 -1
  50. package/dist/generators/projections.d.ts +10 -0
  51. package/dist/generators/projections.d.ts.map +1 -0
  52. package/dist/generators/projections.js +65 -0
  53. package/dist/generators/projections.js.map +1 -0
  54. package/dist/generators/schema-type.d.ts +23 -0
  55. package/dist/generators/schema-type.d.ts.map +1 -0
  56. package/dist/generators/schema-type.js +233 -0
  57. package/dist/generators/schema-type.js.map +1 -0
  58. package/dist/parsers/base.d.ts +2 -0
  59. package/dist/parsers/base.d.ts.map +1 -1
  60. package/dist/parsers/base.js +12 -2
  61. package/dist/parsers/base.js.map +1 -1
  62. package/dist/parsers/factory.d.ts.map +1 -1
  63. package/dist/parsers/factory.js +2 -1
  64. package/dist/parsers/factory.js.map +1 -1
  65. package/dist/parsers/openapi.d.ts +4 -0
  66. package/dist/parsers/openapi.d.ts.map +1 -1
  67. package/dist/parsers/openapi.js +107 -20
  68. package/dist/parsers/openapi.js.map +1 -1
  69. package/dist/plugins/branded-types.d.ts.map +1 -1
  70. package/dist/plugins/branded-types.js +231 -365
  71. package/dist/plugins/branded-types.js.map +1 -1
  72. package/dist/plugins/builtin.d.ts.map +1 -1
  73. package/dist/plugins/builtin.js +6 -141
  74. package/dist/plugins/builtin.js.map +1 -1
  75. package/dist/plugins/index.d.ts +4 -0
  76. package/dist/plugins/index.d.ts.map +1 -1
  77. package/dist/plugins/index.js +17 -0
  78. package/dist/plugins/index.js.map +1 -1
  79. package/dist/plugins/loader.d.ts +5 -1
  80. package/dist/plugins/loader.d.ts.map +1 -1
  81. package/dist/plugins/loader.js +42 -5
  82. package/dist/plugins/loader.js.map +1 -1
  83. package/dist/plugins/manager.d.ts.map +1 -1
  84. package/dist/plugins/manager.js +7 -7
  85. package/dist/plugins/manager.js.map +1 -1
  86. package/dist/plugins/performance-monitoring.d.ts +31 -1
  87. package/dist/plugins/performance-monitoring.d.ts.map +1 -1
  88. package/dist/plugins/performance-monitoring.js +146 -207
  89. package/dist/plugins/performance-monitoring.js.map +1 -1
  90. package/dist/plugins/request-deduplication.d.ts.map +1 -1
  91. package/dist/plugins/request-deduplication.js +151 -586
  92. package/dist/plugins/request-deduplication.js.map +1 -1
  93. package/dist/plugins/zod-validation.d.ts +23 -15
  94. package/dist/plugins/zod-validation.d.ts.map +1 -1
  95. package/dist/plugins/zod-validation.js +392 -268
  96. package/dist/plugins/zod-validation.js.map +1 -1
  97. package/dist/types/core.d.ts +20 -3
  98. package/dist/types/core.d.ts.map +1 -1
  99. package/dist/types/core.js +2 -0
  100. package/dist/types/core.js.map +1 -1
  101. package/dist/types/openapi.d.ts +14 -1
  102. package/dist/types/openapi.d.ts.map +1 -1
  103. package/docs/ADVANCED_PLUGINS_IMPLEMENTATION.md +24 -355
  104. package/docs/ADVANCED_PLUGIN_DEVELOPMENT.md +33 -309
  105. package/docs/API.md +80 -601
  106. package/docs/AUDIT.md +49 -0
  107. package/docs/CONFIGURATION.md +81 -715
  108. package/docs/FASTAPI_TUTORIAL.md +109 -1497
  109. package/docs/NAMING_MIGRATION.md +59 -0
  110. package/docs/PLUGINS.md +49 -687
  111. package/docs/PLUGIN_DEVELOPMENT.md +53 -720
  112. package/docs/REAUDIT.md +399 -0
  113. package/docs/RELEASING.md +88 -0
  114. package/docs/REMEDIATION.md +58 -0
  115. package/docs/superpowers/plans/2026-10-10-audit-remediation.md +113 -0
  116. package/package.json +114 -95
  117. package/dist/__tests__/setup.d.ts +0 -6
  118. package/dist/__tests__/setup.d.ts.map +0 -1
  119. package/dist/__tests__/setup.js +0 -30
  120. package/dist/__tests__/setup.js.map +0 -1
  121. package/dist/__tests__/test-config.d.ts +0 -36
  122. package/dist/__tests__/test-config.d.ts.map +0 -1
  123. package/dist/__tests__/test-config.js +0 -562
  124. 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
- for (const [path, pathItem] of Object.entries(paths)) {
45
- if (!pathItem || typeof pathItem !== "object") {
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
- for (const param of parameters) {
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
- // Skip $ref parameters for now (would need to be resolved)
110
- if ("$ref" in param) {
111
- continue;
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 isRequired = param.required || false;
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
- // Handle reference
243
+ // Resolve the Request Body Object, then inspect its content and required flag.
134
244
  if ("$ref" in requestBody) {
135
- const resolvedSchema = context.schemaResolver(requestBody.$ref);
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.schema) {
152
- let type = this.extractSchemaType(mediaType.schema, context);
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 contentType = Object.keys(responseObj.content)[0];
196
- if (!contentType) {
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 = responseObj.content[contentType];
207
- if (mediaType && mediaType.schema) {
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
- // Handle reference
257
- if ("$ref" in schema) {
258
- if (typeof schema.$ref === "string") {
259
- const refType = this.extractTypeNameFromRef(schema.$ref);
260
- // Normalize and apply prefix/suffix to match type generator output
261
- return this.formatTypeName(refType, context);
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
- // $ref exists but isn't a string; cannot extract a type name
264
- return "unknown";
265
- }
266
- // Handle arrays
267
- if ("type" in schema && schema.type === "array") {
268
- const items = schema.items;
269
- const itemType = items
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 += ` private baseUrl: string;\n`;
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?: Record<string, string>;
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
- constructor(
356
- message: string,
357
- public status?: number,
358
- public response?: Response
359
- ) {
360
- super(message);
361
- this.name = 'ApiClientError';
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 params = [];
397
- // Add path parameters
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 isMultipart = !!endpoint.requestBody.contentType &&
412
- /^multipart\/form-data/i.test(endpoint.requestBody.contentType);
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
- // Add options parameter
420
- params.push("options?: RequestInit");
421
- return params.join(", ");
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
- let urlConstruction = " const url = new URL(`${this.baseUrl}${this.buildPath(`";
446
- // Replace path parameters
447
- const pathParams = endpoint.parameters.filter((p) => p.location === "path");
448
- let path = endpoint.path;
449
- for (const param of pathParams) {
450
- path = path.replace(`{${param.name}}`, `\${${param.name}}`);
451
- }
452
- urlConstruction += path;
453
- // Add path parameters
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
- else {
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 options = " const requestOptions: RequestInit = {\n";
485
- options += " method: '" + endpoint.method + "',\n";
486
- options += " headers: {\n";
487
- options += " ...this.config.headers,\n";
488
- // Add content type for request body only for JSON payloads
489
- if (endpoint.requestBody &&
490
- endpoint.requestBody.contentType &&
491
- /application\/json/i.test(endpoint.requestBody.contentType)) {
492
- options += " 'Content-Type': 'application/json',\n";
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
- options += " ...options?.headers,\n";
495
- options += " },\n";
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 isJson = !!endpoint.requestBody.contentType &&
499
- /application\/json/i.test(endpoint.requestBody.contentType);
500
- const isMultipart = !!endpoint.requestBody.contentType &&
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
- options += " ...options,\n";
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
- // Use request helper for both void and typed responses
530
- if (endpoint.returnType === "void") {
531
- return " await this.request<void>(url.toString(), requestOptions);\n return;\n";
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
- return (" private buildPath(template: string, params: Record<string, string | number>): string {\n" +
540
- " return template.replace(/\\{([^}]+)\\}/g, (_match, key) => {\n" +
541
- " const value = params[key];\n" +
542
- " if (value === undefined) {\n" +
543
- " throw new Error('Missing required path parameter: ' + key);\n" +
544
- " }\n" +
545
- " return String(value);\n" +
546
- " });\n" +
547
- " }\n\n" +
548
- " private toFormData(input: any): FormData {\n" +
549
- " const form = new FormData();\n" +
550
- " if (input && typeof input === 'object') {\n" +
551
- " for (const [key, value] of Object.entries(input)) {\n" +
552
- " if (value === undefined || value === null) continue;\n" +
553
- " if (value instanceof Blob || (typeof File !== 'undefined' && value instanceof File)) {\n" +
554
- " form.append(key, value as any);\n" +
555
- " } else if (Array.isArray(value)) {\n" +
556
- " for (const v of value) {\n" +
557
- " if (v instanceof Blob || (typeof File !== 'undefined' && v instanceof File)) form.append(key, v as any);\n" +
558
- " else if (typeof v === 'object') form.append(key, new Blob([JSON.stringify(v)], { type: 'application/json' }));\n" +
559
- " else form.append(key, String(v));\n" +
560
- " }\n" +
561
- " } else if (typeof value === 'object') {\n" +
562
- " form.append(key, new Blob([JSON.stringify(value)], { type: 'application/json' }));\n" +
563
- " } else {\n" +
564
- " form.append(key, String(value));\n" +
565
- " }\n" +
566
- " }\n" +
567
- " }\n" +
568
- " return form;\n" +
569
- " }\n\n" +
570
- " private async request<T>(url: string, options: RequestInit): Promise<T> {\n" +
571
- " let lastError: unknown;\n" +
572
- " const retries = this.config.retries ?? 0;\n" +
573
- " const delayMs = this.config.retryDelay ?? 0;\n" +
574
- " for (let attempt = 0; attempt <= retries; attempt++) {\n" +
575
- " try {\n" +
576
- " const controller = new AbortController();\n" +
577
- " const timeout = this.config.timeout ?? 0;\n" +
578
- " const timer = timeout > 0 ? setTimeout(() => controller.abort(), timeout) : undefined;\n" +
579
- " // Link external AbortSignal if provided\n" +
580
- " if (options.signal) {\n" +
581
- " const ext = options.signal;\n" +
582
- " if (ext.aborted) controller.abort();\n" +
583
- " else ext.addEventListener('abort', () => controller.abort(), { once: true });\n" +
584
- " }\n" +
585
- " const { signal: _omit, ...rest } = options as any;\n" +
586
- " const response = await fetch(url, { ...rest, signal: controller.signal });\n" +
587
- " if (timer) clearTimeout(timer);\n" +
588
- " if (!response.ok) {\n" +
589
- " throw new ApiClientError('Request failed: ' + response.status + ' ' + response.statusText, response.status, response);\n" +
590
- " }\n" +
591
- " if (response.status === 204 || options.method === 'HEAD') {\n" +
592
- " return undefined as unknown as T;\n" +
593
- " }\n" +
594
- " return (await response.json()) as T;\n" +
595
- " } catch (err) {\n" +
596
- " lastError = err;\n" +
597
- " if (attempt < retries && delayMs > 0) {\n" +
598
- " await new Promise((r) => setTimeout(r, delayMs));\n" +
599
- " continue;\n" +
600
- " }\n" +
601
- " throw lastError;\n" +
602
- " }\n" +
603
- " }\n" +
604
- " throw lastError as Error;\n" +
605
- " }\n\n" +
606
- " // Convenience helpers for bearer auth\n" +
607
- " public setAuthToken(token: string) {\n" +
608
- " this.config.headers = { ...(this.config.headers || {}), Authorization: `Bearer ${token}` };\n" +
609
- " }\n" +
610
- " public clearAuthToken() {\n" +
611
- " if (this.config.headers) delete (this.config.headers as any)['Authorization'];\n" +
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
- * Get type names from endpoints
628
- */
629
- getTypeNamesFromEndpoints(endpoints) {
630
- const typeNames = new Set();
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
- * Extract type names from type string
705
+ * Get type names from endpoints
650
706
  */
651
- extractTypeNamesFromType(type) {
652
- // Capture PascalCase identifiers used as type names, but exclude
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 || "Api") + "ApiClient";
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 name;
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