@sdk-it/typescript 0.16.0 → 0.17.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
@@ -28,15 +28,18 @@ var client_default = (spec) => {
28
28
  }
29
29
  };
30
30
  return `
31
- import { fetchType, sendRequest } from './http/${spec.makeImport("send-request")}';
31
+ import type { RequestConfig } from './http/${spec.makeImport("request")}';
32
+ import { fetchType, sendRequest, parse } from './http/${spec.makeImport("send-request")}';
32
33
  import z from 'zod';
33
- import type { Endpoints } from './api/${spec.makeImport("schemas")}';
34
+ import type { Endpoints } from './api/${spec.makeImport("endpoints")}';
34
35
  import schemas from './api/${spec.makeImport("schemas")}';
35
36
  import {
36
37
  createBaseUrlInterceptor,
37
38
  createHeadersInterceptor,
38
39
  } from './http/${spec.makeImport("interceptors")}';
39
40
 
41
+ import { parseInput, type ParseError } from './http/${spec.makeImport("parser")}';
42
+
40
43
  ${spec.servers.length ? `export const servers = ${JSON.stringify(spec.servers, null, 2)} as const` : ""}
41
44
  const optionsSchema = z.object(${toLitObject(specOptions, (x) => x.schema)});
42
45
  ${spec.servers.length ? `export type Servers = typeof servers[number];` : ""}
@@ -65,6 +68,44 @@ export class ${spec.name} {
65
68
  });
66
69
  }
67
70
 
71
+ async prepare<E extends keyof Endpoints>(
72
+ endpoint: E,
73
+ input: Endpoints[E]['input'],
74
+ options?: { headers?: HeadersInit },
75
+ ): Promise<
76
+ readonly [
77
+ RequestConfig & {
78
+ parse: (response: Response) => ReturnType<typeof parse>;
79
+ },
80
+ ParseError<(typeof schemas)[E]['schema']> | null,
81
+ ]
82
+ > {
83
+ const route = schemas[endpoint];
84
+
85
+ const interceptors = [
86
+ createHeadersInterceptor(
87
+ () => this.defaultHeaders,
88
+ options?.headers ?? {},
89
+ ),
90
+ createBaseUrlInterceptor(() => this.options.baseUrl),
91
+ ];
92
+ const [parsedInput, parseError] = parseInput(route.schema, input);
93
+ if (parseError) {
94
+ return [null as never, parseError as never] as const;
95
+ }
96
+
97
+ let config = route.toRequest(parsedInput as never);
98
+ for (const interceptor of interceptors) {
99
+ if (interceptor.before) {
100
+ config = await interceptor.before(config);
101
+ }
102
+ }
103
+ return [
104
+ { ...config, parse: (response: Response) => parse(route, response) },
105
+ null as never,
106
+ ] as const;
107
+ }
108
+
68
109
  get defaultHeaders() {
69
110
  return ${defaultHeaders}
70
111
  }
@@ -86,10 +127,272 @@ export class ${spec.name} {
86
127
  };
87
128
 
88
129
  // packages/typescript/src/lib/generator.ts
89
- import { get as get2, merge } from "lodash-es";
130
+ import { merge } from "lodash-es";
90
131
  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";
132
+ import { camelcase as camelcase3, pascalcase as pascalcase2, spinalcase as spinalcase2 } from "stringcase";
133
+ import { followRef as followRef4, isEmpty, isRef as isRef5 } from "@sdk-it/core";
134
+
135
+ // packages/spec/dist/lib/operation.js
136
+ import { camelcase } from "stringcase";
137
+ var defaults = {
138
+ operationId: (operation, path, method) => {
139
+ if (operation.operationId) {
140
+ return camelcase(operation.operationId);
141
+ }
142
+ const metadata = operation["x-oaiMeta"];
143
+ if (metadata && metadata.name) {
144
+ return camelcase(metadata.name);
145
+ }
146
+ return camelcase(
147
+ [method, ...path.replace(/[\\/\\{\\}]/g, " ").split(" ")].filter(Boolean).join(" ").trim()
148
+ );
149
+ },
150
+ tag: (operation, path) => {
151
+ return operation.tags?.[0] || determineGenericTag(path, operation);
152
+ }
153
+ };
154
+ function forEachOperation(config, callback) {
155
+ const result = [];
156
+ for (const [path, pathItem] of Object.entries(config.spec.paths ?? {})) {
157
+ const { parameters = [], ...methods2 } = pathItem;
158
+ const fixedPath = path.replace(/:([^/]+)/g, "{$1}");
159
+ for (const [method, operation] of Object.entries(methods2)) {
160
+ const formatOperationId = config.operationId ?? defaults.operationId;
161
+ const formatTag = config.tag ?? defaults.tag;
162
+ const operationName = formatOperationId(operation, fixedPath, method);
163
+ const operationTag = formatTag(operation, fixedPath);
164
+ const metadata = operation["x-oaiMeta"] ?? {};
165
+ result.push(
166
+ callback(
167
+ {
168
+ name: metadata.name,
169
+ method,
170
+ path: fixedPath,
171
+ groupName: operationTag,
172
+ tag: operationTag
173
+ },
174
+ {
175
+ ...operation,
176
+ parameters: [...parameters, ...operation.parameters ?? []],
177
+ operationId: operationName
178
+ }
179
+ )
180
+ );
181
+ }
182
+ }
183
+ return result;
184
+ }
185
+ var reservedKeywords = /* @__PURE__ */ new Set([
186
+ "abstract",
187
+ "arguments",
188
+ "await",
189
+ "boolean",
190
+ "break",
191
+ "byte",
192
+ "case",
193
+ "catch",
194
+ "char",
195
+ "class",
196
+ "const",
197
+ "continue",
198
+ "debugger",
199
+ "default",
200
+ "delete",
201
+ "do",
202
+ "double",
203
+ "else",
204
+ "enum",
205
+ "eval",
206
+ "export",
207
+ "extends",
208
+ "false",
209
+ "final",
210
+ "finally",
211
+ "float",
212
+ "for",
213
+ "function",
214
+ "goto",
215
+ "if",
216
+ "implements",
217
+ "import",
218
+ "in",
219
+ "instanceof",
220
+ "int",
221
+ "interface",
222
+ "let",
223
+ "long",
224
+ "native",
225
+ "new",
226
+ "null",
227
+ "package",
228
+ "private",
229
+ "protected",
230
+ "public",
231
+ "return",
232
+ "short",
233
+ "static",
234
+ "super",
235
+ "switch",
236
+ "synchronized",
237
+ "this",
238
+ "throw",
239
+ "throws",
240
+ "transient",
241
+ "true",
242
+ "try",
243
+ "typeof",
244
+ "var",
245
+ "void",
246
+ "volatile",
247
+ "while",
248
+ "with",
249
+ "yield",
250
+ // Potentially problematic identifiers / Common Verbs used as tags
251
+ "object",
252
+ "string",
253
+ "number",
254
+ "any",
255
+ "unknown",
256
+ "never",
257
+ "get",
258
+ "list",
259
+ "create",
260
+ "update",
261
+ "delete",
262
+ "post",
263
+ "put",
264
+ "patch",
265
+ "do",
266
+ "send",
267
+ "add",
268
+ "remove",
269
+ "set",
270
+ "find",
271
+ "search",
272
+ "check",
273
+ "make"
274
+ // Added make, check
275
+ ]);
276
+ function sanitizeTag(camelCasedTag) {
277
+ if (/^\d/.test(camelCasedTag)) {
278
+ return `_${camelCasedTag}`;
279
+ }
280
+ return reservedKeywords.has(camelCasedTag) ? `${camelCasedTag}_` : camelCasedTag;
281
+ }
282
+ function determineGenericTag(pathString, operation) {
283
+ const operationId = operation.operationId || "";
284
+ const VERSION_REGEX = /^[vV]\d+$/;
285
+ const commonVerbs = /* @__PURE__ */ new Set([
286
+ // Verbs to potentially strip from operationId prefix
287
+ "get",
288
+ "list",
289
+ "create",
290
+ "update",
291
+ "delete",
292
+ "post",
293
+ "put",
294
+ "patch",
295
+ "do",
296
+ "send",
297
+ "add",
298
+ "remove",
299
+ "set",
300
+ "find",
301
+ "search",
302
+ "check",
303
+ "make"
304
+ // Added make
305
+ ]);
306
+ const segments = pathString.split("/").filter(Boolean);
307
+ const potentialCandidates = segments.filter(
308
+ (segment) => segment && !segment.startsWith("{") && !segment.endsWith("}") && !VERSION_REGEX.test(segment)
309
+ );
310
+ for (let i = potentialCandidates.length - 1; i >= 0; i--) {
311
+ const segment = potentialCandidates[i];
312
+ if (!segment.startsWith("@")) {
313
+ return sanitizeTag(camelcase(segment));
314
+ }
315
+ }
316
+ const canFallbackToPathSegment = potentialCandidates.length > 0;
317
+ if (operationId) {
318
+ const lowerOpId = operationId.toLowerCase();
319
+ 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]+/);
320
+ const validParts = parts.filter(Boolean);
321
+ if (commonVerbs.has(lowerOpId) && validParts.length === 1 && canFallbackToPathSegment) {
322
+ } else if (validParts.length > 0) {
323
+ const firstPart = validParts[0];
324
+ const isFirstPartVerb = commonVerbs.has(firstPart);
325
+ if (isFirstPartVerb && validParts.length > 1) {
326
+ const verbPrefixLength = firstPart.length;
327
+ let nextPartStartIndex = -1;
328
+ if (operationId.length > verbPrefixLength) {
329
+ const charAfterPrefix = operationId[verbPrefixLength];
330
+ if (charAfterPrefix >= "A" && charAfterPrefix <= "Z") {
331
+ nextPartStartIndex = verbPrefixLength;
332
+ } else if (charAfterPrefix >= "0" && charAfterPrefix <= "9") {
333
+ nextPartStartIndex = verbPrefixLength;
334
+ } else if (["_", "-"].includes(charAfterPrefix)) {
335
+ nextPartStartIndex = verbPrefixLength + 1;
336
+ } else {
337
+ const match = operationId.substring(verbPrefixLength).match(/[A-Z0-9]/);
338
+ if (match && match.index !== void 0) {
339
+ nextPartStartIndex = verbPrefixLength + match.index;
340
+ }
341
+ if (nextPartStartIndex === -1 && operationId.length > verbPrefixLength) {
342
+ nextPartStartIndex = verbPrefixLength;
343
+ }
344
+ }
345
+ }
346
+ if (nextPartStartIndex !== -1 && nextPartStartIndex < operationId.length) {
347
+ const remainingOriginalSubstring = operationId.substring(nextPartStartIndex);
348
+ const potentialTag = camelcase(remainingOriginalSubstring);
349
+ if (potentialTag) {
350
+ return sanitizeTag(potentialTag);
351
+ }
352
+ }
353
+ const potentialTagJoined = camelcase(validParts.slice(1).join("_"));
354
+ if (potentialTagJoined) {
355
+ return sanitizeTag(potentialTagJoined);
356
+ }
357
+ }
358
+ const potentialTagFull = camelcase(operationId);
359
+ if (potentialTagFull) {
360
+ const isResultSingleVerb = validParts.length === 1 && isFirstPartVerb;
361
+ if (!(isResultSingleVerb && canFallbackToPathSegment)) {
362
+ if (potentialTagFull.length > 0) {
363
+ return sanitizeTag(potentialTagFull);
364
+ }
365
+ }
366
+ }
367
+ const firstPartCamel = camelcase(firstPart);
368
+ if (firstPartCamel) {
369
+ const isFirstPartCamelVerb = commonVerbs.has(firstPartCamel);
370
+ if (!isFirstPartCamelVerb || validParts.length === 1 || !canFallbackToPathSegment) {
371
+ return sanitizeTag(firstPartCamel);
372
+ }
373
+ }
374
+ if (isFirstPartVerb && validParts.length > 1 && validParts[1] && canFallbackToPathSegment) {
375
+ const secondPartCamel = camelcase(validParts[1]);
376
+ if (secondPartCamel) {
377
+ return sanitizeTag(secondPartCamel);
378
+ }
379
+ }
380
+ }
381
+ }
382
+ if (potentialCandidates.length > 0) {
383
+ let firstCandidate = potentialCandidates[0];
384
+ if (firstCandidate.startsWith("@")) {
385
+ firstCandidate = firstCandidate.substring(1);
386
+ }
387
+ if (firstCandidate) {
388
+ return sanitizeTag(camelcase(firstCandidate));
389
+ }
390
+ }
391
+ console.warn(
392
+ `Could not determine a suitable tag for path: ${pathString}, operationId: ${operationId}. Using 'unknown'.`
393
+ );
394
+ return "unknown";
395
+ }
93
396
 
94
397
  // packages/typescript/src/lib/emitters/zod.ts
95
398
  import { cleanRef, followRef, isRef, parseRef } from "@sdk-it/core";
@@ -218,10 +521,13 @@ var ZodDeserialzer = class {
218
521
  }
219
522
  return `z.union([${oneOfSchemas.join(", ")}])${appendOptional(required)}`;
220
523
  }
221
- enum(values) {
524
+ enum(type, values) {
222
525
  if (values.length === 1) {
223
526
  return `z.literal(${values.join(", ")})`;
224
527
  }
528
+ if (type === "integer") {
529
+ return `z.union([${values.map((val) => `z.literal(${val})`).join(", ")}])`;
530
+ }
225
531
  return `z.enum([${values.join(", ")}])`;
226
532
  }
227
533
  /**
@@ -261,7 +567,7 @@ var ZodDeserialzer = class {
261
567
  break;
262
568
  case "byte":
263
569
  case "binary":
264
- base = "z.instanceof(Blob) /* consider base64 check if needed */";
570
+ base = "z.instanceof(Blob)";
265
571
  break;
266
572
  case "int64":
267
573
  base = "z.string() /* or z.bigint() if your app can handle it */";
@@ -321,7 +627,7 @@ var ZodDeserialzer = class {
321
627
  if (schema.enum && Array.isArray(schema.enum)) {
322
628
  const enumVals = schema.enum.map((val) => JSON.stringify(val));
323
629
  const defaultValue = enumVals.includes(JSON.stringify(schema.default)) ? JSON.stringify(schema.default) : void 0;
324
- return `${this.enum(enumVals)}${this.#suffixes(defaultValue, required, false)}`;
630
+ return `${this.enum(schema.type, enumVals)}${this.#suffixes(defaultValue, required, false)}`;
325
631
  }
326
632
  const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
327
633
  if (!types.length) {
@@ -352,7 +658,7 @@ function appendDefault(defaultValue) {
352
658
 
353
659
  // packages/typescript/src/lib/sdk.ts
354
660
  import { get } from "lodash-es";
355
- import { camelcase, pascalcase, spinalcase } from "stringcase";
661
+ import { camelcase as camelcase2, pascalcase, spinalcase } from "stringcase";
356
662
  import { followRef as followRef3, isRef as isRef4, toLitObject as toLitObject2 } from "@sdk-it/core";
357
663
 
358
664
  // packages/typescript/src/lib/emitters/interface.ts
@@ -705,7 +1011,7 @@ function generateInputs(operationsSet, commonZod, makeImport) {
705
1011
  const output = [];
706
1012
  const imports = /* @__PURE__ */ new Set(['import { z } from "zod";']);
707
1013
  for (const operation of operations) {
708
- const schemaName = camelcase(`${operation.name} schema`);
1014
+ const schemaName = camelcase2(`${operation.name} schema`);
709
1015
  const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject2(operation.schemas)};`;
710
1016
  const inputContent = schema;
711
1017
  for (const schema2 of commonImports) {
@@ -742,8 +1048,8 @@ function generateInputs(operationsSet, commonZod, makeImport) {
742
1048
  };
743
1049
  }
744
1050
  function toEndpoint(groupName, spec, specOperation, operation, utils) {
745
- const schemaName = camelcase(`${operation.name} schema`);
746
- const schemaRef = `${camelcase(groupName)}.${schemaName}`;
1051
+ const schemaName = camelcase2(`${operation.name} schema`);
1052
+ const schemaRef = `${camelcase2(groupName)}.${schemaName}`;
747
1053
  const inputHeaders = [];
748
1054
  const inputQuery = [];
749
1055
  const inputBody = [];
@@ -776,12 +1082,7 @@ function toEndpoint(groupName, spec, specOperation, operation, utils) {
776
1082
  return statusCode >= 200 && statusCode < 300;
777
1083
  }).length > 1;
778
1084
  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];
1085
+ const response = isRef4(specOperation.responses[status]) ? followRef3(spec, specOperation.responses[status].$ref) : specOperation.responses[status];
785
1086
  const handled = handleResponse(
786
1087
  spec,
787
1088
  operation.name,
@@ -928,6 +1229,9 @@ function handleResponse(spec, operationName, status, response, utils, numbered)
928
1229
  return { schemas, imports, endpointImports, responses, outputs };
929
1230
  }
930
1231
 
1232
+ // packages/typescript/src/lib/styles/github/endpoints.txt
1233
+ 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};";
1234
+
931
1235
  // packages/typescript/src/lib/generator.ts
932
1236
  function generateCode(config) {
933
1237
  const commonZod = /* @__PURE__ */ new Map();
@@ -983,21 +1287,37 @@ function generateCode(config) {
983
1287
  const schemas = {};
984
1288
  const shortContenTypeMap = {
985
1289
  "application/json": "json",
1290
+ "application/*+json": "json",
1291
+ // type specific of json like application/vnd.api+json (from the generation pov it shouldn't matter)
1292
+ "text/json": "json",
1293
+ // non standard - later standardized to application/json
986
1294
  "application/x-www-form-urlencoded": "urlencoded",
987
1295
  "multipart/form-data": "formdata",
988
1296
  "application/xml": "xml",
989
1297
  "text/plain": "text"
990
1298
  };
991
1299
  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;
1300
+ if (!isEmpty(operation.requestBody)) {
1301
+ const requestBody = isRef5(operation.requestBody) ? followRef4(config.spec, operation.requestBody.$ref) : operation.requestBody;
1302
+ for (const type in requestBody.content) {
1303
+ const ctSchema = isRef5(requestBody.content[type].schema) ? followRef4(config.spec, requestBody.content[type].schema.$ref) : requestBody.content[type].schema;
996
1304
  if (!ctSchema) {
997
- console.warn(`Schema not found for ${type}`);
1305
+ console.warn(
1306
+ `Schema not found for ${type} in ${entry.method} ${entry.path}`
1307
+ );
998
1308
  continue;
999
1309
  }
1000
- const schema = merge({}, ctSchema, {
1310
+ let objectSchema = ctSchema;
1311
+ if (objectSchema.type !== "object") {
1312
+ objectSchema = {
1313
+ type: "object",
1314
+ required: [requestBody.required ? "$body" : ""],
1315
+ properties: {
1316
+ $body: ctSchema
1317
+ }
1318
+ };
1319
+ }
1320
+ const schema = merge({}, objectSchema, {
1001
1321
  required: additionalProperties.filter((p) => p.required).map((p) => p.name),
1002
1322
  properties: additionalProperties.reduce(
1003
1323
  (acc, p) => ({
@@ -1007,14 +1327,14 @@ function generateCode(config) {
1007
1327
  {}
1008
1328
  )
1009
1329
  });
1010
- Object.assign(inputs, bodyInputs(config, ctSchema));
1330
+ Object.assign(inputs, bodyInputs(config, objectSchema));
1011
1331
  schemas[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
1012
1332
  }
1013
- if (content["application/json"]) {
1333
+ if (requestBody.content["application/json"]) {
1014
1334
  outgoingContentType = "json";
1015
- } else if (content["application/x-www-form-urlencoded"]) {
1335
+ } else if (requestBody.content["application/x-www-form-urlencoded"]) {
1016
1336
  outgoingContentType = "urlencoded";
1017
- } else if (content["multipart/form-data"]) {
1337
+ } else if (requestBody.content["multipart/form-data"]) {
1018
1338
  outgoingContentType = "formdata";
1019
1339
  } else {
1020
1340
  outgoingContentType = "json";
@@ -1042,7 +1362,7 @@ function generateCode(config) {
1042
1362
  operation,
1043
1363
  {
1044
1364
  outgoingContentType,
1045
- name: entry.name,
1365
+ name: operation.operationId,
1046
1366
  type: "http",
1047
1367
  trigger: entry,
1048
1368
  schemas,
@@ -1060,13 +1380,15 @@ function generateCode(config) {
1060
1380
  ...responses.map((it) => `export type ${it.name} = ${it.schema};`)
1061
1381
  );
1062
1382
  } else {
1063
- output.push(`export type ${pascalcase2(entry.name + " output")} = void;`);
1383
+ output.push(
1384
+ `export type ${pascalcase2(operation.operationId + " output")} = void;`
1385
+ );
1064
1386
  }
1065
1387
  output.unshift(...useImports(output.join(""), ...responsesImports));
1066
- outputs[`${spinalcase2(entry.name)}.ts`] = output.join("\n");
1388
+ outputs[`${spinalcase2(operation.operationId)}.ts`] = output.join("\n");
1067
1389
  endpoints[entry.groupName].push(endpoint);
1068
1390
  groups[entry.groupName].push({
1069
- name: entry.name,
1391
+ name: operation.operationId,
1070
1392
  type: "http",
1071
1393
  inputs,
1072
1394
  outgoingContentType,
@@ -1091,8 +1413,8 @@ function generateCode(config) {
1091
1413
  {}
1092
1414
  );
1093
1415
  const allSchemas = Object.keys(endpoints).map((it) => ({
1094
- import: `import ${camelcase2(it)} from './${config.makeImport(spinalcase2(it))}';`,
1095
- use: ` ...${camelcase2(it)}`
1416
+ import: `import ${camelcase3(it)} from './${config.makeImport(spinalcase2(it))}';`,
1417
+ use: ` ...${camelcase3(it)}`
1096
1418
  }));
1097
1419
  const imports = [
1098
1420
  'import z from "zod";',
@@ -1105,44 +1427,27 @@ function generateCode(config) {
1105
1427
  commonSchemas,
1106
1428
  commonZod,
1107
1429
  outputs,
1108
- clientFiles: {},
1109
1430
  endpoints: {
1110
- [`${join("api", config.makeImport("schemas"))}`]: `
1111
- ${imports.join("\n")}
1112
- ${allSchemas.map((it) => it.import).join("\n")}
1431
+ [join("api", "endpoints.ts")]: `
1113
1432
 
1114
- const schemas = {
1115
- ${allSchemas.map((it) => it.use).join(",\n")}
1116
- };
1117
1433
 
1434
+ import type z from 'zod';
1435
+ import type { ParseError } from '${config.makeImport("../http/parser")}';
1436
+ import type { ProblematicResponse, SuccessfulResponse } from '${config.makeImport(
1437
+ "../http/response"
1438
+ )}';
1439
+ import type { OutputType, Parser, Type } from '${config.makeImport(
1440
+ "../http/send-request"
1441
+ )}';
1118
1442
 
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;
1127
-
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
- };
1443
+ import schemas from '${config.makeImport("./schemas")}';
1143
1444
 
1144
- export default schemas;
1445
+ ${endpoints_default}`,
1446
+ [`${join("api", "schemas.ts")}`]: `${allSchemas.map((it) => it.import).join("\n")}
1145
1447
 
1448
+ export default {
1449
+ ${allSchemas.map((it) => it.use).join(",\n")}
1450
+ };
1146
1451
 
1147
1452
  `.trim(),
1148
1453
  ...Object.fromEntries(
@@ -1158,14 +1463,14 @@ export default schemas;
1158
1463
  );
1159
1464
  return [
1160
1465
  [
1161
- join("api", config.makeImport(spinalcase2(name))),
1466
+ join("api", `${spinalcase2(name)}.ts`),
1162
1467
  `${[
1163
1468
  ...imps,
1164
1469
  // ...imports,
1165
1470
  `import z from 'zod';`,
1166
1471
  `import { toRequest, json, urlencoded, nobody, formdata, createUrl } from '${config.makeImport("../http/request")}';`,
1167
1472
  `import { chunked, buffered } from "${config.makeImport("../http/parse-response")}";`,
1168
- `import * as ${camelcase2(name)} from '../inputs/${config.makeImport(spinalcase2(name))}';`
1473
+ `import * as ${camelcase3(name)} from '../inputs/${config.makeImport(spinalcase2(name))}';`
1169
1474
  ].join(
1170
1475
  "\n"
1171
1476
  )}
@@ -1226,22 +1531,22 @@ function bodyInputs(config, ctSchema) {
1226
1531
  }
1227
1532
 
1228
1533
  // 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";
1534
+ 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
1535
 
1231
1536
  // 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';
1537
+ 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
1538
 
1234
1539
  // 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';
1540
+ 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
1541
 
1237
1542
  // 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";
1543
+ 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
1544
 
1240
1545
  // 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";
1546
+ 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
1547
 
1243
1548
  // 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";
1549
+ 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 return [null as never, parseError 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 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 return [data as never, null] as const;\n }\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\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
1550
 
1246
1551
  // packages/typescript/src/lib/generate.ts
1247
1552
  function security(spec) {
@@ -1269,11 +1574,13 @@ async function generate(spec, settings) {
1269
1574
  const makeImport = (moduleSpecifier) => {
1270
1575
  return settings.useTsExtension ? `${moduleSpecifier}.ts` : moduleSpecifier;
1271
1576
  };
1272
- const { commonSchemas, endpoints, groups, outputs, commonZod, clientFiles } = generateCode({
1273
- spec,
1274
- style: "github",
1275
- makeImport
1276
- });
1577
+ const { commonSchemas, endpoints, groups, outputs, commonZod } = generateCode(
1578
+ {
1579
+ spec,
1580
+ style: "github",
1581
+ makeImport
1582
+ }
1583
+ );
1277
1584
  const output = settings.mode === "full" ? join2(settings.output, "src") : settings.output;
1278
1585
  const options = security(spec);
1279
1586
  const clientName = settings.name || "Client";
@@ -1285,14 +1592,16 @@ async function generate(spec, settings) {
1285
1592
  // 'README.md': readme,
1286
1593
  });
1287
1594
  await writeFiles(join2(output, "http"), {
1288
- "interceptors.ts": interceptors_default,
1595
+ "interceptors.ts": `
1596
+ import { type RequestConfig } from './${makeImport("request")}';
1597
+ ${interceptors_default}`,
1289
1598
  "parse-response.ts": parse_response_default,
1290
1599
  "send-request.ts": `import z from 'zod';
1291
1600
  import type { Interceptor } from './${makeImport("interceptors")}';
1292
1601
  import { buffered } from './${makeImport("parse-response")}';
1293
- import { parse } from './${makeImport("parser")}';
1602
+ import { parseInput } from './${makeImport("parser")}';
1294
1603
  import type { RequestConfig } from './${makeImport("request")}';
1295
- import { APIResponse } from './${makeImport("response")}';
1604
+ import { APIError, APIResponse } from './${makeImport("response")}';
1296
1605
 
1297
1606
  ${send_request_default}`,
1298
1607
  "response.ts": response_default,
@@ -1308,7 +1617,6 @@ ${send_request_default}`,
1308
1617
  options,
1309
1618
  makeImport
1310
1619
  }),
1311
- ...clientFiles,
1312
1620
  ...inputFiles,
1313
1621
  ...endpoints,
1314
1622
  ...Object.fromEntries(