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.
Files changed (144) hide show
  1. package/CHANGELOG.md +196 -159
  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 +186 -62
  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 -6
  21. package/dist/core/schemantic.d.ts.map +1 -1
  22. package/dist/core/schemantic.js +194 -127
  23. package/dist/core/schemantic.js.map +1 -1
  24. package/dist/generators/api-client-generator.d.ts +9 -20
  25. package/dist/generators/api-client-generator.d.ts.map +1 -1
  26. package/dist/generators/api-client-generator.js +364 -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 +390 -268
  96. package/dist/plugins/zod-validation.js.map +1 -1
  97. package/dist/types/core.d.ts +20 -4
  98. package/dist/types/core.d.ts.map +1 -1
  99. package/dist/types/core.js +2 -1
  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 -600
  106. package/docs/AUDIT.md +49 -0
  107. package/docs/CONFIGURATION.md +81 -722
  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 -563
  124. package/dist/__tests__/test-config.js.map +0 -1
  125. package/dist/lib/api/api-client.d.ts +0 -71
  126. package/dist/lib/api/api-client.d.ts.map +0 -1
  127. package/dist/lib/api/api-client.js +0 -238
  128. package/dist/lib/api/api-client.js.map +0 -1
  129. package/dist/lib/api/barrel.d.ts +0 -4
  130. package/dist/lib/api/barrel.d.ts.map +0 -1
  131. package/dist/lib/api/barrel.js +0 -26
  132. package/dist/lib/api/barrel.js.map +0 -1
  133. package/dist/lib/api/hooks.d.ts +0 -36
  134. package/dist/lib/api/hooks.d.ts.map +0 -1
  135. package/dist/lib/api/hooks.js +0 -120
  136. package/dist/lib/api/hooks.js.map +0 -1
  137. package/dist/lib/api/index.d.ts +0 -4
  138. package/dist/lib/api/index.d.ts.map +0 -1
  139. package/dist/lib/api/index.js +0 -8
  140. package/dist/lib/api/index.js.map +0 -1
  141. package/dist/lib/api/types.d.ts +0 -366
  142. package/dist/lib/api/types.d.ts.map +0 -1
  143. package/dist/lib/api/types.js +0 -315
  144. 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
- 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') : '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
- // 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);
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
- // $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";
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 += ` private baseUrl: string;\n`;
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?: Record<string, string>;
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 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
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 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);
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
- // Add options parameter
420
- params.push("options?: RequestInit");
421
- return params.join(", ");
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
- 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";
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
- 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;
509
+ return output;
479
510
  }
480
511
  /**
481
512
  * Generate request options
482
513
  */
483
514
  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";
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
- options += " ...options?.headers,\n";
495
- options += " },\n";
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 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
- }
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
- options += " ...options,\n";
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
- // 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`;
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
- 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
- " }");
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
- * 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);
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
- * Extract type names from type string
695
+ * Get type names from endpoints
650
696
  */
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));
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 || "Api") + "ApiClient";
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 name;
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