@sdk-it/typescript 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // packages/typescript/src/lib/generate.ts
2
+ import { template } from "lodash-es";
2
3
  import { join as join2 } from "node:path";
3
4
  import { npmRunPathEnv } from "npm-run-path";
4
5
  import { spinalcase as spinalcase3 } from "stringcase";
@@ -6,7 +7,7 @@ import { getFolderExports, methods, writeFiles } from "@sdk-it/core";
6
7
 
7
8
  // packages/typescript/src/lib/client.ts
8
9
  import { toLitObject } from "@sdk-it/core";
9
- var client_default = (spec) => {
10
+ var client_default = (spec, throwError) => {
10
11
  const optionsEntries = Object.entries(spec.options).map(
11
12
  ([key, value]) => [`'${key}'`, value]
12
13
  );
@@ -28,15 +29,18 @@ var client_default = (spec) => {
28
29
  }
29
30
  };
30
31
  return `
31
- import { fetchType, sendRequest } from './http/${spec.makeImport("send-request")}';
32
+ import type { RequestConfig } from './http/${spec.makeImport("request")}';
33
+ import { fetchType, sendRequest, parse } from './http/${spec.makeImport("send-request")}';
32
34
  import z from 'zod';
33
- import type { Endpoints } from './api/${spec.makeImport("schemas")}';
35
+ import type { Endpoints } from './api/${spec.makeImport("endpoints")}';
34
36
  import schemas from './api/${spec.makeImport("schemas")}';
35
37
  import {
36
38
  createBaseUrlInterceptor,
37
39
  createHeadersInterceptor,
38
40
  } from './http/${spec.makeImport("interceptors")}';
39
41
 
42
+ import { parseInput, type ParseError } from './http/${spec.makeImport("parser")}';
43
+
40
44
  ${spec.servers.length ? `export const servers = ${JSON.stringify(spec.servers, null, 2)} as const` : ""}
41
45
  const optionsSchema = z.object(${toLitObject(specOptions, (x) => x.schema)});
42
46
  ${spec.servers.length ? `export type Servers = typeof servers[number];` : ""}
@@ -53,7 +57,9 @@ export class ${spec.name} {
53
57
  endpoint: E,
54
58
  input: Endpoints[E]['input'],
55
59
  options?: { signal?: AbortSignal, headers?: HeadersInit },
56
- ): Promise<readonly [Endpoints[E]['output'], Endpoints[E]['error'] | null]> {
60
+ )
61
+ ${throwError ? `: Endpoints[E]['output']` : `: Promise<readonly [Endpoints[E]['output'], Endpoints[E]['error'] | null]>`}
62
+ {
57
63
  const route = schemas[endpoint];
58
64
  return sendRequest(Object.assign(this.#defaultInputs, input), route, {
59
65
  fetch: this.options.fetch,
@@ -65,6 +71,44 @@ export class ${spec.name} {
65
71
  });
66
72
  }
67
73
 
74
+ async prepare<E extends keyof Endpoints>(
75
+ endpoint: E,
76
+ input: Endpoints[E]['input'],
77
+ options?: { headers?: HeadersInit },
78
+ ): Promise<
79
+ readonly [
80
+ RequestConfig & {
81
+ parse: (response: Response) => ReturnType<typeof parse>;
82
+ },
83
+ ParseError<(typeof schemas)[E]['schema']> | null,
84
+ ]
85
+ > {
86
+ const route = schemas[endpoint];
87
+
88
+ const interceptors = [
89
+ createHeadersInterceptor(
90
+ () => this.defaultHeaders,
91
+ options?.headers ?? {},
92
+ ),
93
+ createBaseUrlInterceptor(() => this.options.baseUrl),
94
+ ];
95
+ const [parsedInput, parseError] = parseInput(route.schema, input);
96
+ if (parseError) {
97
+ return [null as never, parseError as never] as const;
98
+ }
99
+
100
+ let config = route.toRequest(parsedInput as never);
101
+ for (const interceptor of interceptors) {
102
+ if (interceptor.before) {
103
+ config = await interceptor.before(config);
104
+ }
105
+ }
106
+ return [
107
+ { ...config, parse: (response: Response) => parse(route, response) },
108
+ null as never,
109
+ ] as const;
110
+ }
111
+
68
112
  get defaultHeaders() {
69
113
  return ${defaultHeaders}
70
114
  }
@@ -86,10 +130,272 @@ export class ${spec.name} {
86
130
  };
87
131
 
88
132
  // packages/typescript/src/lib/generator.ts
89
- import { get as get2, merge } from "lodash-es";
133
+ import { merge } from "lodash-es";
90
134
  import { join } from "node:path";
91
- import { camelcase as camelcase2, pascalcase as pascalcase2, spinalcase as spinalcase2 } from "stringcase";
92
- import { followRef as followRef4, forEachOperation, isRef as isRef5 } from "@sdk-it/core";
135
+ import { camelcase as camelcase3, pascalcase as pascalcase2, spinalcase as spinalcase2 } from "stringcase";
136
+ import { followRef as followRef4, isEmpty, isRef as isRef5 } from "@sdk-it/core";
137
+
138
+ // packages/spec/dist/lib/operation.js
139
+ import { camelcase } from "stringcase";
140
+ var defaults = {
141
+ operationId: (operation, path, method) => {
142
+ if (operation.operationId) {
143
+ return camelcase(operation.operationId);
144
+ }
145
+ const metadata = operation["x-oaiMeta"];
146
+ if (metadata && metadata.name) {
147
+ return camelcase(metadata.name);
148
+ }
149
+ return camelcase(
150
+ [method, ...path.replace(/[\\/\\{\\}]/g, " ").split(" ")].filter(Boolean).join(" ").trim()
151
+ );
152
+ },
153
+ tag: (operation, path) => {
154
+ return operation.tags?.[0] || determineGenericTag(path, operation);
155
+ }
156
+ };
157
+ function forEachOperation(config, callback) {
158
+ const result = [];
159
+ for (const [path, pathItem] of Object.entries(config.spec.paths ?? {})) {
160
+ const { parameters = [], ...methods2 } = pathItem;
161
+ const fixedPath = path.replace(/:([^/]+)/g, "{$1}");
162
+ for (const [method, operation] of Object.entries(methods2)) {
163
+ const formatOperationId = config.operationId ?? defaults.operationId;
164
+ const formatTag = config.tag ?? defaults.tag;
165
+ const operationName = formatOperationId(operation, fixedPath, method);
166
+ const operationTag = formatTag(operation, fixedPath);
167
+ const metadata = operation["x-oaiMeta"] ?? {};
168
+ result.push(
169
+ callback(
170
+ {
171
+ name: metadata.name,
172
+ method,
173
+ path: fixedPath,
174
+ groupName: operationTag,
175
+ tag: operationTag
176
+ },
177
+ {
178
+ ...operation,
179
+ parameters: [...parameters, ...operation.parameters ?? []],
180
+ operationId: operationName
181
+ }
182
+ )
183
+ );
184
+ }
185
+ }
186
+ return result;
187
+ }
188
+ var reservedKeywords = /* @__PURE__ */ new Set([
189
+ "abstract",
190
+ "arguments",
191
+ "await",
192
+ "boolean",
193
+ "break",
194
+ "byte",
195
+ "case",
196
+ "catch",
197
+ "char",
198
+ "class",
199
+ "const",
200
+ "continue",
201
+ "debugger",
202
+ "default",
203
+ "delete",
204
+ "do",
205
+ "double",
206
+ "else",
207
+ "enum",
208
+ "eval",
209
+ "export",
210
+ "extends",
211
+ "false",
212
+ "final",
213
+ "finally",
214
+ "float",
215
+ "for",
216
+ "function",
217
+ "goto",
218
+ "if",
219
+ "implements",
220
+ "import",
221
+ "in",
222
+ "instanceof",
223
+ "int",
224
+ "interface",
225
+ "let",
226
+ "long",
227
+ "native",
228
+ "new",
229
+ "null",
230
+ "package",
231
+ "private",
232
+ "protected",
233
+ "public",
234
+ "return",
235
+ "short",
236
+ "static",
237
+ "super",
238
+ "switch",
239
+ "synchronized",
240
+ "this",
241
+ "throw",
242
+ "throws",
243
+ "transient",
244
+ "true",
245
+ "try",
246
+ "typeof",
247
+ "var",
248
+ "void",
249
+ "volatile",
250
+ "while",
251
+ "with",
252
+ "yield",
253
+ // Potentially problematic identifiers / Common Verbs used as tags
254
+ "object",
255
+ "string",
256
+ "number",
257
+ "any",
258
+ "unknown",
259
+ "never",
260
+ "get",
261
+ "list",
262
+ "create",
263
+ "update",
264
+ "delete",
265
+ "post",
266
+ "put",
267
+ "patch",
268
+ "do",
269
+ "send",
270
+ "add",
271
+ "remove",
272
+ "set",
273
+ "find",
274
+ "search",
275
+ "check",
276
+ "make"
277
+ // Added make, check
278
+ ]);
279
+ function sanitizeTag(camelCasedTag) {
280
+ if (/^\d/.test(camelCasedTag)) {
281
+ return `_${camelCasedTag}`;
282
+ }
283
+ return reservedKeywords.has(camelCasedTag) ? `${camelCasedTag}_` : camelCasedTag;
284
+ }
285
+ function determineGenericTag(pathString, operation) {
286
+ const operationId = operation.operationId || "";
287
+ const VERSION_REGEX = /^[vV]\d+$/;
288
+ const commonVerbs = /* @__PURE__ */ new Set([
289
+ // Verbs to potentially strip from operationId prefix
290
+ "get",
291
+ "list",
292
+ "create",
293
+ "update",
294
+ "delete",
295
+ "post",
296
+ "put",
297
+ "patch",
298
+ "do",
299
+ "send",
300
+ "add",
301
+ "remove",
302
+ "set",
303
+ "find",
304
+ "search",
305
+ "check",
306
+ "make"
307
+ // Added make
308
+ ]);
309
+ const segments = pathString.split("/").filter(Boolean);
310
+ const potentialCandidates = segments.filter(
311
+ (segment) => segment && !segment.startsWith("{") && !segment.endsWith("}") && !VERSION_REGEX.test(segment)
312
+ );
313
+ for (let i = potentialCandidates.length - 1; i >= 0; i--) {
314
+ const segment = potentialCandidates[i];
315
+ if (!segment.startsWith("@")) {
316
+ return sanitizeTag(camelcase(segment));
317
+ }
318
+ }
319
+ const canFallbackToPathSegment = potentialCandidates.length > 0;
320
+ if (operationId) {
321
+ const lowerOpId = operationId.toLowerCase();
322
+ const parts = operationId.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").replace(/([a-zA-Z])(\d)/g, "$1_$2").replace(/(\d)([a-zA-Z])/g, "$1_$2").toLowerCase().split(/[_-\s]+/);
323
+ const validParts = parts.filter(Boolean);
324
+ if (commonVerbs.has(lowerOpId) && validParts.length === 1 && canFallbackToPathSegment) {
325
+ } else if (validParts.length > 0) {
326
+ const firstPart = validParts[0];
327
+ const isFirstPartVerb = commonVerbs.has(firstPart);
328
+ if (isFirstPartVerb && validParts.length > 1) {
329
+ const verbPrefixLength = firstPart.length;
330
+ let nextPartStartIndex = -1;
331
+ if (operationId.length > verbPrefixLength) {
332
+ const charAfterPrefix = operationId[verbPrefixLength];
333
+ if (charAfterPrefix >= "A" && charAfterPrefix <= "Z") {
334
+ nextPartStartIndex = verbPrefixLength;
335
+ } else if (charAfterPrefix >= "0" && charAfterPrefix <= "9") {
336
+ nextPartStartIndex = verbPrefixLength;
337
+ } else if (["_", "-"].includes(charAfterPrefix)) {
338
+ nextPartStartIndex = verbPrefixLength + 1;
339
+ } else {
340
+ const match = operationId.substring(verbPrefixLength).match(/[A-Z0-9]/);
341
+ if (match && match.index !== void 0) {
342
+ nextPartStartIndex = verbPrefixLength + match.index;
343
+ }
344
+ if (nextPartStartIndex === -1 && operationId.length > verbPrefixLength) {
345
+ nextPartStartIndex = verbPrefixLength;
346
+ }
347
+ }
348
+ }
349
+ if (nextPartStartIndex !== -1 && nextPartStartIndex < operationId.length) {
350
+ const remainingOriginalSubstring = operationId.substring(nextPartStartIndex);
351
+ const potentialTag = camelcase(remainingOriginalSubstring);
352
+ if (potentialTag) {
353
+ return sanitizeTag(potentialTag);
354
+ }
355
+ }
356
+ const potentialTagJoined = camelcase(validParts.slice(1).join("_"));
357
+ if (potentialTagJoined) {
358
+ return sanitizeTag(potentialTagJoined);
359
+ }
360
+ }
361
+ const potentialTagFull = camelcase(operationId);
362
+ if (potentialTagFull) {
363
+ const isResultSingleVerb = validParts.length === 1 && isFirstPartVerb;
364
+ if (!(isResultSingleVerb && canFallbackToPathSegment)) {
365
+ if (potentialTagFull.length > 0) {
366
+ return sanitizeTag(potentialTagFull);
367
+ }
368
+ }
369
+ }
370
+ const firstPartCamel = camelcase(firstPart);
371
+ if (firstPartCamel) {
372
+ const isFirstPartCamelVerb = commonVerbs.has(firstPartCamel);
373
+ if (!isFirstPartCamelVerb || validParts.length === 1 || !canFallbackToPathSegment) {
374
+ return sanitizeTag(firstPartCamel);
375
+ }
376
+ }
377
+ if (isFirstPartVerb && validParts.length > 1 && validParts[1] && canFallbackToPathSegment) {
378
+ const secondPartCamel = camelcase(validParts[1]);
379
+ if (secondPartCamel) {
380
+ return sanitizeTag(secondPartCamel);
381
+ }
382
+ }
383
+ }
384
+ }
385
+ if (potentialCandidates.length > 0) {
386
+ let firstCandidate = potentialCandidates[0];
387
+ if (firstCandidate.startsWith("@")) {
388
+ firstCandidate = firstCandidate.substring(1);
389
+ }
390
+ if (firstCandidate) {
391
+ return sanitizeTag(camelcase(firstCandidate));
392
+ }
393
+ }
394
+ console.warn(
395
+ `Could not determine a suitable tag for path: ${pathString}, operationId: ${operationId}. Using 'unknown'.`
396
+ );
397
+ return "unknown";
398
+ }
93
399
 
94
400
  // packages/typescript/src/lib/emitters/zod.ts
95
401
  import { cleanRef, followRef, isRef, parseRef } from "@sdk-it/core";
@@ -218,10 +524,13 @@ var ZodDeserialzer = class {
218
524
  }
219
525
  return `z.union([${oneOfSchemas.join(", ")}])${appendOptional(required)}`;
220
526
  }
221
- enum(values) {
527
+ enum(type, values) {
222
528
  if (values.length === 1) {
223
529
  return `z.literal(${values.join(", ")})`;
224
530
  }
531
+ if (type === "integer") {
532
+ return `z.union([${values.map((val) => `z.literal(${val})`).join(", ")}])`;
533
+ }
225
534
  return `z.enum([${values.join(", ")}])`;
226
535
  }
227
536
  /**
@@ -261,7 +570,7 @@ var ZodDeserialzer = class {
261
570
  break;
262
571
  case "byte":
263
572
  case "binary":
264
- base = "z.instanceof(Blob) /* consider base64 check if needed */";
573
+ base = "z.instanceof(Blob)";
265
574
  break;
266
575
  case "int64":
267
576
  base = "z.string() /* or z.bigint() if your app can handle it */";
@@ -321,7 +630,7 @@ var ZodDeserialzer = class {
321
630
  if (schema.enum && Array.isArray(schema.enum)) {
322
631
  const enumVals = schema.enum.map((val) => JSON.stringify(val));
323
632
  const defaultValue = enumVals.includes(JSON.stringify(schema.default)) ? JSON.stringify(schema.default) : void 0;
324
- return `${this.enum(enumVals)}${this.#suffixes(defaultValue, required, false)}`;
633
+ return `${this.enum(schema.type, enumVals)}${this.#suffixes(defaultValue, required, false)}`;
325
634
  }
326
635
  const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
327
636
  if (!types.length) {
@@ -352,7 +661,7 @@ function appendDefault(defaultValue) {
352
661
 
353
662
  // packages/typescript/src/lib/sdk.ts
354
663
  import { get } from "lodash-es";
355
- import { camelcase, pascalcase, spinalcase } from "stringcase";
664
+ import { camelcase as camelcase2, pascalcase, spinalcase } from "stringcase";
356
665
  import { followRef as followRef3, isRef as isRef4, toLitObject as toLitObject2 } from "@sdk-it/core";
357
666
 
358
667
  // packages/typescript/src/lib/emitters/interface.ts
@@ -705,7 +1014,7 @@ function generateInputs(operationsSet, commonZod, makeImport) {
705
1014
  const output = [];
706
1015
  const imports = /* @__PURE__ */ new Set(['import { z } from "zod";']);
707
1016
  for (const operation of operations) {
708
- const schemaName = camelcase(`${operation.name} schema`);
1017
+ const schemaName = camelcase2(`${operation.name} schema`);
709
1018
  const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject2(operation.schemas)};`;
710
1019
  const inputContent = schema;
711
1020
  for (const schema2 of commonImports) {
@@ -742,8 +1051,8 @@ function generateInputs(operationsSet, commonZod, makeImport) {
742
1051
  };
743
1052
  }
744
1053
  function toEndpoint(groupName, spec, specOperation, operation, utils) {
745
- const schemaName = camelcase(`${operation.name} schema`);
746
- const schemaRef = `${camelcase(groupName)}.${schemaName}`;
1054
+ const schemaName = camelcase2(`${operation.name} schema`);
1055
+ const schemaRef = `${camelcase2(groupName)}.${schemaName}`;
747
1056
  const inputHeaders = [];
748
1057
  const inputQuery = [];
749
1058
  const inputBody = [];
@@ -776,12 +1085,7 @@ function toEndpoint(groupName, spec, specOperation, operation, utils) {
776
1085
  return statusCode >= 200 && statusCode < 300;
777
1086
  }).length > 1;
778
1087
  for (const status in specOperation.responses) {
779
- const response = isRef4(
780
- specOperation.responses[status]
781
- ) ? followRef3(
782
- spec,
783
- specOperation.responses[status].$ref
784
- ) : specOperation.responses[status];
1088
+ const response = isRef4(specOperation.responses[status]) ? followRef3(spec, specOperation.responses[status].$ref) : specOperation.responses[status];
785
1089
  const handled = handleResponse(
786
1090
  spec,
787
1091
  operation.name,
@@ -928,6 +1232,9 @@ function handleResponse(spec, operationName, status, response, utils, numbered)
928
1232
  return { schemas, imports, endpointImports, responses, outputs };
929
1233
  }
930
1234
 
1235
+ // packages/typescript/src/lib/styles/github/endpoints.txt
1236
+ var endpoints_default = "\ntype Output<T extends OutputType> = T extends {\n parser: Parser;\n type: Type<unknown>;\n}\n ? InstanceType<T['type']>\n : T extends Type<unknown>\n ? InstanceType<T>\n : never;\n\ntype Unionize<T> = T extends [infer Single extends OutputType]\n ? Output<Single>\n : T extends readonly [...infer Tuple extends OutputType[]]\n ? { [I in keyof Tuple]: Output<Tuple[I]> }[number]\n : never;\n\ntype EndpointOutput<K extends keyof typeof schemas> = Extract<\n Unionize<(typeof schemas)[K]['output']>,\n SuccessfulResponse\n>;\n\ntype EndpointError<K extends keyof typeof schemas> = Extract<\n Unionize<(typeof schemas)[K]['output']>,\n ProblematicResponse\n>;\n\nexport type Endpoints = {\n [K in keyof typeof schemas]: {\n input: z.infer<(typeof schemas)[K]['schema']>;\n output: EndpointOutput<K>;\n error: EndpointError<K> | ParseError<(typeof schemas)[K]['schema']>;\n };\n};";
1237
+
931
1238
  // packages/typescript/src/lib/generator.ts
932
1239
  function generateCode(config) {
933
1240
  const commonZod = /* @__PURE__ */ new Map();
@@ -983,21 +1290,37 @@ function generateCode(config) {
983
1290
  const schemas = {};
984
1291
  const shortContenTypeMap = {
985
1292
  "application/json": "json",
1293
+ "application/*+json": "json",
1294
+ // type specific of json like application/vnd.api+json (from the generation pov it shouldn't matter)
1295
+ "text/json": "json",
1296
+ // non standard - later standardized to application/json
986
1297
  "application/x-www-form-urlencoded": "urlencoded",
987
1298
  "multipart/form-data": "formdata",
988
1299
  "application/xml": "xml",
989
1300
  "text/plain": "text"
990
1301
  };
991
1302
  let outgoingContentType;
992
- if (operation.requestBody && Object.keys(operation.requestBody).length) {
993
- const content = isRef5(operation.requestBody) ? get2(followRef4(config.spec, operation.requestBody.$ref), ["content"]) : operation.requestBody.content;
994
- for (const type in content) {
995
- const ctSchema = isRef5(content[type].schema) ? followRef4(config.spec, content[type].schema.$ref) : content[type].schema;
1303
+ if (!isEmpty(operation.requestBody)) {
1304
+ const requestBody = isRef5(operation.requestBody) ? followRef4(config.spec, operation.requestBody.$ref) : operation.requestBody;
1305
+ for (const type in requestBody.content) {
1306
+ const ctSchema = isRef5(requestBody.content[type].schema) ? followRef4(config.spec, requestBody.content[type].schema.$ref) : requestBody.content[type].schema;
996
1307
  if (!ctSchema) {
997
- console.warn(`Schema not found for ${type}`);
1308
+ console.warn(
1309
+ `Schema not found for ${type} in ${entry.method} ${entry.path}`
1310
+ );
998
1311
  continue;
999
1312
  }
1000
- const schema = merge({}, ctSchema, {
1313
+ let objectSchema = ctSchema;
1314
+ if (objectSchema.type !== "object") {
1315
+ objectSchema = {
1316
+ type: "object",
1317
+ required: [requestBody.required ? "$body" : ""],
1318
+ properties: {
1319
+ $body: ctSchema
1320
+ }
1321
+ };
1322
+ }
1323
+ const schema = merge({}, objectSchema, {
1001
1324
  required: additionalProperties.filter((p) => p.required).map((p) => p.name),
1002
1325
  properties: additionalProperties.reduce(
1003
1326
  (acc, p) => ({
@@ -1007,14 +1330,14 @@ function generateCode(config) {
1007
1330
  {}
1008
1331
  )
1009
1332
  });
1010
- Object.assign(inputs, bodyInputs(config, ctSchema));
1333
+ Object.assign(inputs, bodyInputs(config, objectSchema));
1011
1334
  schemas[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
1012
1335
  }
1013
- if (content["application/json"]) {
1336
+ if (requestBody.content["application/json"]) {
1014
1337
  outgoingContentType = "json";
1015
- } else if (content["application/x-www-form-urlencoded"]) {
1338
+ } else if (requestBody.content["application/x-www-form-urlencoded"]) {
1016
1339
  outgoingContentType = "urlencoded";
1017
- } else if (content["multipart/form-data"]) {
1340
+ } else if (requestBody.content["multipart/form-data"]) {
1018
1341
  outgoingContentType = "formdata";
1019
1342
  } else {
1020
1343
  outgoingContentType = "json";
@@ -1042,7 +1365,7 @@ function generateCode(config) {
1042
1365
  operation,
1043
1366
  {
1044
1367
  outgoingContentType,
1045
- name: entry.name,
1368
+ name: operation.operationId,
1046
1369
  type: "http",
1047
1370
  trigger: entry,
1048
1371
  schemas,
@@ -1060,13 +1383,15 @@ function generateCode(config) {
1060
1383
  ...responses.map((it) => `export type ${it.name} = ${it.schema};`)
1061
1384
  );
1062
1385
  } else {
1063
- output.push(`export type ${pascalcase2(entry.name + " output")} = void;`);
1386
+ output.push(
1387
+ `export type ${pascalcase2(operation.operationId + " output")} = void;`
1388
+ );
1064
1389
  }
1065
1390
  output.unshift(...useImports(output.join(""), ...responsesImports));
1066
- outputs[`${spinalcase2(entry.name)}.ts`] = output.join("\n");
1391
+ outputs[`${spinalcase2(operation.operationId)}.ts`] = output.join("\n");
1067
1392
  endpoints[entry.groupName].push(endpoint);
1068
1393
  groups[entry.groupName].push({
1069
- name: entry.name,
1394
+ name: operation.operationId,
1070
1395
  type: "http",
1071
1396
  inputs,
1072
1397
  outgoingContentType,
@@ -1091,8 +1416,8 @@ function generateCode(config) {
1091
1416
  {}
1092
1417
  );
1093
1418
  const allSchemas = Object.keys(endpoints).map((it) => ({
1094
- import: `import ${camelcase2(it)} from './${config.makeImport(spinalcase2(it))}';`,
1095
- use: ` ...${camelcase2(it)}`
1419
+ import: `import ${camelcase3(it)} from './${config.makeImport(spinalcase2(it))}';`,
1420
+ use: ` ...${camelcase3(it)}`
1096
1421
  }));
1097
1422
  const imports = [
1098
1423
  'import z from "zod";',
@@ -1105,44 +1430,27 @@ function generateCode(config) {
1105
1430
  commonSchemas,
1106
1431
  commonZod,
1107
1432
  outputs,
1108
- clientFiles: {},
1109
1433
  endpoints: {
1110
- [`${join("api", config.makeImport("schemas"))}`]: `
1111
- ${imports.join("\n")}
1112
- ${allSchemas.map((it) => it.import).join("\n")}
1113
-
1114
- const schemas = {
1115
- ${allSchemas.map((it) => it.use).join(",\n")}
1116
- };
1434
+ [join("api", "endpoints.ts")]: `
1117
1435
 
1118
1436
 
1119
- type Output<T extends OutputType> = T extends {
1120
- parser: Parser;
1121
- type: Type<any>;
1122
- }
1123
- ? InstanceType<T['type']>
1124
- : T extends Type<any>
1125
- ? InstanceType<T>
1126
- : never;
1437
+ import type z from 'zod';
1438
+ import type { ParseError } from '${config.makeImport("../http/parser")}';
1439
+ import type { ProblematicResponse, SuccessfulResponse } from '${config.makeImport(
1440
+ "../http/response"
1441
+ )}';
1442
+ import type { OutputType, Parser, Type } from '${config.makeImport(
1443
+ "../http/send-request"
1444
+ )}';
1127
1445
 
1128
- export type Endpoints = {
1129
- [K in keyof typeof schemas]: {
1130
- input: z.infer<(typeof schemas)[K]['schema']>;
1131
- output: (typeof schemas)[K]['output'] extends [
1132
- infer Single extends OutputType,
1133
- ]
1134
- ? Output<Single>
1135
- : (typeof schemas)[K]['output'] extends readonly [
1136
- ...infer Tuple extends OutputType[],
1137
- ]
1138
- ? { [I in keyof Tuple]: Output<Tuple[I]> }[number]
1139
- : never;
1140
- error: ServerError | ParseError<(typeof schemas)[K]['schema']>;
1141
- };
1142
- };
1446
+ import schemas from '${config.makeImport("./schemas")}';
1143
1447
 
1144
- export default schemas;
1448
+ ${endpoints_default}`,
1449
+ [`${join("api", "schemas.ts")}`]: `${allSchemas.map((it) => it.import).join("\n")}
1145
1450
 
1451
+ export default {
1452
+ ${allSchemas.map((it) => it.use).join(",\n")}
1453
+ };
1146
1454
 
1147
1455
  `.trim(),
1148
1456
  ...Object.fromEntries(
@@ -1158,14 +1466,14 @@ export default schemas;
1158
1466
  );
1159
1467
  return [
1160
1468
  [
1161
- join("api", config.makeImport(spinalcase2(name))),
1469
+ join("api", `${spinalcase2(name)}.ts`),
1162
1470
  `${[
1163
1471
  ...imps,
1164
1472
  // ...imports,
1165
1473
  `import z from 'zod';`,
1166
1474
  `import { toRequest, json, urlencoded, nobody, formdata, createUrl } from '${config.makeImport("../http/request")}';`,
1167
1475
  `import { chunked, buffered } from "${config.makeImport("../http/parse-response")}";`,
1168
- `import * as ${camelcase2(name)} from '../inputs/${config.makeImport(spinalcase2(name))}';`
1476
+ `import * as ${camelcase3(name)} from '../inputs/${config.makeImport(spinalcase2(name))}';`
1169
1477
  ].join(
1170
1478
  "\n"
1171
1479
  )}
@@ -1226,22 +1534,22 @@ function bodyInputs(config, ctSchema) {
1226
1534
  }
1227
1535
 
1228
1536
  // packages/typescript/src/lib/http/interceptors.txt
1229
- var interceptors_default = "import { type RequestConfig } from './request.ts';\n\nexport interface Interceptor {\n before?: (config: RequestConfig) => Promise<RequestConfig>|RequestConfig;\n after?: (response: Response) => Promise<Response> | Response;\n}\n\nexport const createHeadersInterceptor = (\n defaultHeaders: () => Record<string, string | undefined>,\n requestHeaders: HeadersInit,\n):Interceptor => {\n return {\n before({init, url}) {\n // Priority Levels\n // 1. Headers Input\n // 2. Request Headers\n // 3. Default Headers\n const headers = defaultHeaders();\n\n for (const [key, value] of new Headers(requestHeaders)) {\n // Only set the header if it doesn't already exist and has a value\n // even though these headers are passed at operation level\n // still they are lower priority compared to the headers input\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n for (const [key, value] of Object.entries(headers)) {\n // Only set the header if it doesn't already exist and has a value\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n return {init, url};\n },\n };\n};\n\nexport const createBaseUrlInterceptor = (\n getBaseUrl: () => string,\n): Interceptor => {\n return {\n before({ init, url }) {\n const baseUrl = getBaseUrl();\n if (url.protocol === 'local:') {\n return {\n init,\n url: new URL(url.href.replace('local://', baseUrl))\n };\n }\n return { init, url };\n },\n };\n};\n\nexport const logInterceptor: Interceptor = {\n before({ url, init }) {\n console.dir('Request:', { url, init });\n return { url, init };\n },\n after(response) {\n console.log('Response:', response);\n return response;\n },\n};\n\n/**\n * Creates an interceptor that logs detailed information about requests and responses.\n * @param options Configuration options for the logger\n * @returns An interceptor object with before and after handlers\n */\nexport const createDetailedLogInterceptor = (options?: {\n logLevel?: 'debug' | 'info' | 'warn' | 'error';\n includeRequestBody?: boolean;\n includeResponseBody?: boolean;\n}) => {\n const logLevel = options?.logLevel || 'info';\n const includeRequestBody = options?.includeRequestBody || false;\n const includeResponseBody = options?.includeResponseBody || false;\n\n return {\n async before(request: Request) {\n const logData = {\n url: request.url,\n method: request.method,\n contentType: request.headers.get('Content-Type'),\n headers: Object.fromEntries([...request.headers.entries()]),\n };\n\n console[logLevel]('\u{1F680} Outgoing Request:', logData);\n\n if (includeRequestBody) {\n try {\n // Clone the request to avoid consuming the body stream\n const clonedRequest = request.clone();\n if (clonedRequest.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedRequest.json().catch(() => null);\n console[logLevel]('Request Body:', body);\n } else {\n const body = await clonedRequest.text().catch(() => null);\n console[logLevel]('Request Body:', body);\n }\n } catch (error) {\n console.error('Could not log request body:', error);\n }\n }\n\n return request;\n },\n\n async after(response: Response) {\n const logData = {\n status: response.status,\n statusText: response.statusText,\n url: response.url,\n headers: Object.fromEntries([...response.headers.entries()]),\n };\n\n console[logLevel]('\u{1F4E5} Incoming Response:', logData);\n\n if (includeResponseBody && response.body) {\n try {\n // Clone the response to avoid consuming the body stream\n const clonedResponse = response.clone();\n if (clonedResponse.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedResponse.json().catch(() => null);\n console[logLevel]('Response Body:', body);\n } else {\n const body = await clonedResponse.text().catch(() => null);\n if (body) {\n console[logLevel]('Response Body:', body.substring(0, 500) + (body.length > 500 ? '...' : ''));\n } else {\n console[logLevel]('No response body');\n }\n }\n } catch (error) {\n console.error('Could not log response body:', error);\n }\n }\n\n return response;\n },\n };\n};\n";
1537
+ var interceptors_default = "export interface Interceptor {\n before?: (config: RequestConfig) => Promise<RequestConfig> | RequestConfig;\n after?: (response: Response) => Promise<Response> | Response;\n}\n\nexport const createHeadersInterceptor = (\n defaultHeaders: () => Record<string, string | undefined>,\n requestHeaders: HeadersInit,\n):Interceptor => {\n return {\n before({init, url}) {\n // Priority Levels\n // 1. Headers Input\n // 2. Request Headers\n // 3. Default Headers\n const headers = defaultHeaders();\n\n for (const [key, value] of new Headers(requestHeaders)) {\n // Only set the header if it doesn't already exist and has a value\n // even though these headers are passed at operation level\n // still they are lower priority compared to the headers input\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n for (const [key, value] of Object.entries(headers)) {\n // Only set the header if it doesn't already exist and has a value\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n return {init, url};\n },\n };\n};\n\nexport const createBaseUrlInterceptor = (\n getBaseUrl: () => string,\n): Interceptor => {\n return {\n before({ init, url }) {\n const baseUrl = getBaseUrl();\n if (url.protocol === 'local:') {\n return {\n init,\n url: new URL(url.href.replace('local://', baseUrl))\n };\n }\n return { init, url };\n },\n };\n};\n\nexport const logInterceptor: Interceptor = {\n before({ url, init }) {\n console.dir('Request:', { url, init });\n return { url, init };\n },\n after(response) {\n console.log('Response:', response);\n return response;\n },\n};\n\n/**\n * Creates an interceptor that logs detailed information about requests and responses.\n * @param options Configuration options for the logger\n * @returns An interceptor object with before and after handlers\n */\nexport const createDetailedLogInterceptor = (options?: {\n logLevel?: 'debug' | 'info' | 'warn' | 'error';\n includeRequestBody?: boolean;\n includeResponseBody?: boolean;\n}) => {\n const logLevel = options?.logLevel || 'info';\n const includeRequestBody = options?.includeRequestBody || false;\n const includeResponseBody = options?.includeResponseBody || false;\n\n return {\n async before(request: Request) {\n const logData = {\n url: request.url,\n method: request.method,\n contentType: request.headers.get('Content-Type'),\n headers: Object.fromEntries([...request.headers.entries()]),\n };\n\n console[logLevel]('\u{1F680} Outgoing Request:', logData);\n\n if (includeRequestBody) {\n try {\n // Clone the request to avoid consuming the body stream\n const clonedRequest = request.clone();\n if (clonedRequest.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedRequest.json().catch(() => null);\n console[logLevel]('Request Body:', body);\n } else {\n const body = await clonedRequest.text().catch(() => null);\n console[logLevel]('Request Body:', body);\n }\n } catch (error) {\n console.error('Could not log request body:', error);\n }\n }\n\n return request;\n },\n\n async after(response: Response) {\n const logData = {\n status: response.status,\n statusText: response.statusText,\n url: response.url,\n headers: Object.fromEntries([...response.headers.entries()]),\n };\n\n console[logLevel]('\u{1F4E5} Incoming Response:', logData);\n\n if (includeResponseBody && response.body) {\n try {\n // Clone the response to avoid consuming the body stream\n const clonedResponse = response.clone();\n if (clonedResponse.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedResponse.json().catch(() => null);\n console[logLevel]('Response Body:', body);\n } else {\n const body = await clonedResponse.text().catch(() => null);\n if (body) {\n console[logLevel]('Response Body:', body.substring(0, 500) + (body.length > 500 ? '...' : ''));\n } else {\n console[logLevel]('No response body');\n }\n }\n } catch (error) {\n console.error('Could not log response body:', error);\n }\n }\n\n return response;\n },\n };\n};\n";
1230
1538
 
1231
1539
  // packages/typescript/src/lib/http/parse-response.txt
1232
- var parse_response_default = 'import { parse } from "fast-content-type-parse";\n\nexport async function handleError(response: Response) {\n try {\n if (response.status >= 400 && response.status < 500) {\n const body = (await response.json()) as Record<string, any>;\n return {\n status: response.status,\n body: body,\n };\n }\n return new Error(\n `An error occurred while fetching the data. Status: ${response.status}`,\n );\n } catch (error) {\n return error as any;\n }\n}\n\nasync function handleChunkedResponse(response: Response, contentType: string) {\n const { type } = parse(contentType);\n\n switch (type) {\n case "application/json": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return JSON.parse(buffer);\n }\n case "text/html":\n case "text/plain": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return buffer;\n }\n default:\n return response.body;\n }\n}\n\nexport function chunked(response: Response) {\n return response.body!;\n}\n\nexport async function buffered(response: Response) {\n const contentType = response.headers.get("Content-Type");\n if (!contentType) {\n throw new Error("Content-Type header is missing");\n }\n\n if (response.status === 204) {\n return null;\n }\n\n const { type } = parse(contentType);\n switch (type) {\n case "application/json":\n return response.json();\n case "text/plain":\n return response.text();\n case "text/html":\n return response.text();\n case "text/xml":\n case "application/xml":\n return response.text();\n case "application/x-www-form-urlencoded": {\n const text = await response.text();\n return Object.fromEntries(new URLSearchParams(text));\n }\n case "multipart/form-data":\n return response.formData();\n default:\n throw new Error(`Unsupported content type: ${contentType}`);\n }\n}\n';
1540
+ var parse_response_default = 'import { parse } from "fast-content-type-parse";\n\nasync function handleChunkedResponse(response: Response, contentType: string) {\n const { type } = parse(contentType);\n\n switch (type) {\n case "application/json": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return JSON.parse(buffer);\n }\n case "text/html":\n case "text/plain": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return buffer;\n }\n default:\n return response.body;\n }\n}\n\nexport function chunked(response: Response) {\n return response.body!;\n}\n\nexport async function buffered(response: Response) {\n const contentType = response.headers.get("Content-Type");\n if (!contentType) {\n throw new Error("Content-Type header is missing");\n }\n\n if (response.status === 204) {\n return null;\n }\n\n const { type } = parse(contentType);\n switch (type) {\n case "application/json":\n return response.json();\n case "text/plain":\n return response.text();\n case "text/html":\n return response.text();\n case "text/xml":\n case "application/xml":\n return response.text();\n case "application/x-www-form-urlencoded": {\n const text = await response.text();\n return Object.fromEntries(new URLSearchParams(text));\n }\n case "multipart/form-data":\n return response.formData();\n default:\n throw new Error(`Unsupported content type: ${contentType}`);\n }\n}\n';
1233
1541
 
1234
1542
  // packages/typescript/src/lib/http/parser.txt
1235
- var parser_default = 'import { z } from "zod";\n\nexport class ParseError<T extends z.ZodType<any, any, any>> {\n public data: z.typeToFlattenedError<T, z.ZodIssue>;\n constructor(data: z.typeToFlattenedError<T, z.ZodIssue>) {\n this.data = data;\n }\n}\n\nexport function parse<T extends z.ZodType<any, any, any>>(schema: T, input: unknown) {\n const result = schema.safeParse(input);\n if (!result.success) {\n const error = result.error.flatten((issue) => issue);\n return [null, new ParseError(error)];\n }\n return [result.data as z.infer<T>, null];\n}\n';
1543
+ var parser_default = "import { z } from 'zod';\n\nexport class ParseError<T extends z.ZodType<any, any, any>> {\n public data: z.typeToFlattenedError<T, z.ZodIssue>;\n constructor(data: z.typeToFlattenedError<T, z.ZodIssue>) {\n this.data = data;\n }\n}\n\nexport function parseInput<T extends z.ZodType<any, any, any>>(\n schema: T,\n input: unknown,\n) {\n const result = schema.safeParse(input);\n if (!result.success) {\n const error = result.error.flatten((issue) => issue);\n return [null, new ParseError(error)];\n }\n return [result.data as z.infer<T>, null];\n}\n";
1236
1544
 
1237
1545
  // packages/typescript/src/lib/http/request.txt
1238
- var request_default = "type Init = Omit<RequestInit, 'headers'> & { headers: Headers; };\nexport type RequestConfig = { init: Init; url: URL };\nexport type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\nexport type ContentType = 'xml' | 'json' | 'urlencoded' | 'multipart' | 'formdata';\nexport type Endpoint =\n | `${ContentType} ${Method} ${string}`\n | `${Method} ${string}`;\n\nexport type BodyInit =\n | ArrayBuffer\n | Blob\n | FormData\n | URLSearchParams\n | null\n | string;\n\nexport function createUrl(path: string, query: URLSearchParams) {\n const url = new URL(path, `local://`);\n url.search = query.toString();\n return url;\n}\n\nfunction template(\n templateString: string,\n templateVariables: Record<string, any>,\n): string {\n const nargs = /{([0-9a-zA-Z_]+)}/g;\n return templateString.replace(nargs, (match, key: string, index: number) => {\n // Handle escaped double braces\n if (\n templateString[index - 1] === '{' &&\n templateString[index + match.length] === '}'\n ) {\n return key;\n }\n\n const result = key in templateVariables ? templateVariables[key] : null;\n return result === null || result === undefined ? '' : String(result);\n });\n}\n\ntype Input = Record<string, any>;\ntype Props = {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n};\n\nabstract class Serializer {\n protected input: Input;\n protected props: Props;\n\n constructor(\n input: Input,\n props: Props,\n ) {\n this.input = input;\n this.props = props;\n }\n\n abstract getBody(): BodyInit | null;\n abstract getHeaders(): Record<string, string>;\n serialize(): Serialized {\n const headers = new Headers({});\n for (const header of this.props.inputHeaders) {\n headers.set(header, this.input[header]);\n }\n\n const query = new URLSearchParams();\n for (const key of this.props.inputQuery) {\n const value = this.input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = this.props.inputParams.reduce<Record<string, any>>(\n (acc, key) => {\n acc[key] = this.input[key];\n return acc;\n },\n {},\n );\n\n return {\n body: this.getBody(),\n query,\n params,\n headers: this.getHeaders(),\n };\n }\n}\n\ninterface Serialized {\n body: BodyInit | null;\n query: URLSearchParams;\n params: Record<string, any>;\n headers: Record<string, string>;\n}\n\nclass JsonSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body: Record<string, any> = {};\n for (const prop of this.props.inputBody) {\n body[prop] = this.input[prop];\n }\n return JSON.stringify(body);\n }\n getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n}\n\nclass UrlencodedSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new URLSearchParams();\n for (const prop of this.props.inputBody) {\n body.set(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass NoBodySerializer extends Serializer {\n getBody(): BodyInit | null {\n return null;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass FormDataSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new FormData();\n for (const prop of this.props.inputBody) {\n body.append(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nexport function json(input: Input, props: Props) {\n return new JsonSerializer(input, props).serialize();\n}\nexport function urlencoded(input: Input, props: Props) {\n return new UrlencodedSerializer(input, props).serialize();\n}\nexport function nobody(input: Input, props: Props) {\n return new NoBodySerializer(input, props).serialize();\n}\nexport function formdata(input: Input, props: Props) {\n return new FormDataSerializer(input, props).serialize();\n}\n\nexport function toRequest<T extends Endpoint>(\n endpoint: T,\n input: Serialized,\n): RequestConfig {\n const [method, path] = endpoint.split(' ');\n const pathVariable = template(path, input.params);\n\n return {\n url: createUrl(pathVariable, input.query),\n init: {\n method: method,\n headers: new Headers(input.headers),\n body: method === 'GET' ? undefined : input.body,\n },\n }\n}\n";
1546
+ var request_default = "type Init = Omit<RequestInit, 'headers'> & { headers: Headers; };\nexport type RequestConfig = { init: Init; url: URL };\nexport type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\nexport type ContentType = 'xml' | 'json' | 'urlencoded' | 'multipart' | 'formdata';\nexport type Endpoint =\n | `${ContentType} ${Method} ${string}`\n | `${Method} ${string}`;\n\nexport type BodyInit =\n | ArrayBuffer\n | Blob\n | FormData\n | URLSearchParams\n | null\n | string;\n\nexport function createUrl(path: string, query: URLSearchParams) {\n const url = new URL(path, `local://`);\n url.search = query.toString();\n return url;\n}\n\nfunction template(\n templateString: string,\n templateVariables: Record<string, any>,\n): string {\n const nargs = /{([0-9a-zA-Z_]+)}/g;\n return templateString.replace(nargs, (match, key: string, index: number) => {\n // Handle escaped double braces\n if (\n templateString[index - 1] === '{' &&\n templateString[index + match.length] === '}'\n ) {\n return key;\n }\n\n const result = key in templateVariables ? templateVariables[key] : null;\n return result === null || result === undefined ? '' : String(result);\n });\n}\n\ntype Input = Record<string, any>;\ntype Props = {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n};\n\nabstract class Serializer {\n protected input: Input;\n protected props: Props;\n\n constructor(\n input: Input,\n props: Props,\n ) {\n this.input = input;\n this.props = props;\n }\n\n abstract getBody(): BodyInit | null;\n abstract getHeaders(): Record<string, string>;\n serialize(): Serialized {\n const headers = new Headers({});\n for (const header of this.props.inputHeaders) {\n headers.set(header, this.input[header]);\n }\n\n const query = new URLSearchParams();\n for (const key of this.props.inputQuery) {\n const value = this.input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = this.props.inputParams.reduce<Record<string, any>>(\n (acc, key) => {\n acc[key] = this.input[key];\n return acc;\n },\n {},\n );\n\n return {\n body: this.getBody(),\n query,\n params,\n headers: this.getHeaders(),\n };\n }\n}\n\ninterface Serialized {\n body: BodyInit | null;\n query: URLSearchParams;\n params: Record<string, any>;\n headers: Record<string, string>;\n}\n\nclass JsonSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body: Record<string, any> = {};\n if (\n this.props.inputBody.length === 1 &&\n this.props.inputBody[0] === '$body'\n ) {\n return JSON.stringify(this.input.$body);\n }\n\n for (const prop of this.props.inputBody) {\n body[prop] = this.input[prop];\n }\n return JSON.stringify(body);\n }\n getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n}\n\nclass UrlencodedSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new URLSearchParams();\n for (const prop of this.props.inputBody) {\n body.set(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n };\n }\n}\n\nclass NoBodySerializer extends Serializer {\n getBody(): BodyInit | null {\n return null;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass FormDataSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new FormData();\n for (const prop of this.props.inputBody) {\n body.append(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {\n Accept: 'application/json',\n };\n }\n}\n\nexport function json(input: Input, props: Props) {\n return new JsonSerializer(input, props).serialize();\n}\nexport function urlencoded(input: Input, props: Props) {\n return new UrlencodedSerializer(input, props).serialize();\n}\nexport function nobody(input: Input, props: Props) {\n return new NoBodySerializer(input, props).serialize();\n}\nexport function formdata(input: Input, props: Props) {\n return new FormDataSerializer(input, props).serialize();\n}\n\nexport function toRequest<T extends Endpoint>(\n endpoint: T,\n input: Serialized,\n): RequestConfig {\n const [method, path] = endpoint.split(' ');\n const pathVariable = template(path, input.params);\n\n return {\n url: createUrl(pathVariable, input.query),\n init: {\n method: method,\n headers: new Headers(input.headers),\n body: method === 'GET' ? undefined : input.body,\n },\n }\n}\n";
1239
1547
 
1240
1548
  // packages/typescript/src/lib/http/response.txt
1241
- var response_default = "export class APIResponse<Body = unknown, Status extends number = number> {\n static status: number;\n status: Status;\n data: Body;\n\n constructor(status: Status, data: Body) {\n this.status = status;\n this.data = data;\n }\n}\n\nexport class APIError<Body, Status extends number = number> extends APIResponse<\n Body,\n Status\n> {}\n\n// 2xx Success\nexport class Ok<T> extends APIResponse<T, 200> {\n static status = 200;\n}\nexport class Created<T> extends APIResponse<T, 201> {}\nexport class Accepted<T> extends APIResponse<T, 202> {}\nexport class NoContent extends APIResponse<null, 204> {}\n\n// 4xx Client Errors\nexport class BadRequest<T> extends APIError<T, 400> {}\nexport class Unauthorized<T = { message: string }> extends APIError<T, 401> {}\nexport class PaymentRequired<T = { message: string }> extends APIError<\n T,\n 402\n> {}\nexport class Forbidden<T = { message: string }> extends APIError<T, 403> {}\nexport class NotFound<T = { message: string }> extends APIError<T, 404> {}\nexport class MethodNotAllowed<T = { message: string }> extends APIError<\n T,\n 405\n> {}\nexport class NotAcceptable<T = { message: string }> extends APIError<T, 406> {}\nexport class Conflict<T = { message: string }> extends APIError<T, 409> {}\nexport class Gone<T = { message: string }> extends APIError<T, 410> {}\nexport class UnprocessableEntity<\n T = { message: string; errors?: Record<string, string[]> },\n> extends APIError<T, 422> {}\nexport class TooManyRequests<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 429> {}\nexport class PayloadTooLarge<T = { message: string }> extends APIError<\n T,\n 413\n> {}\nexport class UnsupportedMediaType<T = { message: string }> extends APIError<\n T,\n 415\n> {}\n\n// 5xx Server Errors\nexport class InternalServerError<T = { message: string }> extends APIError<\n T,\n 500\n> {}\nexport class NotImplemented<T = { message: string }> extends APIError<T, 501> {}\nexport class BadGateway<T = { message: string }> extends APIError<T, 502> {}\nexport class ServiceUnavailable<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 503> {}\nexport class GatewayTimeout<T = { message: string }> extends APIError<T, 504> {}\n\nexport type ClientError =\n | BadRequest<{ message: string }>\n | Unauthorized\n | PaymentRequired\n | Forbidden\n | NotFound\n | MethodNotAllowed\n | NotAcceptable\n | Conflict\n | Gone\n | UnprocessableEntity\n | TooManyRequests;\n\nexport type ServerError =\n | InternalServerError\n | NotImplemented\n | BadGateway\n | ServiceUnavailable\n | GatewayTimeout;\n\nexport type ProblematicResponse = ClientError | ServerError;\n";
1549
+ var response_default = "export class APIResponse<Body = unknown, Status extends number = number> {\n static status: number;\n status: Status;\n data: Body;\n\n constructor(status: Status, data: Body) {\n this.status = status;\n this.data = data;\n }\n\n static create<Body = unknown>(status: number, data: Body) {\n return new this(status, data);\n }\n}\n\nexport class APIError<Body, Status extends number = number> extends APIResponse<\n Body,\n Status\n> {\n static override create<T>(status: number, data: T) {\n return new this(status, data);\n }\n}\n\n// 2xx Success\nexport class Ok<T> extends APIResponse<T, 200> {\n static override status = 200 as const;\n constructor(data: T) {\n super(Ok.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class Created<T> extends APIResponse<T, 201> {\n static override status = 201 as const;\n constructor(data: T) {\n super(Created.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class Accepted<T> extends APIResponse<T, 202> {\n static override status = 202 as const;\n constructor(data: T) {\n super(Accepted.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class NoContent extends APIResponse<never, 204> {\n static override status = 204 as const;\n constructor() {\n super(NoContent.status, null as never);\n }\n static override create(status: number, data: never): NoContent {\n return new this();\n }\n}\n\n// 4xx Client Errors\nexport class BadRequest<T> extends APIError<T, 400> {\n static override status = 400 as const;\n constructor(data: T) {\n super(BadRequest.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class Unauthorized<T = { message: string }> extends APIError<T, 401> {\n static override status = 401 as const;\n constructor(data: T) {\n super(Unauthorized.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class PaymentRequired<T = { message: string }> extends APIError<T, 402> {\n static override status = 402 as const;\n constructor(data: T) {\n super(PaymentRequired.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class Forbidden<T = { message: string }> extends APIError<T, 403> {\n static override status = 403 as const;\n constructor(data: T) {\n super(Forbidden.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class NotFound<T = { message: string }> extends APIError<T, 404> {\n static override status = 404 as const;\n constructor(data: T) {\n super(NotFound.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class MethodNotAllowed<T = { message: string }> extends APIError<\n T,\n 405\n> {\n static override status = 405 as const;\n constructor(data: T) {\n super(MethodNotAllowed.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class NotAcceptable<T = { message: string }> extends APIError<T, 406> {\n static override status = 406 as const;\n constructor(data: T) {\n super(NotAcceptable.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class Conflict<T = { message: string }> extends APIError<T, 409> {\n static override status = 409 as const;\n constructor(data: T) {\n super(Conflict.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class Gone<T = { message: string }> extends APIError<T, 410> {\n static override status = 410 as const;\n constructor(data: T) {\n super(Gone.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class UnprocessableEntity<\n T = { message: string; errors?: Record<string, string[]> },\n> extends APIError<T, 422> {\n static override status = 422 as const;\n constructor(data: T) {\n super(UnprocessableEntity.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class TooManyRequests<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 429> {\n static override status = 429 as const;\n constructor(data: T) {\n super(TooManyRequests.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class PayloadTooLarge<T = { message: string }> extends APIError<T, 413> {\n static override status = 413 as const;\n constructor(data: T) {\n super(PayloadTooLarge.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class UnsupportedMediaType<T = { message: string }> extends APIError<\n T,\n 415\n> {\n static override status = 415 as const;\n constructor(data: T) {\n super(UnsupportedMediaType.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\n\n// 5xx Server Errors\nexport class InternalServerError<T = { message: string }> extends APIError<\n T,\n 500\n> {\n static override status = 500 as const;\n constructor(data: T) {\n super(InternalServerError.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class NotImplemented<T = { message: string }> extends APIError<T, 501> {\n static override status = 501 as const;\n constructor(data: T) {\n super(NotImplemented.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class BadGateway<T = { message: string }> extends APIError<T, 502> {\n static override status = 502 as const;\n constructor(data: T) {\n super(BadGateway.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class ServiceUnavailable<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 503> {\n static override status = 503 as const;\n constructor(data: T) {\n super(ServiceUnavailable.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\nexport class GatewayTimeout<T = { message: string }> extends APIError<T, 504> {\n static override status = 504 as const;\n constructor(data: T) {\n super(GatewayTimeout.status, data);\n }\n static override create<T>(status: number, data: T) {\n return new this(data);\n }\n}\n\nexport type ClientError =\n | BadRequest<{ message: string }>\n | Unauthorized<unknown>\n | PaymentRequired<unknown>\n | Forbidden<unknown>\n | NotFound<unknown>\n | MethodNotAllowed<unknown>\n | NotAcceptable<unknown>\n | Conflict<unknown>\n | Gone<unknown>\n | UnprocessableEntity<unknown>\n | TooManyRequests<unknown>;\n\nexport type ServerError =\n | InternalServerError<unknown>\n | NotImplemented<unknown>\n | BadGateway<unknown>\n | ServiceUnavailable<unknown>\n | GatewayTimeout<unknown>;\n\nexport type ProblematicResponse = ClientError | ServerError;\n\nexport type SuccessfulResponse = Ok<unknown> | Created<unknown> | Accepted<unknown> | NoContent;";
1242
1550
 
1243
1551
  // packages/typescript/src/lib/http/send-request.txt
1244
- var send_request_default = "export interface Type<T> {\n new (...args: any[]): T;\n}\nexport type Parser = (\n response: Response,\n) => Promise<unknown> | ReadableStream<any>;\nexport type OutputType =\n | Type<APIResponse>\n | { parser: Parser; type: Type<APIResponse> };\ntype Constructor<T> = new (...args: any[]) => T;\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (input: any) => RequestConfig;\n output: OutputType[];\n}\n\nexport const fetchType = z\n .function()\n .args(z.instanceof(Request))\n .returns(z.promise(z.instanceof(Response)))\n .optional();\n\nexport async function sendRequest(\n input: unknown,\n route: RequestSchema,\n options: {\n fetch?: z.infer<typeof fetchType>;\n interceptors?: Interceptor[];\n signal?: AbortSignal;\n },\n) {\n const { interceptors = [] } = options;\n const [parsedInput, parseError] = parse(route.schema, input);\n if (parseError) {\n return [null as never, { ...parseError, kind: 'parse' } as never] as const;\n }\n\n let config = route.toRequest(parsedInput as never);\n for (const interceptor of interceptors) {\n if (interceptor.before) {\n config = await interceptor.before(config);\n }\n }\n\n let response = await (options.fetch ?? fetch)(\n new Request(config.url, config.init),\n {\n ...config.init,\n signal: options.signal,\n },\n );\n\n for (let i = interceptors.length - 1; i >= 0; i--) {\n const interceptor = interceptors[i];\n if (interceptor.after) {\n response = await interceptor.after(response.clone());\n }\n }\n\n let output: Constructor<APIResponse> | null = APIResponse;\n let parser: Parser = buffered;\n for (const outputType of route.output) {\n if ('parser' in outputType) {\n parser = outputType.parser;\n if (isTypeOf(outputType.type, APIResponse)) {\n if (response.status === outputType.type.status) {\n output = outputType.type;\n break;\n }\n }\n } else if (isTypeOf(outputType, APIResponse)) {\n if (response.status === outputType.status) {\n output = outputType;\n break;\n }\n }\n }\n\n const data = new output(response.status, await parser(response));\n if (response.ok) {\n return [data as never, null] as const;\n }\n return [null as never, data as never] as const;\n}\n\nexport function isTypeOf<T extends Type<APIResponse>>(\n instance: any,\n baseType: T,\n): instance is T {\n if (instance === baseType) {\n return true;\n }\n const prototype = Object.getPrototypeOf(instance);\n if (prototype === null) {\n return false;\n }\n return isTypeOf(prototype, baseType);\n}\n";
1552
+ var send_request_default = "export interface Type<T> {\n new (...args: any[]): T;\n}\nexport type Parser = (\n response: Response,\n) => Promise<unknown> | ReadableStream<any>;\nexport type OutputType =\n | Type<APIResponse>\n | { parser: Parser; type: Type<APIResponse> };\n\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (input: any) => RequestConfig;\n output: OutputType[];\n}\n\nexport const fetchType = z\n .function()\n .args(z.instanceof(Request))\n .returns(z.promise(z.instanceof(Response)))\n .optional();\n\nexport async function sendRequest(\n input: unknown,\n route: RequestSchema,\n options: {\n fetch?: z.infer<typeof fetchType>;\n interceptors?: Interceptor[];\n signal?: AbortSignal;\n },\n) {\n const { interceptors = [] } = options;\n const [parsedInput, parseError] = parseInput(route.schema, input);\n if (parseError) {\n <% if(throwError) { %>\n throw parseError;\n <% } else { %>\n return [null as never, parseError as never] as const;\n }\n <% } %>\n\n let config = route.toRequest(parsedInput as never);\n for (const interceptor of interceptors) {\n if (interceptor.before) {\n config = await interceptor.before(config);\n }\n }\n\n let response = await (options.fetch ?? fetch)(\n new Request(config.url, config.init),\n {\n ...config.init,\n signal: options.signal,\n },\n );\n\n for (let i = interceptors.length - 1; i >= 0; i--) {\n const interceptor = interceptors[i];\n if (interceptor.after) {\n response = await interceptor.after(response.clone());\n }\n }\n return await parse(route, response);\n}\n\nexport async function parse(route: RequestSchema, response: Response) {\n let output: typeof APIResponse | null = null;\n let parser: Parser = buffered;\n for (const outputType of route.output) {\n if ('parser' in outputType) {\n parser = outputType.parser;\n if (isTypeOf(outputType.type, APIResponse)) {\n if (response.status === outputType.type.status) {\n output = outputType.type;\n break;\n }\n }\n } else if (isTypeOf(outputType, APIResponse)) {\n if (response.status === outputType.status) {\n output = outputType;\n break;\n }\n }\n }\n\n if (response.ok) {\n const data = (output || APIResponse).create(\n response.status,\n await parser(response),\n );\n <% if(throwError) { %>\n return data;\n <% } else { %>\n return [data as never, null] as const;\n <% } %>\n }\n<% if(throwError) { %>\n throw (output || APIError).create(\n response.status,\n await parser(response),\n );\n<% } else { %>\n const data = (output || APIError).create(\n response.status,\n await parser(response),\n );\n return [null as never, data as never] as const;\n<% } %>\n}\n\nexport function isTypeOf<T extends Type<APIResponse>>(\n instance: any,\n baseType: T,\n): instance is T {\n if (instance === baseType) {\n return true;\n }\n const prototype = Object.getPrototypeOf(instance);\n if (prototype === null) {\n return false;\n }\n return isTypeOf(prototype, baseType);\n}\n";
1245
1553
 
1246
1554
  // packages/typescript/src/lib/generate.ts
1247
1555
  function security(spec) {
@@ -1269,11 +1577,13 @@ async function generate(spec, settings) {
1269
1577
  const makeImport = (moduleSpecifier) => {
1270
1578
  return settings.useTsExtension ? `${moduleSpecifier}.ts` : moduleSpecifier;
1271
1579
  };
1272
- const { commonSchemas, endpoints, groups, outputs, commonZod, clientFiles } = generateCode({
1273
- spec,
1274
- style: "github",
1275
- makeImport
1276
- });
1580
+ const { commonSchemas, endpoints, groups, outputs, commonZod } = generateCode(
1581
+ {
1582
+ spec,
1583
+ style: "github",
1584
+ makeImport
1585
+ }
1586
+ );
1277
1587
  const output = settings.mode === "full" ? join2(settings.output, "src") : settings.output;
1278
1588
  const options = security(spec);
1279
1589
  const clientName = settings.name || "Client";
@@ -1285,16 +1595,18 @@ async function generate(spec, settings) {
1285
1595
  // 'README.md': readme,
1286
1596
  });
1287
1597
  await writeFiles(join2(output, "http"), {
1288
- "interceptors.ts": interceptors_default,
1598
+ "interceptors.ts": `
1599
+ import { type RequestConfig } from './${makeImport("request")}';
1600
+ ${interceptors_default}`,
1289
1601
  "parse-response.ts": parse_response_default,
1290
1602
  "send-request.ts": `import z from 'zod';
1291
1603
  import type { Interceptor } from './${makeImport("interceptors")}';
1292
1604
  import { buffered } from './${makeImport("parse-response")}';
1293
- import { parse } from './${makeImport("parser")}';
1605
+ import { parseInput } from './${makeImport("parser")}';
1294
1606
  import type { RequestConfig } from './${makeImport("request")}';
1295
- import { APIResponse } from './${makeImport("response")}';
1607
+ import { APIError, APIResponse } from './${makeImport("response")}';
1296
1608
 
1297
- ${send_request_default}`,
1609
+ ${template(send_request_default, {})({ throwError: settings.throwError })}`,
1298
1610
  "response.ts": response_default,
1299
1611
  "parser.ts": parser_default,
1300
1612
  "request.ts": request_default
@@ -1302,13 +1614,15 @@ ${send_request_default}`,
1302
1614
  await writeFiles(join2(output, "outputs"), outputs);
1303
1615
  const modelsImports = Object.entries(commonSchemas).map(([name]) => name);
1304
1616
  await writeFiles(output, {
1305
- "client.ts": client_default({
1306
- name: clientName,
1307
- servers: (spec.servers ?? []).map((server) => server.url) || [],
1308
- options,
1309
- makeImport
1310
- }),
1311
- ...clientFiles,
1617
+ "client.ts": client_default(
1618
+ {
1619
+ name: clientName,
1620
+ servers: (spec.servers ?? []).map((server) => server.url) || [],
1621
+ options,
1622
+ makeImport
1623
+ },
1624
+ settings.throwError ?? false
1625
+ ),
1312
1626
  ...inputFiles,
1313
1627
  ...endpoints,
1314
1628
  ...Object.fromEntries(