@sdk-it/typescript 0.15.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
@@ -1,25 +1,9 @@
1
1
  // packages/typescript/src/lib/generate.ts
2
- import { join } from "node:path";
2
+ import { join as join2 } from "node:path";
3
3
  import { npmRunPathEnv } from "npm-run-path";
4
4
  import { spinalcase as spinalcase3 } from "stringcase";
5
5
  import { getFolderExports, methods, writeFiles } from "@sdk-it/core";
6
6
 
7
- // packages/typescript/src/lib/generator.ts
8
- import { get as get2, merge } from "lodash-es";
9
- import { pascalcase, spinalcase as spinalcase2 } from "stringcase";
10
- import {
11
- forEachOperation,
12
- removeDuplicates as removeDuplicates3
13
- } from "@sdk-it/core";
14
-
15
- // packages/typescript/src/lib/utils.ts
16
- import { get } from "lodash-es";
17
- import { removeDuplicates as removeDuplicates2 } from "@sdk-it/core";
18
-
19
- // packages/typescript/src/lib/sdk.ts
20
- import { camelcase, spinalcase } from "stringcase";
21
- import { removeDuplicates, toLitObject as toLitObject2 } from "@sdk-it/core";
22
-
23
7
  // packages/typescript/src/lib/client.ts
24
8
  import { toLitObject } from "@sdk-it/core";
25
9
  var client_default = (spec) => {
@@ -44,15 +28,18 @@ var client_default = (spec) => {
44
28
  }
45
29
  };
46
30
  return `
47
- 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")}';
48
33
  import z from 'zod';
49
- import type { Endpoints } from './${spec.makeImport("endpoints")}';
50
- import schemas from './${spec.makeImport("schemas")}';
34
+ import type { Endpoints } from './api/${spec.makeImport("endpoints")}';
35
+ import schemas from './api/${spec.makeImport("schemas")}';
51
36
  import {
52
37
  createBaseUrlInterceptor,
53
38
  createHeadersInterceptor,
54
39
  } from './http/${spec.makeImport("interceptors")}';
55
40
 
41
+ import { parseInput, type ParseError } from './http/${spec.makeImport("parser")}';
42
+
56
43
  ${spec.servers.length ? `export const servers = ${JSON.stringify(spec.servers, null, 2)} as const` : ""}
57
44
  const optionsSchema = z.object(${toLitObject(specOptions, (x) => x.schema)});
58
45
  ${spec.servers.length ? `export type Servers = typeof servers[number];` : ""}
@@ -81,6 +68,44 @@ export class ${spec.name} {
81
68
  });
82
69
  }
83
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
+
84
109
  get defaultHeaders() {
85
110
  return ${defaultHeaders}
86
111
  }
@@ -101,536 +126,276 @@ export class ${spec.name} {
101
126
  }`;
102
127
  };
103
128
 
104
- // packages/typescript/src/lib/sdk.ts
105
- var SchemaEndpoint = class {
106
- #makeImport;
107
- #imports = [];
108
- constructor(makeImport) {
109
- this.#makeImport = makeImport;
110
- this.#imports = [
111
- `import z from 'zod';`,
112
- `import type { Endpoints } from '${this.#makeImport("./endpoints")}';`,
113
- `import { toRequest, json, urlencoded, nobody, formdata, createUrl } from '${this.#makeImport("./http/request")}';`,
114
- `import type { ParseError } from '${this.#makeImport("./http/parser")}';`,
115
- `import { chunked, buffered } from "${this.#makeImport("./http/parse-response")}";`
116
- ];
117
- }
118
- #endpoints = [];
119
- addEndpoint(endpoint, operation) {
120
- this.#endpoints.push(` "${endpoint}": ${operation},`);
121
- }
122
- addImport(value) {
123
- this.#imports.push(value);
124
- }
125
- complete() {
126
- return `${this.#imports.join("\n")}
127
- export default {
128
- ${this.#endpoints.join("\n")}
129
- }`;
130
- }
131
- };
132
- var Emitter = class {
133
- #makeImport;
134
- imports = [];
135
- constructor(makeImport) {
136
- this.#makeImport = makeImport;
137
- this.imports = [
138
- `import type z from 'zod';`,
139
- `import type { ParseError } from '${this.#makeImport("./http/parser")}';`
140
- ];
141
- }
142
- endpoints = [];
143
- addEndpoint(endpoint, operation) {
144
- this.endpoints.push(` "${endpoint}": ${operation};`);
145
- }
146
- addImport(value) {
147
- this.imports.push(value);
148
- }
149
- complete() {
150
- return `${this.imports.join("\n")}
151
- export interface Endpoints {
152
- ${this.endpoints.join("\n")}
153
- }`;
129
+ // packages/typescript/src/lib/generator.ts
130
+ import { merge } from "lodash-es";
131
+ import { join } from "node:path";
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);
154
152
  }
155
153
  };
156
- function generateInputs(operationsSet, commonZod, makeImport) {
157
- const commonImports = commonZod.keys().toArray();
158
- const inputs = {};
159
- for (const [name, operations] of Object.entries(operationsSet)) {
160
- const output = [];
161
- const imports = /* @__PURE__ */ new Set(['import { z } from "zod";']);
162
- for (const operation of operations) {
163
- const schemaName = camelcase(`${operation.name} schema`);
164
- const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject2(operation.schemas)};`;
165
- const inputContent = schema;
166
- for (const schema2 of commonImports) {
167
- if (inputContent.includes(schema2)) {
168
- imports.add(
169
- `import { ${schema2} } from './schemas/${makeImport(spinalcase(schema2))}';`
170
- );
171
- }
172
- }
173
- output.push(inputContent);
174
- }
175
- inputs[`inputs/${spinalcase(name)}.ts`] = [...imports, ...output].join("\n") + "\n";
176
- }
177
- const schemas = commonZod.entries().reduce((acc, [name, schema]) => {
178
- const output = [`import { z } from 'zod';`];
179
- const content = `export const ${name} = ${schema};`;
180
- for (const schema2 of commonImports) {
181
- const preciseMatch = new RegExp(`\\b${schema2}\\b`);
182
- if (preciseMatch.test(content) && schema2 !== name) {
183
- output.push(
184
- `import { ${schema2} } from './${makeImport(spinalcase(schema2))}';`
185
- );
186
- }
187
- }
188
- output.push(content);
189
- return [
190
- [`inputs/schemas/${spinalcase(name)}.ts`, output.join("\n")],
191
- ...acc
192
- ];
193
- }, []);
194
- return {
195
- ...Object.fromEntries(schemas),
196
- ...inputs
197
- };
198
- }
199
- function generateSDK(spec) {
200
- const emitter = new Emitter(spec.makeImport);
201
- const schemaEndpoint = new SchemaEndpoint(spec.makeImport);
202
- const errors = [];
203
- for (const [name, operations] of Object.entries(spec.operations)) {
204
- emitter.addImport(
205
- `import type * as ${camelcase(name)} from './inputs/${spec.makeImport(spinalcase(name))}';`
206
- );
207
- schemaEndpoint.addImport(
208
- `import * as ${camelcase(name)} from './inputs/${spec.makeImport(spinalcase(name))}';`
209
- );
210
- for (const operation of operations) {
211
- const schemaName = camelcase(`${operation.name} schema`);
212
- const schemaRef = `${camelcase(name)}.${schemaName}`;
213
- const output = operation.formatOutput();
214
- const inputHeaders = [];
215
- const inputQuery = [];
216
- const inputBody = [];
217
- const inputParams = [];
218
- for (const [name2, prop] of Object.entries(operation.inputs)) {
219
- if (prop.in === "headers" || prop.in === "header") {
220
- inputHeaders.push(`"${name2}"`);
221
- } else if (prop.in === "query") {
222
- inputQuery.push(`"${name2}"`);
223
- } else if (prop.in === "body") {
224
- inputBody.push(`"${name2}"`);
225
- } else if (prop.in === "path") {
226
- inputParams.push(`"${name2}"`);
227
- } else if (prop.in === "internal") {
228
- continue;
229
- } else {
230
- throw new Error(
231
- `Unknown source ${prop.in} in ${name2} ${JSON.stringify(
232
- prop
233
- )} in ${operation.name}`
234
- );
235
- }
236
- }
237
- emitter.addImport(
238
- `import type {${output.import}} from './outputs/${spec.makeImport(spinalcase(operation.name))}';`
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
+ )
239
180
  );
240
- errors.push(...operation.errors ?? []);
241
- const addTypeParser = Object.keys(operation.schemas).length > 1;
242
- for (const type in operation.schemas ?? {}) {
243
- let typePrefix = "";
244
- if (addTypeParser && type !== "json") {
245
- typePrefix = `${type} `;
246
- }
247
- const input = `typeof ${schemaRef}${addTypeParser ? `.${type}` : ""}`;
248
- const endpoint = `${typePrefix}${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
249
- emitter.addEndpoint(
250
- endpoint,
251
- `{input: z.infer<${input}>; output: ${output.use}; error: ${(operation.errors ?? ["ServerError"]).concat(`ParseError<${input}>`).join("|")}}`
252
- );
253
- schemaEndpoint.addEndpoint(
254
- endpoint,
255
- `{
256
- schema: ${schemaRef}${addTypeParser ? `.${type}` : ""},
257
- deserializer: ${operation.parser === "chunked" ? "chunked" : "buffered"},
258
- toRequest(input: Endpoints['${endpoint}']['input']) {
259
- const endpoint = '${endpoint}';
260
- return toRequest(endpoint, ${operation.outgoingContentType || "nobody"}(input, {
261
- inputHeaders: [${inputHeaders}],
262
- inputQuery: [${inputQuery}],
263
- inputBody: [${inputBody}],
264
- inputParams: [${inputParams}],
265
- }));
266
- },
267
- }`
268
- );
269
- }
270
181
  }
271
182
  }
272
- emitter.addImport(
273
- `import type { ${removeDuplicates(errors, (it) => it).join(", ")} } from '${spec.makeImport("./http/response")}';`
274
- );
275
- return {
276
- "client.ts": client_default(spec),
277
- "schemas.ts": schemaEndpoint.complete(),
278
- "endpoints.ts": emitter.complete()
279
- };
280
- }
281
-
282
- // packages/typescript/src/lib/utils.ts
283
- function isRef(obj) {
284
- return obj && "$ref" in obj;
285
- }
286
- function cleanRef(ref) {
287
- return ref.replace(/^#\//, "");
183
+ return result;
288
184
  }
289
- function parseRef(ref) {
290
- const parts = ref.split("/");
291
- const [model] = parts.splice(-1);
292
- return { model, path: parts.join("/") };
293
- }
294
- function followRef(spec, ref) {
295
- const pathParts = cleanRef(ref).split("/");
296
- const entry = get(spec, pathParts);
297
- if (entry && "$ref" in entry) {
298
- return followRef(spec, entry.$ref);
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}`;
299
279
  }
300
- return entry;
280
+ return reservedKeywords.has(camelCasedTag) ? `${camelCasedTag}_` : camelCasedTag;
301
281
  }
302
- function securityToOptions(security2, securitySchemes, staticIn) {
303
- securitySchemes ??= {};
304
- const options = {};
305
- for (const it of security2) {
306
- const [name] = Object.keys(it);
307
- if (!name) {
308
- continue;
309
- }
310
- const schema = securitySchemes[name];
311
- if (isRef(schema)) {
312
- throw new Error(`Ref security schemas are not supported`);
313
- }
314
- if (schema.type === "http") {
315
- options["authorization"] = {
316
- in: staticIn ?? "header",
317
- schema: "z.string().optional().transform((val) => (val ? `Bearer ${val}` : undefined))",
318
- optionName: "token"
319
- };
320
- continue;
321
- }
322
- if (schema.type === "apiKey") {
323
- if (!schema.in) {
324
- throw new Error(`apiKey security schema must have an "in" field`);
325
- }
326
- if (!schema.name) {
327
- throw new Error(`apiKey security schema must have a "name" field`);
328
- }
329
- options[schema.name] = {
330
- in: staticIn ?? schema.in,
331
- schema: "z.string().optional()"
332
- };
333
- continue;
334
- }
335
- }
336
- return options;
337
- }
338
- function mergeImports(imports) {
339
- const merged = {};
340
- for (const i of imports) {
341
- merged[i.moduleSpecifier] = merged[i.moduleSpecifier] ?? {
342
- moduleSpecifier: i.moduleSpecifier,
343
- defaultImport: i.defaultImport,
344
- namespaceImport: i.namespaceImport,
345
- namedImports: []
346
- };
347
- if (i.namedImports) {
348
- merged[i.moduleSpecifier].namedImports.push(...i.namedImports);
349
- }
350
- }
351
- return Object.values(merged);
352
- }
353
- function importsToString(...imports) {
354
- return imports.map((it) => {
355
- if (it.defaultImport) {
356
- return `import ${it.defaultImport} from '${it.moduleSpecifier}'`;
357
- }
358
- if (it.namespaceImport) {
359
- return `import * as ${it.namespaceImport} from '${it.moduleSpecifier}'`;
360
- }
361
- if (it.namedImports) {
362
- return `import {${removeDuplicates2(it.namedImports, (it2) => it2.name).map((n) => `${n.isTypeOnly ? "type" : ""} ${n.name}`).join(", ")}} from '${it.moduleSpecifier}'`;
363
- }
364
- throw new Error(`Invalid import ${JSON.stringify(it)}`);
365
- });
366
- }
367
- function exclude(list, exclude2) {
368
- return list.filter((it) => !exclude2.includes(it));
369
- }
370
- function useImports(content, imports) {
371
- const output = [];
372
- for (const it of mergeImports(imports)) {
373
- const singleImport = it.defaultImport ?? it.namespaceImport;
374
- if (singleImport && content.includes(singleImport)) {
375
- output.push(importsToString(it).join("\n"));
376
- } else if (it.namedImports.length) {
377
- for (const namedImport of it.namedImports) {
378
- if (content.includes(namedImport.name)) {
379
- output.push(importsToString(it).join("\n"));
380
- }
381
- }
382
- }
383
- }
384
- return output;
385
- }
386
-
387
- // packages/typescript/src/lib/emitters/interface.ts
388
- var TypeScriptDeserialzer = class {
389
- circularRefTracker = /* @__PURE__ */ new Set();
390
- #spec;
391
- #onRef;
392
- constructor(spec, onRef) {
393
- this.#spec = spec;
394
- this.#onRef = onRef;
395
- }
396
- #stringifyKey = (key) => {
397
- const reservedWords = [
398
- "constructor",
399
- "prototype",
400
- "break",
401
- "case",
402
- "catch",
403
- "class",
404
- "const",
405
- "continue",
406
- "debugger",
407
- "default",
408
- "delete",
409
- "do",
410
- "else",
411
- "export",
412
- "extends",
413
- "false",
414
- "finally",
415
- "for",
416
- "function",
417
- "if",
418
- "import",
419
- "in",
420
- "instanceof",
421
- "new",
422
- "null",
423
- "return",
424
- "super",
425
- "switch",
426
- "this",
427
- "throw",
428
- "true",
429
- "try",
430
- "typeof",
431
- "var",
432
- "void",
433
- "while",
434
- "with",
435
- "yield"
436
- ];
437
- if (reservedWords.includes(key)) {
438
- return `'${key}'`;
439
- }
440
- if (key.trim() === "") {
441
- return `'${key}'`;
442
- }
443
- const firstChar = key.charAt(0);
444
- const validFirstChar = firstChar >= "a" && firstChar <= "z" || firstChar >= "A" && firstChar <= "Z" || firstChar === "_" || firstChar === "$";
445
- if (!validFirstChar) {
446
- return `'${key.replace(/'/g, "\\'")}'`;
447
- }
448
- for (let i = 1; i < key.length; i++) {
449
- const char = key.charAt(i);
450
- const validChar = char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char >= "0" && char <= "9" || char === "_" || char === "$";
451
- if (!validChar) {
452
- return `'${key.replace(/'/g, "\\'")}'`;
453
- }
454
- }
455
- return key;
456
- };
457
- #stringifyKeyV2 = (value) => {
458
- return `'${value}'`;
459
- };
460
- /**
461
- * Handle objects (properties)
462
- */
463
- object(schema, required = false) {
464
- const properties = schema.properties || {};
465
- const propEntries = Object.entries(properties).map(([key, propSchema]) => {
466
- const isRequired = (schema.required ?? []).includes(key);
467
- const tsType = this.handle(propSchema, isRequired);
468
- return `${this.#stringifyKeyV2(key)}: ${tsType}`;
469
- });
470
- if (schema.additionalProperties) {
471
- if (typeof schema.additionalProperties === "object") {
472
- const indexType = this.handle(schema.additionalProperties, true);
473
- propEntries.push(`[key: string]: ${indexType}`);
474
- } else if (schema.additionalProperties === true) {
475
- propEntries.push("[key: string]: any");
476
- }
477
- }
478
- return `{ ${propEntries.join("; ")} }`;
479
- }
480
- /**
481
- * Handle arrays (items could be a single schema or a tuple)
482
- */
483
- array(schema, required = false) {
484
- const { items } = schema;
485
- if (!items) {
486
- return "any[]";
487
- }
488
- if (Array.isArray(items)) {
489
- const tupleItems = items.map((sub) => this.handle(sub, true));
490
- return `[${tupleItems.join(", ")}]`;
491
- }
492
- const itemsType = this.handle(items, true);
493
- return `${itemsType}[]`;
494
- }
495
- /**
496
- * Convert a basic type (string | number | boolean | object | array, etc.) to TypeScript
497
- */
498
- normal(type, schema, required = false) {
499
- switch (type) {
500
- case "string":
501
- return this.string(schema, required);
502
- case "number":
503
- case "integer":
504
- return this.number(schema, required);
505
- case "boolean":
506
- return appendOptional("boolean", required);
507
- case "object":
508
- return this.object(schema, required);
509
- case "array":
510
- return this.array(schema, required);
511
- case "null":
512
- return "null";
513
- default:
514
- return appendOptional("any", required);
515
- }
516
- }
517
- ref($ref, required) {
518
- const schemaName = cleanRef($ref).split("/").pop();
519
- if (this.circularRefTracker.has(schemaName)) {
520
- return schemaName;
521
- }
522
- this.circularRefTracker.add(schemaName);
523
- this.#onRef(schemaName, this.handle(followRef(this.#spec, $ref), true));
524
- this.circularRefTracker.delete(schemaName);
525
- return appendOptional(schemaName, required);
526
- }
527
- allOf(schemas) {
528
- const allOfTypes = schemas.map((sub) => this.handle(sub, true));
529
- return allOfTypes.length > 1 ? `${allOfTypes.join(" & ")}` : allOfTypes[0];
530
- }
531
- anyOf(schemas, required) {
532
- const anyOfTypes = schemas.map((sub) => this.handle(sub, true));
533
- return appendOptional(
534
- anyOfTypes.length > 1 ? `${anyOfTypes.join(" | ")}` : anyOfTypes[0],
535
- required
536
- );
537
- }
538
- oneOf(schemas, required) {
539
- const oneOfTypes = schemas.map((sub) => {
540
- if (isRef(sub)) {
541
- const { model } = parseRef(sub.$ref);
542
- if (this.circularRefTracker.has(model)) {
543
- return model;
544
- }
545
- }
546
- return this.handle(sub, false);
547
- });
548
- return appendOptional(
549
- oneOfTypes.length > 1 ? `${oneOfTypes.join(" | ")}` : oneOfTypes[0],
550
- required
551
- );
552
- }
553
- enum(values, required) {
554
- const enumValues = values.map((val) => typeof val === "string" ? `'${val}'` : `${val}`).join(" | ");
555
- return appendOptional(enumValues, required);
556
- }
557
- /**
558
- * Handle string type with formats
559
- */
560
- string(schema, required) {
561
- let type;
562
- switch (schema.format) {
563
- case "date-time":
564
- case "datetime":
565
- case "date":
566
- type = "Date";
567
- break;
568
- case "binary":
569
- case "byte":
570
- type = "Blob";
571
- break;
572
- case "int64":
573
- type = "bigint";
574
- break;
575
- default:
576
- type = "string";
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));
577
314
  }
578
- return appendOptional(type, required);
579
- }
580
- /**
581
- * Handle number/integer types with formats
582
- */
583
- number(schema, required) {
584
- const type = schema.format === "int64" ? "bigint" : "number";
585
- return appendOptional(type, required);
586
315
  }
587
- handle(schema, required) {
588
- if (isRef(schema)) {
589
- return this.ref(schema.$ref, required);
590
- }
591
- if (schema.allOf && Array.isArray(schema.allOf)) {
592
- return this.allOf(schema.allOf);
593
- }
594
- if (schema.anyOf && Array.isArray(schema.anyOf)) {
595
- return this.anyOf(schema.anyOf, required);
596
- }
597
- if (schema.oneOf && Array.isArray(schema.oneOf)) {
598
- return this.oneOf(schema.oneOf, required);
599
- }
600
- if (schema.enum && Array.isArray(schema.enum)) {
601
- return this.enum(schema.enum, required);
602
- }
603
- const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
604
- if (!types.length) {
605
- if ("properties" in schema) {
606
- return this.object(schema, required);
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
+ }
607
357
  }
608
- return appendOptional("any", required);
609
- }
610
- if (types.length > 1) {
611
- const realTypes = types.filter((t) => t !== "null");
612
- if (realTypes.length === 1 && types.includes("null")) {
613
- const tsType = this.normal(realTypes[0], schema, false);
614
- return appendOptional(`${tsType} | null`, required);
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
+ }
615
379
  }
616
- const typeResults = types.map((t) => this.normal(t, schema, false));
617
- return appendOptional(typeResults.join(" | "), required);
618
380
  }
619
- return this.normal(types[0], schema, required);
620
381
  }
621
- /**
622
- * Generate an interface declaration
623
- */
624
- generateInterface(name, schema) {
625
- const content = this.handle(schema, true);
626
- return `interface ${name} ${content}`;
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
+ }
627
390
  }
628
- };
629
- function appendOptional(type, isRequired) {
630
- return isRequired ? type : `${type} | undefined`;
391
+ console.warn(
392
+ `Could not determine a suitable tag for path: ${pathString}, operationId: ${operationId}. Using 'unknown'.`
393
+ );
394
+ return "unknown";
631
395
  }
632
396
 
633
397
  // packages/typescript/src/lib/emitters/zod.ts
398
+ import { cleanRef, followRef, isRef, parseRef } from "@sdk-it/core";
634
399
  var ZodDeserialzer = class {
635
400
  circularRefTracker = /* @__PURE__ */ new Set();
636
401
  #spec;
@@ -642,12 +407,11 @@ var ZodDeserialzer = class {
642
407
  /**
643
408
  * Handle objects (properties, additionalProperties).
644
409
  */
645
- object(schema, required = false) {
410
+ object(schema) {
646
411
  const properties = schema.properties || {};
647
412
  const propEntries = Object.entries(properties).map(([key, propSchema]) => {
648
413
  const isRequired = (schema.required ?? []).includes(key);
649
- const zodPart = this.handle(propSchema, isRequired);
650
- return `'${key}': ${zodPart}`;
414
+ return `'${key}': ${this.handle(propSchema, isRequired)}`;
651
415
  });
652
416
  let additionalProps = "";
653
417
  if (schema.additionalProperties) {
@@ -658,8 +422,7 @@ var ZodDeserialzer = class {
658
422
  additionalProps = `.catchall(z.unknown())`;
659
423
  }
660
424
  }
661
- const objectSchema = `z.object({${propEntries.join(", ")}})${additionalProps}`;
662
- return `${objectSchema}${appendOptional2(required)}`;
425
+ return `z.object({${propEntries.join(", ")}})${additionalProps}`;
663
426
  }
664
427
  /**
665
428
  * Handle arrays (items could be a single schema or a tuple (array of schemas)).
@@ -668,18 +431,18 @@ var ZodDeserialzer = class {
668
431
  array(schema, required = false) {
669
432
  const { items } = schema;
670
433
  if (!items) {
671
- return `z.array(z.unknown())${appendOptional2(required)}`;
434
+ return `z.array(z.unknown())${appendOptional(required)}`;
672
435
  }
673
436
  if (Array.isArray(items)) {
674
437
  const tupleItems = items.map((sub) => this.handle(sub, true));
675
438
  const base = `z.tuple([${tupleItems.join(", ")}])`;
676
- return `${base}${appendOptional2(required)}`;
439
+ return `${base}${appendOptional(required)}`;
677
440
  }
678
441
  const itemsSchema = this.handle(items, true);
679
- return `z.array(${itemsSchema})${appendOptional2(required)}`;
442
+ return `z.array(${itemsSchema})${appendOptional(required)}`;
680
443
  }
681
444
  #suffixes = (defaultValue, required, nullable) => {
682
- return `${nullable ? ".nullable()" : ""}${appendDefault(defaultValue)}${appendOptional2(required)}`;
445
+ return `${nullable ? ".nullable()" : ""}${appendDefault(defaultValue)}${appendOptional(required)}`;
683
446
  };
684
447
  /**
685
448
  * Convert a basic type (string | number | boolean | object | array, etc.) to Zod.
@@ -697,13 +460,13 @@ var ZodDeserialzer = class {
697
460
  case "boolean":
698
461
  return `z.boolean()${this.#suffixes(schema.default, required, nullable)}`;
699
462
  case "object":
700
- return this.object(schema, true);
463
+ return `${this.object(schema)}${this.#suffixes(JSON.stringify(schema.default), required, nullable)}`;
701
464
  case "array":
702
465
  return this.array(schema, required);
703
466
  case "null":
704
- return `z.null()${appendOptional2(required)}`;
467
+ return `z.null()${appendOptional(required)}`;
705
468
  default:
706
- return `z.unknown()${appendOptional2(required)}`;
469
+ return `z.unknown()${appendOptional(required)}`;
707
470
  }
708
471
  }
709
472
  ref($ref, required) {
@@ -719,15 +482,15 @@ var ZodDeserialzer = class {
719
482
  this.circularRefTracker.delete(schemaName);
720
483
  return schemaName;
721
484
  }
722
- allOf(schemas) {
485
+ allOf(schemas, required) {
723
486
  const allOfSchemas = schemas.map((sub) => this.handle(sub, true));
724
487
  if (allOfSchemas.length === 0) {
725
488
  return `z.unknown()`;
726
489
  }
727
490
  if (allOfSchemas.length === 1) {
728
- return allOfSchemas[0];
491
+ return `${allOfSchemas[0]}${appendOptional(required)}`;
729
492
  }
730
- return this.#toIntersection(allOfSchemas);
493
+ return `${this.#toIntersection(allOfSchemas)}${appendOptional(required)}`;
731
494
  }
732
495
  #toIntersection(schemas) {
733
496
  const [left, ...right] = schemas;
@@ -737,39 +500,35 @@ var ZodDeserialzer = class {
737
500
  return `z.intersection(${left}, ${this.#toIntersection(right)})`;
738
501
  }
739
502
  anyOf(schemas, required) {
740
- const anyOfSchemas = schemas.map((sub) => this.handle(sub, false));
503
+ const anyOfSchemas = schemas.map((sub) => this.handle(sub, true));
741
504
  if (anyOfSchemas.length === 1) {
742
- return anyOfSchemas[0];
505
+ return `${anyOfSchemas[0]}${appendOptional(required)}`;
743
506
  }
744
- return anyOfSchemas.length > 1 ? `z.union([${anyOfSchemas.join(", ")}])${appendOptional2(required)}` : (
745
- // Handle an invalid anyOf with one schema
746
- anyOfSchemas[0]
747
- );
507
+ return `z.union([${anyOfSchemas.join(", ")}])${appendOptional(required)}`;
748
508
  }
749
509
  oneOf(schemas, required) {
750
510
  const oneOfSchemas = schemas.map((sub) => {
751
511
  if (isRef(sub)) {
752
512
  const { model } = parseRef(sub.$ref);
753
513
  if (this.circularRefTracker.has(model)) {
754
- return model;
514
+ return `${model}${appendOptional(required)}`;
755
515
  }
756
516
  }
757
517
  return this.handle(sub, true);
758
518
  });
759
519
  if (oneOfSchemas.length === 1) {
760
- return oneOfSchemas[0];
520
+ return `${oneOfSchemas[0]}${appendOptional(required)}`;
761
521
  }
762
- return oneOfSchemas.length > 1 ? `z.union([${oneOfSchemas.join(", ")}])${appendOptional2(required)}` : (
763
- // Handle an invalid oneOf with one schema
764
- oneOfSchemas[0]
765
- );
522
+ return `z.union([${oneOfSchemas.join(", ")}])${appendOptional(required)}`;
766
523
  }
767
- enum(values, required) {
768
- const enumVals = values.map((val) => JSON.stringify(val)).join(", ");
524
+ enum(type, values) {
769
525
  if (values.length === 1) {
770
- return `z.literal(${enumVals})${appendOptional2(required)}`;
526
+ return `z.literal(${values.join(", ")})`;
771
527
  }
772
- return `z.enum([${enumVals}])${appendOptional2(required)}`;
528
+ if (type === "integer") {
529
+ return `z.union([${values.map((val) => `z.literal(${val})`).join(", ")}])`;
530
+ }
531
+ return `z.enum([${values.join(", ")}])`;
773
532
  }
774
533
  /**
775
534
  * Handle a `string` schema with possible format keywords (JSON Schema).
@@ -808,7 +567,7 @@ var ZodDeserialzer = class {
808
567
  break;
809
568
  case "byte":
810
569
  case "binary":
811
- base = "z.instanceof(Blob) /* consider base64 check if needed */";
570
+ base = "z.instanceof(Blob)";
812
571
  break;
813
572
  case "int64":
814
573
  base = "z.string() /* or z.bigint() if your app can handle it */";
@@ -850,51 +609,522 @@ var ZodDeserialzer = class {
850
609
  if (typeof schema.multipleOf === "number") {
851
610
  base += `.refine((val) => Number.isInteger(val / ${schema.multipleOf}), "Must be a multiple of ${schema.multipleOf}")`;
852
611
  }
853
- return { base, defaultValue };
612
+ return { base, defaultValue };
613
+ }
614
+ handle(schema, required) {
615
+ if (isRef(schema)) {
616
+ return `${this.ref(schema.$ref, true)}${appendOptional(required)}`;
617
+ }
618
+ if (schema.allOf && Array.isArray(schema.allOf)) {
619
+ return this.allOf(schema.allOf ?? [], required);
620
+ }
621
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
622
+ return this.anyOf(schema.anyOf ?? [], required);
623
+ }
624
+ if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length) {
625
+ return this.oneOf(schema.oneOf ?? [], required);
626
+ }
627
+ if (schema.enum && Array.isArray(schema.enum)) {
628
+ const enumVals = schema.enum.map((val) => JSON.stringify(val));
629
+ const defaultValue = enumVals.includes(JSON.stringify(schema.default)) ? JSON.stringify(schema.default) : void 0;
630
+ return `${this.enum(schema.type, enumVals)}${this.#suffixes(defaultValue, required, false)}`;
631
+ }
632
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
633
+ if (!types.length) {
634
+ return `z.unknown()${appendOptional(required)}`;
635
+ }
636
+ if ("nullable" in schema && schema.nullable) {
637
+ types.push("null");
638
+ } else if (schema.default === null) {
639
+ types.push("null");
640
+ }
641
+ if (types.length > 1) {
642
+ const realTypes = types.filter((t) => t !== "null");
643
+ if (realTypes.length === 1 && types.includes("null")) {
644
+ return this.normal(realTypes[0], schema, required, true);
645
+ }
646
+ const subSchemas = types.map((t) => this.normal(t, schema, false));
647
+ return `z.union([${subSchemas.join(", ")}])${appendOptional(required)}`;
648
+ }
649
+ return this.normal(types[0], schema, required, false);
650
+ }
651
+ };
652
+ function appendOptional(isRequired) {
653
+ return isRequired ? "" : ".optional()";
654
+ }
655
+ function appendDefault(defaultValue) {
656
+ return defaultValue !== void 0 || typeof defaultValue !== "undefined" ? `.default(${defaultValue})` : "";
657
+ }
658
+
659
+ // packages/typescript/src/lib/sdk.ts
660
+ import { get } from "lodash-es";
661
+ import { camelcase as camelcase2, pascalcase, spinalcase } from "stringcase";
662
+ import { followRef as followRef3, isRef as isRef4, toLitObject as toLitObject2 } from "@sdk-it/core";
663
+
664
+ // packages/typescript/src/lib/emitters/interface.ts
665
+ import { cleanRef as cleanRef2, followRef as followRef2, isRef as isRef2, parseRef as parseRef2 } from "@sdk-it/core";
666
+ var TypeScriptDeserialzer = class {
667
+ circularRefTracker = /* @__PURE__ */ new Set();
668
+ #spec;
669
+ #onRef;
670
+ constructor(spec, onRef) {
671
+ this.#spec = spec;
672
+ this.#onRef = onRef;
673
+ }
674
+ #stringifyKey = (key) => {
675
+ const reservedWords = [
676
+ "constructor",
677
+ "prototype",
678
+ "break",
679
+ "case",
680
+ "catch",
681
+ "class",
682
+ "const",
683
+ "continue",
684
+ "debugger",
685
+ "default",
686
+ "delete",
687
+ "do",
688
+ "else",
689
+ "export",
690
+ "extends",
691
+ "false",
692
+ "finally",
693
+ "for",
694
+ "function",
695
+ "if",
696
+ "import",
697
+ "in",
698
+ "instanceof",
699
+ "new",
700
+ "null",
701
+ "return",
702
+ "super",
703
+ "switch",
704
+ "this",
705
+ "throw",
706
+ "true",
707
+ "try",
708
+ "typeof",
709
+ "var",
710
+ "void",
711
+ "while",
712
+ "with",
713
+ "yield"
714
+ ];
715
+ if (reservedWords.includes(key)) {
716
+ return `'${key}'`;
717
+ }
718
+ if (key.trim() === "") {
719
+ return `'${key}'`;
720
+ }
721
+ const firstChar = key.charAt(0);
722
+ const validFirstChar = firstChar >= "a" && firstChar <= "z" || firstChar >= "A" && firstChar <= "Z" || firstChar === "_" || firstChar === "$";
723
+ if (!validFirstChar) {
724
+ return `'${key.replace(/'/g, "\\'")}'`;
725
+ }
726
+ for (let i = 1; i < key.length; i++) {
727
+ const char = key.charAt(i);
728
+ const validChar = char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char >= "0" && char <= "9" || char === "_" || char === "$";
729
+ if (!validChar) {
730
+ return `'${key.replace(/'/g, "\\'")}'`;
731
+ }
732
+ }
733
+ return key;
734
+ };
735
+ #stringifyKeyV2 = (value) => {
736
+ return `'${value}'`;
737
+ };
738
+ /**
739
+ * Handle objects (properties)
740
+ */
741
+ object(schema, required = false) {
742
+ const properties = schema.properties || {};
743
+ const propEntries = Object.entries(properties).map(([key, propSchema]) => {
744
+ const isRequired = (schema.required ?? []).includes(key);
745
+ const tsType = this.handle(propSchema, isRequired);
746
+ return `${this.#stringifyKeyV2(key)}: ${tsType}`;
747
+ });
748
+ if (schema.additionalProperties) {
749
+ if (typeof schema.additionalProperties === "object") {
750
+ const indexType = this.handle(schema.additionalProperties, true);
751
+ propEntries.push(`[key: string]: ${indexType}`);
752
+ } else if (schema.additionalProperties === true) {
753
+ propEntries.push("[key: string]: any");
754
+ }
755
+ }
756
+ return `{ ${propEntries.join("; ")} }`;
757
+ }
758
+ /**
759
+ * Handle arrays (items could be a single schema or a tuple)
760
+ */
761
+ array(schema, required = false) {
762
+ const { items } = schema;
763
+ if (!items) {
764
+ return "any[]";
765
+ }
766
+ if (Array.isArray(items)) {
767
+ const tupleItems = items.map((sub) => this.handle(sub, true));
768
+ return `[${tupleItems.join(", ")}]`;
769
+ }
770
+ const itemsType = this.handle(items, true);
771
+ return `${itemsType}[]`;
772
+ }
773
+ /**
774
+ * Convert a basic type (string | number | boolean | object | array, etc.) to TypeScript
775
+ */
776
+ normal(type, schema, required = false) {
777
+ switch (type) {
778
+ case "string":
779
+ return this.string(schema, required);
780
+ case "number":
781
+ case "integer":
782
+ return this.number(schema, required);
783
+ case "boolean":
784
+ return appendOptional2("boolean", required);
785
+ case "object":
786
+ return this.object(schema, required);
787
+ case "array":
788
+ return this.array(schema, required);
789
+ case "null":
790
+ return "null";
791
+ default:
792
+ console.warn(`Unknown type: ${type}`);
793
+ return appendOptional2("any", required);
794
+ }
795
+ }
796
+ ref($ref, required) {
797
+ const schemaName = cleanRef2($ref).split("/").pop();
798
+ if (this.circularRefTracker.has(schemaName)) {
799
+ return schemaName;
800
+ }
801
+ this.circularRefTracker.add(schemaName);
802
+ this.#onRef?.(
803
+ schemaName,
804
+ this.handle(followRef2(this.#spec, $ref), required)
805
+ );
806
+ this.circularRefTracker.delete(schemaName);
807
+ return appendOptional2(schemaName, required);
808
+ }
809
+ allOf(schemas) {
810
+ const allOfTypes = schemas.map((sub) => this.handle(sub, true));
811
+ return allOfTypes.length > 1 ? `${allOfTypes.join(" & ")}` : allOfTypes[0];
812
+ }
813
+ anyOf(schemas, required) {
814
+ const anyOfTypes = schemas.map((sub) => this.handle(sub, true));
815
+ return appendOptional2(
816
+ anyOfTypes.length > 1 ? `${anyOfTypes.join(" | ")}` : anyOfTypes[0],
817
+ required
818
+ );
819
+ }
820
+ oneOf(schemas, required) {
821
+ const oneOfTypes = schemas.map((sub) => {
822
+ if (isRef2(sub)) {
823
+ const { model } = parseRef2(sub.$ref);
824
+ if (this.circularRefTracker.has(model)) {
825
+ return model;
826
+ }
827
+ }
828
+ return this.handle(sub, false);
829
+ });
830
+ return appendOptional2(
831
+ oneOfTypes.length > 1 ? `${oneOfTypes.join(" | ")}` : oneOfTypes[0],
832
+ required
833
+ );
834
+ }
835
+ enum(values, required) {
836
+ const enumValues = values.map((val) => typeof val === "string" ? `'${val}'` : `${val}`).join(" | ");
837
+ return appendOptional2(enumValues, required);
838
+ }
839
+ /**
840
+ * Handle string type with formats
841
+ */
842
+ string(schema, required) {
843
+ let type;
844
+ switch (schema.format) {
845
+ case "date-time":
846
+ case "datetime":
847
+ case "date":
848
+ type = "Date";
849
+ break;
850
+ case "binary":
851
+ case "byte":
852
+ type = "Blob";
853
+ break;
854
+ case "int64":
855
+ type = "bigint";
856
+ break;
857
+ default:
858
+ type = "string";
859
+ }
860
+ return appendOptional2(type, required);
861
+ }
862
+ /**
863
+ * Handle number/integer types with formats
864
+ */
865
+ number(schema, required) {
866
+ const type = schema.format === "int64" ? "bigint" : "number";
867
+ return appendOptional2(type, required);
854
868
  }
855
869
  handle(schema, required) {
856
- if (isRef(schema)) {
870
+ if (isRef2(schema)) {
857
871
  return this.ref(schema.$ref, required);
858
872
  }
859
873
  if (schema.allOf && Array.isArray(schema.allOf)) {
860
- return this.allOf(schema.allOf ?? []);
874
+ return this.allOf(schema.allOf);
861
875
  }
862
876
  if (schema.anyOf && Array.isArray(schema.anyOf)) {
863
- return this.anyOf(schema.anyOf ?? [], required);
877
+ return this.anyOf(schema.anyOf, required);
864
878
  }
865
- if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length) {
866
- return this.oneOf(schema.oneOf ?? [], required);
879
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
880
+ return this.oneOf(schema.oneOf, required);
867
881
  }
868
882
  if (schema.enum && Array.isArray(schema.enum)) {
869
883
  return this.enum(schema.enum, required);
870
884
  }
871
885
  const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
872
886
  if (!types.length) {
873
- return `z.unknown()${appendOptional2(required)}`;
874
- }
875
- if ("nullable" in schema && schema.nullable) {
876
- types.push("null");
887
+ if ("properties" in schema) {
888
+ return this.object(schema, required);
889
+ }
890
+ return appendOptional2("any", required);
877
891
  }
878
892
  if (types.length > 1) {
879
893
  const realTypes = types.filter((t) => t !== "null");
880
894
  if (realTypes.length === 1 && types.includes("null")) {
881
- return this.normal(realTypes[0], schema, required, true);
895
+ const tsType = this.normal(realTypes[0], schema, false);
896
+ return appendOptional2(`${tsType} | null`, required);
882
897
  }
883
- const subSchemas = types.map((t) => this.normal(t, schema, false));
884
- return `z.union([${subSchemas.join(", ")}])${appendOptional2(required)}`;
898
+ const typeResults = types.map((t) => this.normal(t, schema, false));
899
+ return appendOptional2(typeResults.join(" | "), required);
885
900
  }
886
- return this.normal(types[0], schema, required, false);
901
+ return this.normal(types[0], schema, required);
902
+ }
903
+ /**
904
+ * Generate an interface declaration
905
+ */
906
+ generateInterface(name, schema) {
907
+ const content = this.handle(schema, true);
908
+ return `interface ${name} ${content}`;
887
909
  }
888
910
  };
889
- function appendOptional2(isRequired) {
890
- return isRequired ? "" : ".optional()";
911
+ function appendOptional2(type, isRequired) {
912
+ return isRequired ? type : `${type} | undefined`;
891
913
  }
892
- function appendDefault(defaultValue) {
893
- return defaultValue !== void 0 ? `.default(${defaultValue})` : "";
914
+
915
+ // packages/typescript/src/lib/utils.ts
916
+ import { isRef as isRef3, removeDuplicates } from "@sdk-it/core";
917
+ function securityToOptions(security2, securitySchemes, staticIn) {
918
+ securitySchemes ??= {};
919
+ const options = {};
920
+ for (const it of security2) {
921
+ const [name] = Object.keys(it);
922
+ if (!name) {
923
+ continue;
924
+ }
925
+ const schema = securitySchemes[name];
926
+ if (isRef3(schema)) {
927
+ throw new Error(`Ref security schemas are not supported`);
928
+ }
929
+ if (schema.type === "http") {
930
+ options["authorization"] = {
931
+ in: staticIn ?? "header",
932
+ schema: "z.string().optional().transform((val) => (val ? `Bearer ${val}` : undefined))",
933
+ optionName: "token"
934
+ };
935
+ continue;
936
+ }
937
+ if (schema.type === "apiKey") {
938
+ if (!schema.in) {
939
+ throw new Error(`apiKey security schema must have an "in" field`);
940
+ }
941
+ if (!schema.name) {
942
+ throw new Error(`apiKey security schema must have a "name" field`);
943
+ }
944
+ options[schema.name] = {
945
+ in: staticIn ?? schema.in,
946
+ schema: "z.string().optional()"
947
+ };
948
+ continue;
949
+ }
950
+ }
951
+ return options;
952
+ }
953
+ function mergeImports(...imports) {
954
+ const merged = {};
955
+ for (const it of imports) {
956
+ merged[it.moduleSpecifier] = merged[it.moduleSpecifier] ?? {
957
+ moduleSpecifier: it.moduleSpecifier,
958
+ defaultImport: it.defaultImport,
959
+ namespaceImport: it.namespaceImport,
960
+ namedImports: []
961
+ };
962
+ for (const named of it.namedImports) {
963
+ if (!merged[it.moduleSpecifier].namedImports.some(
964
+ (x) => x.name === named.name
965
+ )) {
966
+ merged[it.moduleSpecifier].namedImports.push(named);
967
+ }
968
+ }
969
+ }
970
+ return Object.values(merged);
971
+ }
972
+ function importsToString(...imports) {
973
+ return imports.map((it) => {
974
+ if (it.defaultImport) {
975
+ return `import ${it.defaultImport} from '${it.moduleSpecifier}'`;
976
+ }
977
+ if (it.namespaceImport) {
978
+ return `import * as ${it.namespaceImport} from '${it.moduleSpecifier}'`;
979
+ }
980
+ if (it.namedImports) {
981
+ return `import {${removeDuplicates(it.namedImports, (it2) => it2.name).map((n) => `${n.isTypeOnly ? "type" : ""} ${n.name}`).join(", ")}} from '${it.moduleSpecifier}'`;
982
+ }
983
+ throw new Error(`Invalid import ${JSON.stringify(it)}`);
984
+ });
985
+ }
986
+ function exclude(list, exclude2) {
987
+ return list.filter((it) => !exclude2.includes(it));
988
+ }
989
+ function useImports(content, ...imports) {
990
+ const output = [];
991
+ for (const it of mergeImports(...imports)) {
992
+ const singleImport = it.defaultImport ?? it.namespaceImport;
993
+ if (singleImport && content.includes(singleImport)) {
994
+ output.push(importsToString(it).join("\n"));
995
+ } else if (it.namedImports.length) {
996
+ for (const namedImport of it.namedImports) {
997
+ if (content.includes(namedImport.name)) {
998
+ output.push(importsToString(it).join("\n"));
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ return output;
894
1004
  }
895
1005
 
896
- // packages/typescript/src/lib/generator.ts
897
- var statusCdeToMessageMap = {
1006
+ // packages/typescript/src/lib/sdk.ts
1007
+ function generateInputs(operationsSet, commonZod, makeImport) {
1008
+ const commonImports = commonZod.keys().toArray();
1009
+ const inputs = {};
1010
+ for (const [name, operations] of Object.entries(operationsSet)) {
1011
+ const output = [];
1012
+ const imports = /* @__PURE__ */ new Set(['import { z } from "zod";']);
1013
+ for (const operation of operations) {
1014
+ const schemaName = camelcase2(`${operation.name} schema`);
1015
+ const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject2(operation.schemas)};`;
1016
+ const inputContent = schema;
1017
+ for (const schema2 of commonImports) {
1018
+ if (inputContent.includes(schema2)) {
1019
+ imports.add(
1020
+ `import { ${schema2} } from './schemas/${makeImport(spinalcase(schema2))}';`
1021
+ );
1022
+ }
1023
+ }
1024
+ output.push(inputContent);
1025
+ }
1026
+ inputs[`inputs/${spinalcase(name)}.ts`] = [...imports, ...output].join("\n") + "\n";
1027
+ }
1028
+ const schemas = commonZod.entries().reduce((acc, [name, schema]) => {
1029
+ const output = [`import { z } from 'zod';`];
1030
+ const content = `export const ${name} = ${schema};`;
1031
+ for (const schema2 of commonImports) {
1032
+ const preciseMatch = new RegExp(`\\b${schema2}\\b`);
1033
+ if (preciseMatch.test(content) && schema2 !== name) {
1034
+ output.push(
1035
+ `import { ${schema2} } from './${makeImport(spinalcase(schema2))}';`
1036
+ );
1037
+ }
1038
+ }
1039
+ output.push(content);
1040
+ return [
1041
+ [`inputs/schemas/${spinalcase(name)}.ts`, output.join("\n")],
1042
+ ...acc
1043
+ ];
1044
+ }, []);
1045
+ return {
1046
+ ...Object.fromEntries(schemas),
1047
+ ...inputs
1048
+ };
1049
+ }
1050
+ function toEndpoint(groupName, spec, specOperation, operation, utils) {
1051
+ const schemaName = camelcase2(`${operation.name} schema`);
1052
+ const schemaRef = `${camelcase2(groupName)}.${schemaName}`;
1053
+ const inputHeaders = [];
1054
+ const inputQuery = [];
1055
+ const inputBody = [];
1056
+ const inputParams = [];
1057
+ const schemas = [];
1058
+ const responses = [];
1059
+ for (const [name, prop] of Object.entries(operation.inputs)) {
1060
+ if (prop.in === "headers" || prop.in === "header") {
1061
+ inputHeaders.push(`"${name}"`);
1062
+ } else if (prop.in === "query") {
1063
+ inputQuery.push(`"${name}"`);
1064
+ } else if (prop.in === "body") {
1065
+ inputBody.push(`"${name}"`);
1066
+ } else if (prop.in === "path") {
1067
+ inputParams.push(`"${name}"`);
1068
+ } else if (prop.in === "internal") {
1069
+ continue;
1070
+ } else {
1071
+ throw new Error(
1072
+ `Unknown source ${prop.in} in ${name} ${JSON.stringify(
1073
+ prop
1074
+ )} in ${operation.name}`
1075
+ );
1076
+ }
1077
+ }
1078
+ specOperation.responses ??= {};
1079
+ const outputs = [];
1080
+ const statusesCount = Object.keys(specOperation.responses).filter((status) => {
1081
+ const statusCode = +status;
1082
+ return statusCode >= 200 && statusCode < 300;
1083
+ }).length > 1;
1084
+ for (const status in specOperation.responses) {
1085
+ const response = isRef4(specOperation.responses[status]) ? followRef3(spec, specOperation.responses[status].$ref) : specOperation.responses[status];
1086
+ const handled = handleResponse(
1087
+ spec,
1088
+ operation.name,
1089
+ status,
1090
+ response,
1091
+ utils,
1092
+ true
1093
+ // statusesCount,
1094
+ );
1095
+ responses.push(handled);
1096
+ outputs.push(...handled.outputs);
1097
+ }
1098
+ const addTypeParser = Object.keys(operation.schemas).length > 1;
1099
+ for (const type in operation.schemas ?? {}) {
1100
+ let typePrefix = "";
1101
+ if (addTypeParser && type !== "json") {
1102
+ typePrefix = `${type} `;
1103
+ }
1104
+ const endpoint = `${typePrefix}${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
1105
+ schemas.push(
1106
+ `"${endpoint}": {
1107
+ schema: ${schemaRef}${addTypeParser ? `.${type}` : ""},
1108
+ output:[${outputs.join(",")}],
1109
+ toRequest(input: z.infer<typeof ${schemaRef}${addTypeParser ? `.${type}` : ""}>) {
1110
+ const endpoint = '${endpoint}';
1111
+ return toRequest(endpoint, ${operation.outgoingContentType || "nobody"}(input, {
1112
+ inputHeaders: [${inputHeaders}],
1113
+ inputQuery: [${inputQuery}],
1114
+ inputBody: [${inputBody}],
1115
+ inputParams: [${inputParams}],
1116
+ }));
1117
+ },
1118
+ }`
1119
+ );
1120
+ }
1121
+ return { responses, schemas };
1122
+ }
1123
+ var statusCodeToResponseMap = {
1124
+ "200": "Ok",
1125
+ "201": "Created",
1126
+ "202": "Accepted",
1127
+ "204": "NoContent",
898
1128
  "400": "BadRequest",
899
1129
  "401": "Unauthorized",
900
1130
  "402": "PaymentRequired",
@@ -913,8 +1143,97 @@ var statusCdeToMessageMap = {
913
1143
  "503": "ServiceUnavailable",
914
1144
  "504": "GatewayTimeout"
915
1145
  };
1146
+ function handleResponse(spec, operationName, status, response, utils, numbered) {
1147
+ const schemas = {};
1148
+ const imports = {};
1149
+ const endpointImports = {
1150
+ ParseError: {
1151
+ defaultImport: void 0,
1152
+ isTypeOnly: false,
1153
+ moduleSpecifier: utils.makeImport(`../http/parser`),
1154
+ namedImports: [{ isTypeOnly: false, name: "ParseError" }],
1155
+ namespaceImport: void 0
1156
+ }
1157
+ };
1158
+ const responses = [];
1159
+ const outputs = [];
1160
+ const typeScriptDeserialzer = new TypeScriptDeserialzer(
1161
+ spec,
1162
+ (schemaName, zod) => {
1163
+ schemas[schemaName] = zod;
1164
+ imports[schemaName] = {
1165
+ defaultImport: void 0,
1166
+ isTypeOnly: true,
1167
+ moduleSpecifier: `../models/${utils.makeImport(schemaName)}`,
1168
+ namedImports: [{ isTypeOnly: true, name: schemaName }],
1169
+ namespaceImport: void 0
1170
+ };
1171
+ }
1172
+ );
1173
+ const statusCode = +status;
1174
+ const parser = (response.headers ?? {})["Transfer-Encoding"] ? "chunked" : "buffered";
1175
+ const statusName = statusCodeToResponseMap[status] || "APIResponse";
1176
+ const interfaceName = pascalcase(
1177
+ operationName + ` output${numbered ? status : ""}`
1178
+ );
1179
+ if (statusCode === 204) {
1180
+ outputs.push(statusName);
1181
+ } else {
1182
+ if (status.endsWith("XX")) {
1183
+ outputs.push(`APIError<${interfaceName}>`);
1184
+ } else {
1185
+ outputs.push(
1186
+ parser !== "buffered" ? `{type: ${statusName}<${interfaceName}>, parser: ${parser}}` : `${statusName}<${interfaceName}>`
1187
+ );
1188
+ }
1189
+ }
1190
+ const responseContent = get(response, ["content"]);
1191
+ const isJson = responseContent && responseContent["application/json"];
1192
+ const responseSchema = isJson ? typeScriptDeserialzer.handle(
1193
+ responseContent["application/json"].schema,
1194
+ true
1195
+ ) : "void";
1196
+ responses.push({
1197
+ name: interfaceName,
1198
+ schema: responseSchema
1199
+ });
1200
+ const statusGroup = +status.slice(0, 1);
1201
+ if (statusCode >= 400 || statusGroup >= 4) {
1202
+ endpointImports[statusCodeToResponseMap[status] ?? "APIError"] = {
1203
+ moduleSpecifier: utils.makeImport("../http/response"),
1204
+ namedImports: [{ name: statusCodeToResponseMap[status] ?? "APIError" }]
1205
+ };
1206
+ endpointImports[interfaceName] = {
1207
+ isTypeOnly: true,
1208
+ moduleSpecifier: `../outputs/${utils.makeImport(spinalcase(operationName))}`,
1209
+ namedImports: [{ isTypeOnly: true, name: interfaceName }]
1210
+ };
1211
+ } else if (statusCode >= 200 && statusCode < 300 || statusCode >= 2 || statusGroup <= 3) {
1212
+ endpointImports[statusName] = {
1213
+ moduleSpecifier: utils.makeImport("../http/response"),
1214
+ namedImports: [
1215
+ {
1216
+ isTypeOnly: false,
1217
+ name: statusName
1218
+ }
1219
+ ]
1220
+ };
1221
+ endpointImports[interfaceName] = {
1222
+ defaultImport: void 0,
1223
+ isTypeOnly: true,
1224
+ moduleSpecifier: `../outputs/${utils.makeImport(spinalcase(operationName))}`,
1225
+ namedImports: [{ isTypeOnly: true, name: interfaceName }],
1226
+ namespaceImport: void 0
1227
+ };
1228
+ }
1229
+ return { schemas, imports, endpointImports, responses, outputs };
1230
+ }
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
+
1235
+ // packages/typescript/src/lib/generator.ts
916
1236
  function generateCode(config) {
917
- const commonSchemas = {};
918
1237
  const commonZod = /* @__PURE__ */ new Map();
919
1238
  const commonZodImports = [];
920
1239
  const zodDeserialzer = new ZodDeserialzer(config.spec, (model, schema) => {
@@ -929,14 +1248,15 @@ function generateCode(config) {
929
1248
  });
930
1249
  const groups = {};
931
1250
  const outputs = {};
1251
+ const endpoints = {};
932
1252
  forEachOperation(config, (entry, operation) => {
933
1253
  console.log(`Processing ${entry.method} ${entry.path}`);
934
- const [groupName] = Array.isArray(operation.tags) ? operation.tags : ["unknown"];
935
- groups[groupName] ??= [];
1254
+ groups[entry.groupName] ??= [];
1255
+ endpoints[entry.groupName] ??= [];
936
1256
  const inputs = {};
937
1257
  const additionalProperties = [];
938
1258
  for (const param of operation.parameters ?? []) {
939
- if (isRef(param)) {
1259
+ if (isRef5(param)) {
940
1260
  throw new Error(`Found reference in parameter ${param.$ref}`);
941
1261
  }
942
1262
  if (!param.schema) {
@@ -964,24 +1284,40 @@ function generateCode(config) {
964
1284
  })
965
1285
  )
966
1286
  );
967
- const types = {};
1287
+ const schemas = {};
968
1288
  const shortContenTypeMap = {
969
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
970
1294
  "application/x-www-form-urlencoded": "urlencoded",
971
1295
  "multipart/form-data": "formdata",
972
1296
  "application/xml": "xml",
973
1297
  "text/plain": "text"
974
1298
  };
975
1299
  let outgoingContentType;
976
- if (operation.requestBody && Object.keys(operation.requestBody).length) {
977
- const content = isRef(operation.requestBody) ? get2(followRef(config.spec, operation.requestBody.$ref), ["content"]) : operation.requestBody.content;
978
- for (const type in content) {
979
- const ctSchema = isRef(content[type].schema) ? followRef(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;
980
1304
  if (!ctSchema) {
981
- console.warn(`Schema not found for ${type}`);
1305
+ console.warn(
1306
+ `Schema not found for ${type} in ${entry.method} ${entry.path}`
1307
+ );
982
1308
  continue;
983
1309
  }
984
- 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, {
985
1321
  required: additionalProperties.filter((p) => p.required).map((p) => p.name),
986
1322
  properties: additionalProperties.reduce(
987
1323
  (acc, p) => ({
@@ -991,14 +1327,14 @@ function generateCode(config) {
991
1327
  {}
992
1328
  )
993
1329
  });
994
- Object.assign(inputs, bodyInputs(config, ctSchema));
995
- types[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
1330
+ Object.assign(inputs, bodyInputs(config, objectSchema));
1331
+ schemas[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
996
1332
  }
997
- if (content["application/json"]) {
1333
+ if (requestBody.content["application/json"]) {
998
1334
  outgoingContentType = "json";
999
- } else if (content["application/x-www-form-urlencoded"]) {
1335
+ } else if (requestBody.content["application/x-www-form-urlencoded"]) {
1000
1336
  outgoingContentType = "urlencoded";
1001
- } else if (content["multipart/form-data"]) {
1337
+ } else if (requestBody.content["multipart/form-data"]) {
1002
1338
  outgoingContentType = "formdata";
1003
1339
  } else {
1004
1340
  outgoingContentType = "json";
@@ -1011,7 +1347,7 @@ function generateCode(config) {
1011
1347
  }),
1012
1348
  {}
1013
1349
  );
1014
- types[shortContenTypeMap["application/json"]] = zodDeserialzer.handle(
1350
+ schemas[shortContenTypeMap["application/json"]] = zodDeserialzer.handle(
1015
1351
  {
1016
1352
  type: "object",
1017
1353
  required: additionalProperties.filter((p) => p.required).map((p) => p.name),
@@ -1020,90 +1356,137 @@ function generateCode(config) {
1020
1356
  true
1021
1357
  );
1022
1358
  }
1023
- const errors = [];
1024
- operation.responses ??= {};
1025
- let foundResponse = false;
1359
+ const endpoint = toEndpoint(
1360
+ entry.groupName,
1361
+ config.spec,
1362
+ operation,
1363
+ {
1364
+ outgoingContentType,
1365
+ name: operation.operationId,
1366
+ type: "http",
1367
+ trigger: entry,
1368
+ schemas,
1369
+ inputs
1370
+ },
1371
+ { makeImport: config.makeImport }
1372
+ );
1026
1373
  const output = [`import z from 'zod';`];
1027
- let parser = "buffered";
1028
- const responses = [];
1029
- const responsesImports = {};
1030
- for (const status in operation.responses) {
1031
- const response = isRef(
1032
- operation.responses[status]
1033
- ) ? followRef(
1034
- config.spec,
1035
- operation.responses[status].$ref
1036
- ) : operation.responses[status];
1037
- const statusCode = +status;
1038
- if (statusCode >= 400) {
1039
- errors.push(statusCdeToMessageMap[status] ?? "ProblematicResponse");
1040
- }
1041
- if (statusCode >= 200 && statusCode < 300) {
1042
- foundResponse = true;
1043
- const responseContent = get2(response, ["content"]);
1044
- const isJson = responseContent && responseContent["application/json"];
1045
- if ((response.headers ?? {})["Transfer-Encoding"]) {
1046
- parser = "chunked";
1047
- }
1048
- const typeScriptDeserialzer = new TypeScriptDeserialzer(
1049
- config.spec,
1050
- (schemaName, zod) => {
1051
- commonSchemas[schemaName] = zod;
1052
- responsesImports[schemaName] = {
1053
- defaultImport: void 0,
1054
- isTypeOnly: true,
1055
- moduleSpecifier: `../models/${config.makeImport(schemaName)}`,
1056
- namedImports: [{ isTypeOnly: true, name: schemaName }],
1057
- namespaceImport: void 0
1058
- };
1059
- }
1060
- );
1061
- const responseSchema = isJson ? typeScriptDeserialzer.handle(
1062
- responseContent["application/json"].schema,
1063
- true
1064
- ) : statusCode === 204 ? "void" : "ReadableStream";
1065
- responses.push(responseSchema);
1066
- }
1067
- }
1068
- if (responses.length > 1) {
1374
+ const responses = endpoint.responses.flatMap((it) => it.responses);
1375
+ const responsesImports = endpoint.responses.flatMap(
1376
+ (it) => Object.values(it.imports)
1377
+ );
1378
+ if (responses.length) {
1069
1379
  output.push(
1070
- `export type ${pascalcase(entry.name + " output")} = ${removeDuplicates3(
1071
- responses,
1072
- (it) => it
1073
- ).join(" | ")};`
1380
+ ...responses.map((it) => `export type ${it.name} = ${it.schema};`)
1074
1381
  );
1075
1382
  } else {
1076
1383
  output.push(
1077
- `export type ${pascalcase(entry.name + " output")} = ${responses[0]};`
1384
+ `export type ${pascalcase2(operation.operationId + " output")} = void;`
1078
1385
  );
1079
1386
  }
1080
- output.push(
1081
- ...useImports(output.join(""), Object.values(responsesImports))
1082
- );
1083
- if (!foundResponse) {
1084
- output.push(`export type ${pascalcase(entry.name + " output")} = void`);
1085
- }
1086
- outputs[`${spinalcase2(entry.name)}.ts`] = output.join("\n");
1087
- groups[groupName].push({
1088
- name: entry.name,
1387
+ output.unshift(...useImports(output.join(""), ...responsesImports));
1388
+ outputs[`${spinalcase2(operation.operationId)}.ts`] = output.join("\n");
1389
+ endpoints[entry.groupName].push(endpoint);
1390
+ groups[entry.groupName].push({
1391
+ name: operation.operationId,
1089
1392
  type: "http",
1090
1393
  inputs,
1091
- errors: errors.length ? errors : ["ServerError"],
1092
1394
  outgoingContentType,
1093
- schemas: types,
1094
- parser,
1095
- formatOutput: () => ({
1096
- import: pascalcase(entry.name + " output"),
1097
- use: pascalcase(entry.name + " output")
1098
- }),
1395
+ schemas,
1099
1396
  trigger: entry
1100
1397
  });
1101
1398
  });
1102
- return { groups, commonSchemas, commonZod, outputs };
1399
+ const commonSchemas = Object.values(endpoints).reduce(
1400
+ (acc, endpoint) => ({
1401
+ ...acc,
1402
+ ...endpoint.reduce(
1403
+ (acc2, { responses }) => ({
1404
+ ...acc2,
1405
+ ...responses.reduce(
1406
+ (acc3, it) => ({ ...acc3, ...it.schemas }),
1407
+ {}
1408
+ )
1409
+ }),
1410
+ {}
1411
+ )
1412
+ }),
1413
+ {}
1414
+ );
1415
+ const allSchemas = Object.keys(endpoints).map((it) => ({
1416
+ import: `import ${camelcase3(it)} from './${config.makeImport(spinalcase2(it))}';`,
1417
+ use: ` ...${camelcase3(it)}`
1418
+ }));
1419
+ const imports = [
1420
+ 'import z from "zod";',
1421
+ `import type { ParseError } from '${config.makeImport("../http/parser")}';`,
1422
+ `import type { ServerError } from '${config.makeImport("../http/response")}';`,
1423
+ `import type { OutputType, Parser, Type } from '../http/send-request.ts';`
1424
+ ];
1425
+ return {
1426
+ groups,
1427
+ commonSchemas,
1428
+ commonZod,
1429
+ outputs,
1430
+ endpoints: {
1431
+ [join("api", "endpoints.ts")]: `
1432
+
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
+ )}';
1442
+
1443
+ import schemas from '${config.makeImport("./schemas")}';
1444
+
1445
+ ${endpoints_default}`,
1446
+ [`${join("api", "schemas.ts")}`]: `${allSchemas.map((it) => it.import).join("\n")}
1447
+
1448
+ export default {
1449
+ ${allSchemas.map((it) => it.use).join(",\n")}
1450
+ };
1451
+
1452
+ `.trim(),
1453
+ ...Object.fromEntries(
1454
+ Object.entries(endpoints).map(([name, endpoint]) => {
1455
+ const imps = importsToString(
1456
+ ...mergeImports(
1457
+ ...endpoint.flatMap(
1458
+ (it) => it.responses.flatMap(
1459
+ (it2) => Object.values(it2.endpointImports)
1460
+ )
1461
+ )
1462
+ )
1463
+ );
1464
+ return [
1465
+ [
1466
+ join("api", `${spinalcase2(name)}.ts`),
1467
+ `${[
1468
+ ...imps,
1469
+ // ...imports,
1470
+ `import z from 'zod';`,
1471
+ `import { toRequest, json, urlencoded, nobody, formdata, createUrl } from '${config.makeImport("../http/request")}';`,
1472
+ `import { chunked, buffered } from "${config.makeImport("../http/parse-response")}";`,
1473
+ `import * as ${camelcase3(name)} from '../inputs/${config.makeImport(spinalcase2(name))}';`
1474
+ ].join(
1475
+ "\n"
1476
+ )}
1477
+ export default {
1478
+ ${endpoint.flatMap((it) => it.schemas).join(",\n")}
1479
+ }`
1480
+ ]
1481
+ ];
1482
+ }).flat()
1483
+ )
1484
+ }
1485
+ };
1103
1486
  }
1104
1487
  function toProps(spec, schemaOrRef, aggregator = []) {
1105
- if (isRef(schemaOrRef)) {
1106
- const schema = followRef(spec, schemaOrRef.$ref);
1488
+ if (isRef5(schemaOrRef)) {
1489
+ const schema = followRef4(spec, schemaOrRef.$ref);
1107
1490
  return toProps(spec, schema, aggregator);
1108
1491
  } else if (schemaOrRef.type === "object") {
1109
1492
  for (const [name] of Object.entries(schemaOrRef.properties ?? {})) {
@@ -1148,22 +1531,22 @@ function bodyInputs(config, ctSchema) {
1148
1531
  }
1149
1532
 
1150
1533
  // packages/typescript/src/lib/http/interceptors.txt
1151
- 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 = (getBaseUrl: () => string) :Interceptor => {\n return {\n before({init, url}) {\n const baseUrl = getBaseUrl();\n if (url.protocol === 'local:') {\n return {init, url: new URL(url.pathname, baseUrl)}\n }\n return {init, url}\n },\n };\n};\n\nexport const logInterceptor = {\n before(request: Request) {\n console.log('Request', request);\n return request;\n },\n after(response: 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";
1152
1535
 
1153
1536
  // packages/typescript/src/lib/http/parse-response.txt
1154
- 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';
1155
1538
 
1156
1539
  // packages/typescript/src/lib/http/parser.txt
1157
- var parser_default = "import { z } from 'zod';\n\nexport type ParseError<T extends z.ZodType<any, any, any>> = {\n kind: 'parse';\n} & z.inferFlattenedErrors<T>;\n\nexport function parse<T extends z.ZodType>(\n schema: T,\n input: unknown,\n) {\n const result = schema.safeParse(input);\n if (!result.success) {\n const errors = result.error.flatten((issue) => issue);\n return [null, errors];\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";
1158
1541
 
1159
1542
  // packages/typescript/src/lib/http/request.txt
1160
- 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";
1161
1544
 
1162
1545
  // packages/typescript/src/lib/http/response.txt
1163
- var response_default = "export interface ApiResponse<Status extends number, Body extends unknown> {\n kind: 'response';\n status: Status;\n body: Body;\n}\n\n// 4xx Client Errors\nexport type BadRequest = ApiResponse<400, { message: string }>;\nexport type Unauthorized = ApiResponse<401, { message: string }>;\nexport type PaymentRequired = ApiResponse<402, { message: string }>;\nexport type Forbidden = ApiResponse<403, { message: string }>;\nexport type NotFound = ApiResponse<404, { message: string }>;\nexport type MethodNotAllowed = ApiResponse<405, { message: string }>;\nexport type NotAcceptable = ApiResponse<406, { message: string }>;\nexport type Conflict = ApiResponse<409, { message: string }>;\nexport type Gone = ApiResponse<410, { message: string }>;\nexport type UnprocessableEntity = ApiResponse<422, { message: string; errors?: Record<string, string[]> }>;\nexport type TooManyRequests = ApiResponse<429, { message: string; retryAfter?: string }>;\nexport type PayloadTooLarge = ApiResponse<413, { message: string; }>;\nexport type UnsupportedMediaType = ApiResponse<415, { message: string; }>;\n\n// 5xx Server Errors\nexport type InternalServerError = ApiResponse<500, { message: string }>;\nexport type NotImplemented = ApiResponse<501, { message: string }>;\nexport type BadGateway = ApiResponse<502, { message: string }>;\nexport type ServiceUnavailable = ApiResponse<503, { message: string; retryAfter?: string }>;\nexport type GatewayTimeout = ApiResponse<504, { message: string }>;\n\nexport type ClientError =\n | BadRequest\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;";
1164
1547
 
1165
1548
  // packages/typescript/src/lib/http/send-request.txt
1166
- var send_request_default = "\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (input: any) => RequestConfig;\n deserializer: (response: Response) => Promise<unknown> | unknown;\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)(new Request(config.url, config.init), {\n ...config.init,\n signal: options.signal,\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 if (response.ok) {\n const data = await route.deserializer(response);\n return [data as never, null] as const;\n }\n const error = await handleError(response);\n return [null as never, { ...error, kind: 'response' }] as const;\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";
1167
1550
 
1168
1551
  // packages/typescript/src/lib/generate.ts
1169
1552
  function security(spec) {
@@ -1191,21 +1574,16 @@ async function generate(spec, settings) {
1191
1574
  const makeImport = (moduleSpecifier) => {
1192
1575
  return settings.useTsExtension ? `${moduleSpecifier}.ts` : moduleSpecifier;
1193
1576
  };
1194
- const { commonSchemas, groups, outputs, commonZod } = generateCode({
1195
- spec,
1196
- style: "github",
1197
- makeImport
1198
- });
1199
- const output = settings.mode === "full" ? join(settings.output, "src") : settings.output;
1577
+ const { commonSchemas, endpoints, groups, outputs, commonZod } = generateCode(
1578
+ {
1579
+ spec,
1580
+ style: "github",
1581
+ makeImport
1582
+ }
1583
+ );
1584
+ const output = settings.mode === "full" ? join2(settings.output, "src") : settings.output;
1200
1585
  const options = security(spec);
1201
1586
  const clientName = settings.name || "Client";
1202
- const clientFiles = generateSDK({
1203
- name: clientName,
1204
- operations: groups,
1205
- servers: spec.servers?.map((server) => server.url) || [],
1206
- options,
1207
- makeImport
1208
- });
1209
1587
  const inputFiles = generateInputs(groups, commonZod, makeImport);
1210
1588
  await writeFiles(output, {
1211
1589
  "outputs/.gitkeep": "",
@@ -1213,24 +1591,34 @@ async function generate(spec, settings) {
1213
1591
  "models/.getkeep": ""
1214
1592
  // 'README.md': readme,
1215
1593
  });
1216
- await writeFiles(join(output, "http"), {
1217
- "interceptors.ts": interceptors_default,
1594
+ await writeFiles(join2(output, "http"), {
1595
+ "interceptors.ts": `
1596
+ import { type RequestConfig } from './${makeImport("request")}';
1597
+ ${interceptors_default}`,
1218
1598
  "parse-response.ts": parse_response_default,
1219
1599
  "send-request.ts": `import z from 'zod';
1220
1600
  import type { Interceptor } from './${makeImport("interceptors")}';
1221
- import { handleError } from './${makeImport("parse-response")}';
1222
- import { parse } from './${makeImport("parser")}';
1223
- import type { RequestConfig } from './request.ts';
1601
+ import { buffered } from './${makeImport("parse-response")}';
1602
+ import { parseInput } from './${makeImport("parser")}';
1603
+ import type { RequestConfig } from './${makeImport("request")}';
1604
+ import { APIError, APIResponse } from './${makeImport("response")}';
1605
+
1224
1606
  ${send_request_default}`,
1225
1607
  "response.ts": response_default,
1226
1608
  "parser.ts": parser_default,
1227
1609
  "request.ts": request_default
1228
1610
  });
1229
- await writeFiles(join(output, "outputs"), outputs);
1611
+ await writeFiles(join2(output, "outputs"), outputs);
1230
1612
  const modelsImports = Object.entries(commonSchemas).map(([name]) => name);
1231
1613
  await writeFiles(output, {
1232
- ...clientFiles,
1614
+ "client.ts": client_default({
1615
+ name: clientName,
1616
+ servers: (spec.servers ?? []).map((server) => server.url) || [],
1617
+ options,
1618
+ makeImport
1619
+ }),
1233
1620
  ...inputFiles,
1621
+ ...endpoints,
1234
1622
  ...Object.fromEntries(
1235
1623
  Object.entries(commonSchemas).map(([name, schema]) => [
1236
1624
  `models/${name}.ts`,
@@ -1245,29 +1633,36 @@ ${send_request_default}`,
1245
1633
  )
1246
1634
  });
1247
1635
  const folders = [
1248
- getFolderExports(join(output, "outputs"), settings.useTsExtension),
1636
+ getFolderExports(join2(output, "outputs"), settings.useTsExtension),
1249
1637
  getFolderExports(
1250
- join(output, "inputs"),
1638
+ join2(output, "inputs"),
1251
1639
  settings.useTsExtension,
1252
1640
  ["ts"],
1253
- (dirent) => dirent.isDirectory() && dirent.name === "schemas"
1641
+ (dirent) => dirent.isDirectory() && ["schemas"].includes(dirent.name)
1254
1642
  ),
1255
- getFolderExports(join(output, "http"), settings.useTsExtension)
1643
+ getFolderExports(join2(output, "api"), settings.useTsExtension),
1644
+ getFolderExports(
1645
+ join2(output, "http"),
1646
+ settings.useTsExtension,
1647
+ ["ts"],
1648
+ (dirent) => dirent.name !== "response.ts"
1649
+ )
1256
1650
  ];
1257
1651
  if (modelsImports.length) {
1258
1652
  folders.push(
1259
- getFolderExports(join(output, "models"), settings.useTsExtension)
1653
+ getFolderExports(join2(output, "models"), settings.useTsExtension)
1260
1654
  );
1261
1655
  }
1262
- const [outputIndex, inputsIndex, httpIndex, modelsIndex] = await Promise.all(folders);
1656
+ const [outputIndex, inputsIndex, apiIndex, httpIndex, modelsIndex] = await Promise.all(folders);
1263
1657
  await writeFiles(output, {
1658
+ "api/index.ts": apiIndex,
1264
1659
  "outputs/index.ts": outputIndex,
1265
1660
  "inputs/index.ts": inputsIndex || null,
1266
1661
  "http/index.ts": httpIndex,
1267
1662
  ...modelsImports.length ? { "models/index.ts": modelsIndex } : {}
1268
1663
  });
1269
1664
  await writeFiles(output, {
1270
- "index.ts": await getFolderExports(output, settings.useTsExtension)
1665
+ "index.ts": await getFolderExports(output, settings.useTsExtension, ["ts"])
1271
1666
  });
1272
1667
  if (settings.mode === "full") {
1273
1668
  await writeFiles(settings.output, {