@sdk-it/typescript 0.15.0 → 0.16.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 +805 -718
- package/dist/index.js.map +4 -4
- package/dist/lib/client.d.ts +1 -1
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/emitters/interface.d.ts.map +1 -1
- package/dist/lib/emitters/zod.d.ts +4 -4
- package/dist/lib/emitters/zod.d.ts.map +1 -1
- package/dist/lib/generate.d.ts.map +1 -1
- package/dist/lib/generator.d.ts +2 -0
- package/dist/lib/generator.d.ts.map +1 -1
- package/dist/lib/readme-generator.d.ts.map +1 -1
- package/dist/lib/sdk.d.ts +18 -13
- package/dist/lib/sdk.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +7 -14
- package/dist/lib/utils.d.ts.map +1 -1
- package/package.json +2 -2
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) => {
|
|
@@ -46,8 +30,8 @@ var client_default = (spec) => {
|
|
|
46
30
|
return `
|
|
47
31
|
import { fetchType, sendRequest } from './http/${spec.makeImport("send-request")}';
|
|
48
32
|
import z from 'zod';
|
|
49
|
-
import type { Endpoints } from '
|
|
50
|
-
import schemas from '
|
|
33
|
+
import type { Endpoints } from './api/${spec.makeImport("schemas")}';
|
|
34
|
+
import schemas from './api/${spec.makeImport("schemas")}';
|
|
51
35
|
import {
|
|
52
36
|
createBaseUrlInterceptor,
|
|
53
37
|
createHeadersInterceptor,
|
|
@@ -80,557 +64,35 @@ export class ${spec.name} {
|
|
|
80
64
|
signal: options?.signal,
|
|
81
65
|
});
|
|
82
66
|
}
|
|
83
|
-
|
|
84
|
-
get defaultHeaders() {
|
|
85
|
-
return ${defaultHeaders}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
get #defaultInputs() {
|
|
89
|
-
return ${defaultInputs}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
setOptions(options: Partial<${spec.name}Options>) {
|
|
93
|
-
const validated = optionsSchema.partial().parse(options);
|
|
94
|
-
|
|
95
|
-
for (const key of Object.keys(validated) as (keyof ${spec.name}Options)[]) {
|
|
96
|
-
if (validated[key] !== undefined) {
|
|
97
|
-
(this.options[key] as typeof validated[typeof key]) = validated[key]!;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}`;
|
|
102
|
-
};
|
|
103
|
-
|
|
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
|
-
}`;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
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))}';`
|
|
239
|
-
);
|
|
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
|
-
}
|
|
271
|
-
}
|
|
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(/^#\//, "");
|
|
288
|
-
}
|
|
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);
|
|
299
|
-
}
|
|
300
|
-
return entry;
|
|
301
|
-
}
|
|
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";
|
|
577
|
-
}
|
|
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
|
-
}
|
|
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);
|
|
607
|
-
}
|
|
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);
|
|
67
|
+
|
|
68
|
+
get defaultHeaders() {
|
|
69
|
+
return ${defaultHeaders}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get #defaultInputs() {
|
|
73
|
+
return ${defaultInputs}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setOptions(options: Partial<${spec.name}Options>) {
|
|
77
|
+
const validated = optionsSchema.partial().parse(options);
|
|
78
|
+
|
|
79
|
+
for (const key of Object.keys(validated) as (keyof ${spec.name}Options)[]) {
|
|
80
|
+
if (validated[key] !== undefined) {
|
|
81
|
+
(this.options[key] as typeof validated[typeof key]) = validated[key]!;
|
|
615
82
|
}
|
|
616
|
-
const typeResults = types.map((t) => this.normal(t, schema, false));
|
|
617
|
-
return appendOptional(typeResults.join(" | "), required);
|
|
618
83
|
}
|
|
619
|
-
return this.normal(types[0], schema, required);
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Generate an interface declaration
|
|
623
|
-
*/
|
|
624
|
-
generateInterface(name, schema) {
|
|
625
|
-
const content = this.handle(schema, true);
|
|
626
|
-
return `interface ${name} ${content}`;
|
|
627
84
|
}
|
|
85
|
+
}`;
|
|
628
86
|
};
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
87
|
+
|
|
88
|
+
// packages/typescript/src/lib/generator.ts
|
|
89
|
+
import { get as get2, merge } from "lodash-es";
|
|
90
|
+
import { join } from "node:path";
|
|
91
|
+
import { camelcase as camelcase2, pascalcase as pascalcase2, spinalcase as spinalcase2 } from "stringcase";
|
|
92
|
+
import { followRef as followRef4, forEachOperation, isRef as isRef5 } from "@sdk-it/core";
|
|
632
93
|
|
|
633
94
|
// packages/typescript/src/lib/emitters/zod.ts
|
|
95
|
+
import { cleanRef, followRef, isRef, parseRef } from "@sdk-it/core";
|
|
634
96
|
var ZodDeserialzer = class {
|
|
635
97
|
circularRefTracker = /* @__PURE__ */ new Set();
|
|
636
98
|
#spec;
|
|
@@ -642,12 +104,11 @@ var ZodDeserialzer = class {
|
|
|
642
104
|
/**
|
|
643
105
|
* Handle objects (properties, additionalProperties).
|
|
644
106
|
*/
|
|
645
|
-
object(schema
|
|
107
|
+
object(schema) {
|
|
646
108
|
const properties = schema.properties || {};
|
|
647
109
|
const propEntries = Object.entries(properties).map(([key, propSchema]) => {
|
|
648
110
|
const isRequired = (schema.required ?? []).includes(key);
|
|
649
|
-
|
|
650
|
-
return `'${key}': ${zodPart}`;
|
|
111
|
+
return `'${key}': ${this.handle(propSchema, isRequired)}`;
|
|
651
112
|
});
|
|
652
113
|
let additionalProps = "";
|
|
653
114
|
if (schema.additionalProperties) {
|
|
@@ -658,8 +119,7 @@ var ZodDeserialzer = class {
|
|
|
658
119
|
additionalProps = `.catchall(z.unknown())`;
|
|
659
120
|
}
|
|
660
121
|
}
|
|
661
|
-
|
|
662
|
-
return `${objectSchema}${appendOptional2(required)}`;
|
|
122
|
+
return `z.object({${propEntries.join(", ")}})${additionalProps}`;
|
|
663
123
|
}
|
|
664
124
|
/**
|
|
665
125
|
* Handle arrays (items could be a single schema or a tuple (array of schemas)).
|
|
@@ -668,18 +128,18 @@ var ZodDeserialzer = class {
|
|
|
668
128
|
array(schema, required = false) {
|
|
669
129
|
const { items } = schema;
|
|
670
130
|
if (!items) {
|
|
671
|
-
return `z.array(z.unknown())${
|
|
131
|
+
return `z.array(z.unknown())${appendOptional(required)}`;
|
|
672
132
|
}
|
|
673
133
|
if (Array.isArray(items)) {
|
|
674
134
|
const tupleItems = items.map((sub) => this.handle(sub, true));
|
|
675
135
|
const base = `z.tuple([${tupleItems.join(", ")}])`;
|
|
676
|
-
return `${base}${
|
|
136
|
+
return `${base}${appendOptional(required)}`;
|
|
677
137
|
}
|
|
678
138
|
const itemsSchema = this.handle(items, true);
|
|
679
|
-
return `z.array(${itemsSchema})${
|
|
139
|
+
return `z.array(${itemsSchema})${appendOptional(required)}`;
|
|
680
140
|
}
|
|
681
141
|
#suffixes = (defaultValue, required, nullable) => {
|
|
682
|
-
return `${nullable ? ".nullable()" : ""}${appendDefault(defaultValue)}${
|
|
142
|
+
return `${nullable ? ".nullable()" : ""}${appendDefault(defaultValue)}${appendOptional(required)}`;
|
|
683
143
|
};
|
|
684
144
|
/**
|
|
685
145
|
* Convert a basic type (string | number | boolean | object | array, etc.) to Zod.
|
|
@@ -697,13 +157,13 @@ var ZodDeserialzer = class {
|
|
|
697
157
|
case "boolean":
|
|
698
158
|
return `z.boolean()${this.#suffixes(schema.default, required, nullable)}`;
|
|
699
159
|
case "object":
|
|
700
|
-
return this.object(schema,
|
|
160
|
+
return `${this.object(schema)}${this.#suffixes(JSON.stringify(schema.default), required, nullable)}`;
|
|
701
161
|
case "array":
|
|
702
162
|
return this.array(schema, required);
|
|
703
163
|
case "null":
|
|
704
|
-
return `z.null()${
|
|
164
|
+
return `z.null()${appendOptional(required)}`;
|
|
705
165
|
default:
|
|
706
|
-
return `z.unknown()${
|
|
166
|
+
return `z.unknown()${appendOptional(required)}`;
|
|
707
167
|
}
|
|
708
168
|
}
|
|
709
169
|
ref($ref, required) {
|
|
@@ -719,15 +179,15 @@ var ZodDeserialzer = class {
|
|
|
719
179
|
this.circularRefTracker.delete(schemaName);
|
|
720
180
|
return schemaName;
|
|
721
181
|
}
|
|
722
|
-
allOf(schemas) {
|
|
182
|
+
allOf(schemas, required) {
|
|
723
183
|
const allOfSchemas = schemas.map((sub) => this.handle(sub, true));
|
|
724
184
|
if (allOfSchemas.length === 0) {
|
|
725
185
|
return `z.unknown()`;
|
|
726
186
|
}
|
|
727
187
|
if (allOfSchemas.length === 1) {
|
|
728
|
-
return allOfSchemas[0]
|
|
188
|
+
return `${allOfSchemas[0]}${appendOptional(required)}`;
|
|
729
189
|
}
|
|
730
|
-
return this.#toIntersection(allOfSchemas)
|
|
190
|
+
return `${this.#toIntersection(allOfSchemas)}${appendOptional(required)}`;
|
|
731
191
|
}
|
|
732
192
|
#toIntersection(schemas) {
|
|
733
193
|
const [left, ...right] = schemas;
|
|
@@ -737,39 +197,32 @@ var ZodDeserialzer = class {
|
|
|
737
197
|
return `z.intersection(${left}, ${this.#toIntersection(right)})`;
|
|
738
198
|
}
|
|
739
199
|
anyOf(schemas, required) {
|
|
740
|
-
const anyOfSchemas = schemas.map((sub) => this.handle(sub,
|
|
200
|
+
const anyOfSchemas = schemas.map((sub) => this.handle(sub, true));
|
|
741
201
|
if (anyOfSchemas.length === 1) {
|
|
742
|
-
return anyOfSchemas[0]
|
|
202
|
+
return `${anyOfSchemas[0]}${appendOptional(required)}`;
|
|
743
203
|
}
|
|
744
|
-
return
|
|
745
|
-
// Handle an invalid anyOf with one schema
|
|
746
|
-
anyOfSchemas[0]
|
|
747
|
-
);
|
|
204
|
+
return `z.union([${anyOfSchemas.join(", ")}])${appendOptional(required)}`;
|
|
748
205
|
}
|
|
749
206
|
oneOf(schemas, required) {
|
|
750
207
|
const oneOfSchemas = schemas.map((sub) => {
|
|
751
208
|
if (isRef(sub)) {
|
|
752
209
|
const { model } = parseRef(sub.$ref);
|
|
753
210
|
if (this.circularRefTracker.has(model)) {
|
|
754
|
-
return model
|
|
211
|
+
return `${model}${appendOptional(required)}`;
|
|
755
212
|
}
|
|
756
213
|
}
|
|
757
214
|
return this.handle(sub, true);
|
|
758
215
|
});
|
|
759
216
|
if (oneOfSchemas.length === 1) {
|
|
760
|
-
return oneOfSchemas[0]
|
|
217
|
+
return `${oneOfSchemas[0]}${appendOptional(required)}`;
|
|
761
218
|
}
|
|
762
|
-
return
|
|
763
|
-
// Handle an invalid oneOf with one schema
|
|
764
|
-
oneOfSchemas[0]
|
|
765
|
-
);
|
|
219
|
+
return `z.union([${oneOfSchemas.join(", ")}])${appendOptional(required)}`;
|
|
766
220
|
}
|
|
767
|
-
enum(values
|
|
768
|
-
const enumVals = values.map((val) => JSON.stringify(val)).join(", ");
|
|
221
|
+
enum(values) {
|
|
769
222
|
if (values.length === 1) {
|
|
770
|
-
return `z.literal(${
|
|
223
|
+
return `z.literal(${values.join(", ")})`;
|
|
771
224
|
}
|
|
772
|
-
return `z.enum([${
|
|
225
|
+
return `z.enum([${values.join(", ")}])`;
|
|
773
226
|
}
|
|
774
227
|
/**
|
|
775
228
|
* Handle a `string` schema with possible format keywords (JSON Schema).
|
|
@@ -850,51 +303,527 @@ var ZodDeserialzer = class {
|
|
|
850
303
|
if (typeof schema.multipleOf === "number") {
|
|
851
304
|
base += `.refine((val) => Number.isInteger(val / ${schema.multipleOf}), "Must be a multiple of ${schema.multipleOf}")`;
|
|
852
305
|
}
|
|
853
|
-
return { base, defaultValue };
|
|
306
|
+
return { base, defaultValue };
|
|
307
|
+
}
|
|
308
|
+
handle(schema, required) {
|
|
309
|
+
if (isRef(schema)) {
|
|
310
|
+
return `${this.ref(schema.$ref, true)}${appendOptional(required)}`;
|
|
311
|
+
}
|
|
312
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
313
|
+
return this.allOf(schema.allOf ?? [], required);
|
|
314
|
+
}
|
|
315
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
316
|
+
return this.anyOf(schema.anyOf ?? [], required);
|
|
317
|
+
}
|
|
318
|
+
if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length) {
|
|
319
|
+
return this.oneOf(schema.oneOf ?? [], required);
|
|
320
|
+
}
|
|
321
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
322
|
+
const enumVals = schema.enum.map((val) => JSON.stringify(val));
|
|
323
|
+
const defaultValue = enumVals.includes(JSON.stringify(schema.default)) ? JSON.stringify(schema.default) : void 0;
|
|
324
|
+
return `${this.enum(enumVals)}${this.#suffixes(defaultValue, required, false)}`;
|
|
325
|
+
}
|
|
326
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
327
|
+
if (!types.length) {
|
|
328
|
+
return `z.unknown()${appendOptional(required)}`;
|
|
329
|
+
}
|
|
330
|
+
if ("nullable" in schema && schema.nullable) {
|
|
331
|
+
types.push("null");
|
|
332
|
+
} else if (schema.default === null) {
|
|
333
|
+
types.push("null");
|
|
334
|
+
}
|
|
335
|
+
if (types.length > 1) {
|
|
336
|
+
const realTypes = types.filter((t) => t !== "null");
|
|
337
|
+
if (realTypes.length === 1 && types.includes("null")) {
|
|
338
|
+
return this.normal(realTypes[0], schema, required, true);
|
|
339
|
+
}
|
|
340
|
+
const subSchemas = types.map((t) => this.normal(t, schema, false));
|
|
341
|
+
return `z.union([${subSchemas.join(", ")}])${appendOptional(required)}`;
|
|
342
|
+
}
|
|
343
|
+
return this.normal(types[0], schema, required, false);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
function appendOptional(isRequired) {
|
|
347
|
+
return isRequired ? "" : ".optional()";
|
|
348
|
+
}
|
|
349
|
+
function appendDefault(defaultValue) {
|
|
350
|
+
return defaultValue !== void 0 || typeof defaultValue !== "undefined" ? `.default(${defaultValue})` : "";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// packages/typescript/src/lib/sdk.ts
|
|
354
|
+
import { get } from "lodash-es";
|
|
355
|
+
import { camelcase, pascalcase, spinalcase } from "stringcase";
|
|
356
|
+
import { followRef as followRef3, isRef as isRef4, toLitObject as toLitObject2 } from "@sdk-it/core";
|
|
357
|
+
|
|
358
|
+
// packages/typescript/src/lib/emitters/interface.ts
|
|
359
|
+
import { cleanRef as cleanRef2, followRef as followRef2, isRef as isRef2, parseRef as parseRef2 } from "@sdk-it/core";
|
|
360
|
+
var TypeScriptDeserialzer = class {
|
|
361
|
+
circularRefTracker = /* @__PURE__ */ new Set();
|
|
362
|
+
#spec;
|
|
363
|
+
#onRef;
|
|
364
|
+
constructor(spec, onRef) {
|
|
365
|
+
this.#spec = spec;
|
|
366
|
+
this.#onRef = onRef;
|
|
367
|
+
}
|
|
368
|
+
#stringifyKey = (key) => {
|
|
369
|
+
const reservedWords = [
|
|
370
|
+
"constructor",
|
|
371
|
+
"prototype",
|
|
372
|
+
"break",
|
|
373
|
+
"case",
|
|
374
|
+
"catch",
|
|
375
|
+
"class",
|
|
376
|
+
"const",
|
|
377
|
+
"continue",
|
|
378
|
+
"debugger",
|
|
379
|
+
"default",
|
|
380
|
+
"delete",
|
|
381
|
+
"do",
|
|
382
|
+
"else",
|
|
383
|
+
"export",
|
|
384
|
+
"extends",
|
|
385
|
+
"false",
|
|
386
|
+
"finally",
|
|
387
|
+
"for",
|
|
388
|
+
"function",
|
|
389
|
+
"if",
|
|
390
|
+
"import",
|
|
391
|
+
"in",
|
|
392
|
+
"instanceof",
|
|
393
|
+
"new",
|
|
394
|
+
"null",
|
|
395
|
+
"return",
|
|
396
|
+
"super",
|
|
397
|
+
"switch",
|
|
398
|
+
"this",
|
|
399
|
+
"throw",
|
|
400
|
+
"true",
|
|
401
|
+
"try",
|
|
402
|
+
"typeof",
|
|
403
|
+
"var",
|
|
404
|
+
"void",
|
|
405
|
+
"while",
|
|
406
|
+
"with",
|
|
407
|
+
"yield"
|
|
408
|
+
];
|
|
409
|
+
if (reservedWords.includes(key)) {
|
|
410
|
+
return `'${key}'`;
|
|
411
|
+
}
|
|
412
|
+
if (key.trim() === "") {
|
|
413
|
+
return `'${key}'`;
|
|
414
|
+
}
|
|
415
|
+
const firstChar = key.charAt(0);
|
|
416
|
+
const validFirstChar = firstChar >= "a" && firstChar <= "z" || firstChar >= "A" && firstChar <= "Z" || firstChar === "_" || firstChar === "$";
|
|
417
|
+
if (!validFirstChar) {
|
|
418
|
+
return `'${key.replace(/'/g, "\\'")}'`;
|
|
419
|
+
}
|
|
420
|
+
for (let i = 1; i < key.length; i++) {
|
|
421
|
+
const char = key.charAt(i);
|
|
422
|
+
const validChar = char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char >= "0" && char <= "9" || char === "_" || char === "$";
|
|
423
|
+
if (!validChar) {
|
|
424
|
+
return `'${key.replace(/'/g, "\\'")}'`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return key;
|
|
428
|
+
};
|
|
429
|
+
#stringifyKeyV2 = (value) => {
|
|
430
|
+
return `'${value}'`;
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Handle objects (properties)
|
|
434
|
+
*/
|
|
435
|
+
object(schema, required = false) {
|
|
436
|
+
const properties = schema.properties || {};
|
|
437
|
+
const propEntries = Object.entries(properties).map(([key, propSchema]) => {
|
|
438
|
+
const isRequired = (schema.required ?? []).includes(key);
|
|
439
|
+
const tsType = this.handle(propSchema, isRequired);
|
|
440
|
+
return `${this.#stringifyKeyV2(key)}: ${tsType}`;
|
|
441
|
+
});
|
|
442
|
+
if (schema.additionalProperties) {
|
|
443
|
+
if (typeof schema.additionalProperties === "object") {
|
|
444
|
+
const indexType = this.handle(schema.additionalProperties, true);
|
|
445
|
+
propEntries.push(`[key: string]: ${indexType}`);
|
|
446
|
+
} else if (schema.additionalProperties === true) {
|
|
447
|
+
propEntries.push("[key: string]: any");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return `{ ${propEntries.join("; ")} }`;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Handle arrays (items could be a single schema or a tuple)
|
|
454
|
+
*/
|
|
455
|
+
array(schema, required = false) {
|
|
456
|
+
const { items } = schema;
|
|
457
|
+
if (!items) {
|
|
458
|
+
return "any[]";
|
|
459
|
+
}
|
|
460
|
+
if (Array.isArray(items)) {
|
|
461
|
+
const tupleItems = items.map((sub) => this.handle(sub, true));
|
|
462
|
+
return `[${tupleItems.join(", ")}]`;
|
|
463
|
+
}
|
|
464
|
+
const itemsType = this.handle(items, true);
|
|
465
|
+
return `${itemsType}[]`;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Convert a basic type (string | number | boolean | object | array, etc.) to TypeScript
|
|
469
|
+
*/
|
|
470
|
+
normal(type, schema, required = false) {
|
|
471
|
+
switch (type) {
|
|
472
|
+
case "string":
|
|
473
|
+
return this.string(schema, required);
|
|
474
|
+
case "number":
|
|
475
|
+
case "integer":
|
|
476
|
+
return this.number(schema, required);
|
|
477
|
+
case "boolean":
|
|
478
|
+
return appendOptional2("boolean", required);
|
|
479
|
+
case "object":
|
|
480
|
+
return this.object(schema, required);
|
|
481
|
+
case "array":
|
|
482
|
+
return this.array(schema, required);
|
|
483
|
+
case "null":
|
|
484
|
+
return "null";
|
|
485
|
+
default:
|
|
486
|
+
console.warn(`Unknown type: ${type}`);
|
|
487
|
+
return appendOptional2("any", required);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
ref($ref, required) {
|
|
491
|
+
const schemaName = cleanRef2($ref).split("/").pop();
|
|
492
|
+
if (this.circularRefTracker.has(schemaName)) {
|
|
493
|
+
return schemaName;
|
|
494
|
+
}
|
|
495
|
+
this.circularRefTracker.add(schemaName);
|
|
496
|
+
this.#onRef?.(
|
|
497
|
+
schemaName,
|
|
498
|
+
this.handle(followRef2(this.#spec, $ref), required)
|
|
499
|
+
);
|
|
500
|
+
this.circularRefTracker.delete(schemaName);
|
|
501
|
+
return appendOptional2(schemaName, required);
|
|
502
|
+
}
|
|
503
|
+
allOf(schemas) {
|
|
504
|
+
const allOfTypes = schemas.map((sub) => this.handle(sub, true));
|
|
505
|
+
return allOfTypes.length > 1 ? `${allOfTypes.join(" & ")}` : allOfTypes[0];
|
|
506
|
+
}
|
|
507
|
+
anyOf(schemas, required) {
|
|
508
|
+
const anyOfTypes = schemas.map((sub) => this.handle(sub, true));
|
|
509
|
+
return appendOptional2(
|
|
510
|
+
anyOfTypes.length > 1 ? `${anyOfTypes.join(" | ")}` : anyOfTypes[0],
|
|
511
|
+
required
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
oneOf(schemas, required) {
|
|
515
|
+
const oneOfTypes = schemas.map((sub) => {
|
|
516
|
+
if (isRef2(sub)) {
|
|
517
|
+
const { model } = parseRef2(sub.$ref);
|
|
518
|
+
if (this.circularRefTracker.has(model)) {
|
|
519
|
+
return model;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return this.handle(sub, false);
|
|
523
|
+
});
|
|
524
|
+
return appendOptional2(
|
|
525
|
+
oneOfTypes.length > 1 ? `${oneOfTypes.join(" | ")}` : oneOfTypes[0],
|
|
526
|
+
required
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
enum(values, required) {
|
|
530
|
+
const enumValues = values.map((val) => typeof val === "string" ? `'${val}'` : `${val}`).join(" | ");
|
|
531
|
+
return appendOptional2(enumValues, required);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Handle string type with formats
|
|
535
|
+
*/
|
|
536
|
+
string(schema, required) {
|
|
537
|
+
let type;
|
|
538
|
+
switch (schema.format) {
|
|
539
|
+
case "date-time":
|
|
540
|
+
case "datetime":
|
|
541
|
+
case "date":
|
|
542
|
+
type = "Date";
|
|
543
|
+
break;
|
|
544
|
+
case "binary":
|
|
545
|
+
case "byte":
|
|
546
|
+
type = "Blob";
|
|
547
|
+
break;
|
|
548
|
+
case "int64":
|
|
549
|
+
type = "bigint";
|
|
550
|
+
break;
|
|
551
|
+
default:
|
|
552
|
+
type = "string";
|
|
553
|
+
}
|
|
554
|
+
return appendOptional2(type, required);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Handle number/integer types with formats
|
|
558
|
+
*/
|
|
559
|
+
number(schema, required) {
|
|
560
|
+
const type = schema.format === "int64" ? "bigint" : "number";
|
|
561
|
+
return appendOptional2(type, required);
|
|
854
562
|
}
|
|
855
563
|
handle(schema, required) {
|
|
856
|
-
if (
|
|
564
|
+
if (isRef2(schema)) {
|
|
857
565
|
return this.ref(schema.$ref, required);
|
|
858
566
|
}
|
|
859
567
|
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
860
|
-
return this.allOf(schema.allOf
|
|
568
|
+
return this.allOf(schema.allOf);
|
|
861
569
|
}
|
|
862
570
|
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
863
|
-
return this.anyOf(schema.anyOf
|
|
571
|
+
return this.anyOf(schema.anyOf, required);
|
|
864
572
|
}
|
|
865
|
-
if (schema.oneOf && Array.isArray(schema.oneOf)
|
|
866
|
-
return this.oneOf(schema.oneOf
|
|
573
|
+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
574
|
+
return this.oneOf(schema.oneOf, required);
|
|
867
575
|
}
|
|
868
576
|
if (schema.enum && Array.isArray(schema.enum)) {
|
|
869
577
|
return this.enum(schema.enum, required);
|
|
870
578
|
}
|
|
871
579
|
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
872
580
|
if (!types.length) {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
581
|
+
if ("properties" in schema) {
|
|
582
|
+
return this.object(schema, required);
|
|
583
|
+
}
|
|
584
|
+
return appendOptional2("any", required);
|
|
877
585
|
}
|
|
878
586
|
if (types.length > 1) {
|
|
879
587
|
const realTypes = types.filter((t) => t !== "null");
|
|
880
588
|
if (realTypes.length === 1 && types.includes("null")) {
|
|
881
|
-
|
|
589
|
+
const tsType = this.normal(realTypes[0], schema, false);
|
|
590
|
+
return appendOptional2(`${tsType} | null`, required);
|
|
882
591
|
}
|
|
883
|
-
const
|
|
884
|
-
return
|
|
592
|
+
const typeResults = types.map((t) => this.normal(t, schema, false));
|
|
593
|
+
return appendOptional2(typeResults.join(" | "), required);
|
|
885
594
|
}
|
|
886
|
-
return this.normal(types[0], schema, required
|
|
595
|
+
return this.normal(types[0], schema, required);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Generate an interface declaration
|
|
599
|
+
*/
|
|
600
|
+
generateInterface(name, schema) {
|
|
601
|
+
const content = this.handle(schema, true);
|
|
602
|
+
return `interface ${name} ${content}`;
|
|
887
603
|
}
|
|
888
604
|
};
|
|
889
|
-
function appendOptional2(isRequired) {
|
|
890
|
-
return isRequired ?
|
|
605
|
+
function appendOptional2(type, isRequired) {
|
|
606
|
+
return isRequired ? type : `${type} | undefined`;
|
|
891
607
|
}
|
|
892
|
-
|
|
893
|
-
|
|
608
|
+
|
|
609
|
+
// packages/typescript/src/lib/utils.ts
|
|
610
|
+
import { isRef as isRef3, removeDuplicates } from "@sdk-it/core";
|
|
611
|
+
function securityToOptions(security2, securitySchemes, staticIn) {
|
|
612
|
+
securitySchemes ??= {};
|
|
613
|
+
const options = {};
|
|
614
|
+
for (const it of security2) {
|
|
615
|
+
const [name] = Object.keys(it);
|
|
616
|
+
if (!name) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const schema = securitySchemes[name];
|
|
620
|
+
if (isRef3(schema)) {
|
|
621
|
+
throw new Error(`Ref security schemas are not supported`);
|
|
622
|
+
}
|
|
623
|
+
if (schema.type === "http") {
|
|
624
|
+
options["authorization"] = {
|
|
625
|
+
in: staticIn ?? "header",
|
|
626
|
+
schema: "z.string().optional().transform((val) => (val ? `Bearer ${val}` : undefined))",
|
|
627
|
+
optionName: "token"
|
|
628
|
+
};
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (schema.type === "apiKey") {
|
|
632
|
+
if (!schema.in) {
|
|
633
|
+
throw new Error(`apiKey security schema must have an "in" field`);
|
|
634
|
+
}
|
|
635
|
+
if (!schema.name) {
|
|
636
|
+
throw new Error(`apiKey security schema must have a "name" field`);
|
|
637
|
+
}
|
|
638
|
+
options[schema.name] = {
|
|
639
|
+
in: staticIn ?? schema.in,
|
|
640
|
+
schema: "z.string().optional()"
|
|
641
|
+
};
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return options;
|
|
646
|
+
}
|
|
647
|
+
function mergeImports(...imports) {
|
|
648
|
+
const merged = {};
|
|
649
|
+
for (const it of imports) {
|
|
650
|
+
merged[it.moduleSpecifier] = merged[it.moduleSpecifier] ?? {
|
|
651
|
+
moduleSpecifier: it.moduleSpecifier,
|
|
652
|
+
defaultImport: it.defaultImport,
|
|
653
|
+
namespaceImport: it.namespaceImport,
|
|
654
|
+
namedImports: []
|
|
655
|
+
};
|
|
656
|
+
for (const named of it.namedImports) {
|
|
657
|
+
if (!merged[it.moduleSpecifier].namedImports.some(
|
|
658
|
+
(x) => x.name === named.name
|
|
659
|
+
)) {
|
|
660
|
+
merged[it.moduleSpecifier].namedImports.push(named);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return Object.values(merged);
|
|
665
|
+
}
|
|
666
|
+
function importsToString(...imports) {
|
|
667
|
+
return imports.map((it) => {
|
|
668
|
+
if (it.defaultImport) {
|
|
669
|
+
return `import ${it.defaultImport} from '${it.moduleSpecifier}'`;
|
|
670
|
+
}
|
|
671
|
+
if (it.namespaceImport) {
|
|
672
|
+
return `import * as ${it.namespaceImport} from '${it.moduleSpecifier}'`;
|
|
673
|
+
}
|
|
674
|
+
if (it.namedImports) {
|
|
675
|
+
return `import {${removeDuplicates(it.namedImports, (it2) => it2.name).map((n) => `${n.isTypeOnly ? "type" : ""} ${n.name}`).join(", ")}} from '${it.moduleSpecifier}'`;
|
|
676
|
+
}
|
|
677
|
+
throw new Error(`Invalid import ${JSON.stringify(it)}`);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function exclude(list, exclude2) {
|
|
681
|
+
return list.filter((it) => !exclude2.includes(it));
|
|
682
|
+
}
|
|
683
|
+
function useImports(content, ...imports) {
|
|
684
|
+
const output = [];
|
|
685
|
+
for (const it of mergeImports(...imports)) {
|
|
686
|
+
const singleImport = it.defaultImport ?? it.namespaceImport;
|
|
687
|
+
if (singleImport && content.includes(singleImport)) {
|
|
688
|
+
output.push(importsToString(it).join("\n"));
|
|
689
|
+
} else if (it.namedImports.length) {
|
|
690
|
+
for (const namedImport of it.namedImports) {
|
|
691
|
+
if (content.includes(namedImport.name)) {
|
|
692
|
+
output.push(importsToString(it).join("\n"));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return output;
|
|
894
698
|
}
|
|
895
699
|
|
|
896
|
-
// packages/typescript/src/lib/
|
|
897
|
-
|
|
700
|
+
// packages/typescript/src/lib/sdk.ts
|
|
701
|
+
function generateInputs(operationsSet, commonZod, makeImport) {
|
|
702
|
+
const commonImports = commonZod.keys().toArray();
|
|
703
|
+
const inputs = {};
|
|
704
|
+
for (const [name, operations] of Object.entries(operationsSet)) {
|
|
705
|
+
const output = [];
|
|
706
|
+
const imports = /* @__PURE__ */ new Set(['import { z } from "zod";']);
|
|
707
|
+
for (const operation of operations) {
|
|
708
|
+
const schemaName = camelcase(`${operation.name} schema`);
|
|
709
|
+
const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject2(operation.schemas)};`;
|
|
710
|
+
const inputContent = schema;
|
|
711
|
+
for (const schema2 of commonImports) {
|
|
712
|
+
if (inputContent.includes(schema2)) {
|
|
713
|
+
imports.add(
|
|
714
|
+
`import { ${schema2} } from './schemas/${makeImport(spinalcase(schema2))}';`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
output.push(inputContent);
|
|
719
|
+
}
|
|
720
|
+
inputs[`inputs/${spinalcase(name)}.ts`] = [...imports, ...output].join("\n") + "\n";
|
|
721
|
+
}
|
|
722
|
+
const schemas = commonZod.entries().reduce((acc, [name, schema]) => {
|
|
723
|
+
const output = [`import { z } from 'zod';`];
|
|
724
|
+
const content = `export const ${name} = ${schema};`;
|
|
725
|
+
for (const schema2 of commonImports) {
|
|
726
|
+
const preciseMatch = new RegExp(`\\b${schema2}\\b`);
|
|
727
|
+
if (preciseMatch.test(content) && schema2 !== name) {
|
|
728
|
+
output.push(
|
|
729
|
+
`import { ${schema2} } from './${makeImport(spinalcase(schema2))}';`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
output.push(content);
|
|
734
|
+
return [
|
|
735
|
+
[`inputs/schemas/${spinalcase(name)}.ts`, output.join("\n")],
|
|
736
|
+
...acc
|
|
737
|
+
];
|
|
738
|
+
}, []);
|
|
739
|
+
return {
|
|
740
|
+
...Object.fromEntries(schemas),
|
|
741
|
+
...inputs
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function toEndpoint(groupName, spec, specOperation, operation, utils) {
|
|
745
|
+
const schemaName = camelcase(`${operation.name} schema`);
|
|
746
|
+
const schemaRef = `${camelcase(groupName)}.${schemaName}`;
|
|
747
|
+
const inputHeaders = [];
|
|
748
|
+
const inputQuery = [];
|
|
749
|
+
const inputBody = [];
|
|
750
|
+
const inputParams = [];
|
|
751
|
+
const schemas = [];
|
|
752
|
+
const responses = [];
|
|
753
|
+
for (const [name, prop] of Object.entries(operation.inputs)) {
|
|
754
|
+
if (prop.in === "headers" || prop.in === "header") {
|
|
755
|
+
inputHeaders.push(`"${name}"`);
|
|
756
|
+
} else if (prop.in === "query") {
|
|
757
|
+
inputQuery.push(`"${name}"`);
|
|
758
|
+
} else if (prop.in === "body") {
|
|
759
|
+
inputBody.push(`"${name}"`);
|
|
760
|
+
} else if (prop.in === "path") {
|
|
761
|
+
inputParams.push(`"${name}"`);
|
|
762
|
+
} else if (prop.in === "internal") {
|
|
763
|
+
continue;
|
|
764
|
+
} else {
|
|
765
|
+
throw new Error(
|
|
766
|
+
`Unknown source ${prop.in} in ${name} ${JSON.stringify(
|
|
767
|
+
prop
|
|
768
|
+
)} in ${operation.name}`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
specOperation.responses ??= {};
|
|
773
|
+
const outputs = [];
|
|
774
|
+
const statusesCount = Object.keys(specOperation.responses).filter((status) => {
|
|
775
|
+
const statusCode = +status;
|
|
776
|
+
return statusCode >= 200 && statusCode < 300;
|
|
777
|
+
}).length > 1;
|
|
778
|
+
for (const status in specOperation.responses) {
|
|
779
|
+
const response = isRef4(
|
|
780
|
+
specOperation.responses[status]
|
|
781
|
+
) ? followRef3(
|
|
782
|
+
spec,
|
|
783
|
+
specOperation.responses[status].$ref
|
|
784
|
+
) : specOperation.responses[status];
|
|
785
|
+
const handled = handleResponse(
|
|
786
|
+
spec,
|
|
787
|
+
operation.name,
|
|
788
|
+
status,
|
|
789
|
+
response,
|
|
790
|
+
utils,
|
|
791
|
+
true
|
|
792
|
+
// statusesCount,
|
|
793
|
+
);
|
|
794
|
+
responses.push(handled);
|
|
795
|
+
outputs.push(...handled.outputs);
|
|
796
|
+
}
|
|
797
|
+
const addTypeParser = Object.keys(operation.schemas).length > 1;
|
|
798
|
+
for (const type in operation.schemas ?? {}) {
|
|
799
|
+
let typePrefix = "";
|
|
800
|
+
if (addTypeParser && type !== "json") {
|
|
801
|
+
typePrefix = `${type} `;
|
|
802
|
+
}
|
|
803
|
+
const endpoint = `${typePrefix}${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
|
|
804
|
+
schemas.push(
|
|
805
|
+
`"${endpoint}": {
|
|
806
|
+
schema: ${schemaRef}${addTypeParser ? `.${type}` : ""},
|
|
807
|
+
output:[${outputs.join(",")}],
|
|
808
|
+
toRequest(input: z.infer<typeof ${schemaRef}${addTypeParser ? `.${type}` : ""}>) {
|
|
809
|
+
const endpoint = '${endpoint}';
|
|
810
|
+
return toRequest(endpoint, ${operation.outgoingContentType || "nobody"}(input, {
|
|
811
|
+
inputHeaders: [${inputHeaders}],
|
|
812
|
+
inputQuery: [${inputQuery}],
|
|
813
|
+
inputBody: [${inputBody}],
|
|
814
|
+
inputParams: [${inputParams}],
|
|
815
|
+
}));
|
|
816
|
+
},
|
|
817
|
+
}`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
return { responses, schemas };
|
|
821
|
+
}
|
|
822
|
+
var statusCodeToResponseMap = {
|
|
823
|
+
"200": "Ok",
|
|
824
|
+
"201": "Created",
|
|
825
|
+
"202": "Accepted",
|
|
826
|
+
"204": "NoContent",
|
|
898
827
|
"400": "BadRequest",
|
|
899
828
|
"401": "Unauthorized",
|
|
900
829
|
"402": "PaymentRequired",
|
|
@@ -913,8 +842,94 @@ var statusCdeToMessageMap = {
|
|
|
913
842
|
"503": "ServiceUnavailable",
|
|
914
843
|
"504": "GatewayTimeout"
|
|
915
844
|
};
|
|
845
|
+
function handleResponse(spec, operationName, status, response, utils, numbered) {
|
|
846
|
+
const schemas = {};
|
|
847
|
+
const imports = {};
|
|
848
|
+
const endpointImports = {
|
|
849
|
+
ParseError: {
|
|
850
|
+
defaultImport: void 0,
|
|
851
|
+
isTypeOnly: false,
|
|
852
|
+
moduleSpecifier: utils.makeImport(`../http/parser`),
|
|
853
|
+
namedImports: [{ isTypeOnly: false, name: "ParseError" }],
|
|
854
|
+
namespaceImport: void 0
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
const responses = [];
|
|
858
|
+
const outputs = [];
|
|
859
|
+
const typeScriptDeserialzer = new TypeScriptDeserialzer(
|
|
860
|
+
spec,
|
|
861
|
+
(schemaName, zod) => {
|
|
862
|
+
schemas[schemaName] = zod;
|
|
863
|
+
imports[schemaName] = {
|
|
864
|
+
defaultImport: void 0,
|
|
865
|
+
isTypeOnly: true,
|
|
866
|
+
moduleSpecifier: `../models/${utils.makeImport(schemaName)}`,
|
|
867
|
+
namedImports: [{ isTypeOnly: true, name: schemaName }],
|
|
868
|
+
namespaceImport: void 0
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
const statusCode = +status;
|
|
873
|
+
const parser = (response.headers ?? {})["Transfer-Encoding"] ? "chunked" : "buffered";
|
|
874
|
+
const statusName = statusCodeToResponseMap[status] || "APIResponse";
|
|
875
|
+
const interfaceName = pascalcase(
|
|
876
|
+
operationName + ` output${numbered ? status : ""}`
|
|
877
|
+
);
|
|
878
|
+
if (statusCode === 204) {
|
|
879
|
+
outputs.push(statusName);
|
|
880
|
+
} else {
|
|
881
|
+
if (status.endsWith("XX")) {
|
|
882
|
+
outputs.push(`APIError<${interfaceName}>`);
|
|
883
|
+
} else {
|
|
884
|
+
outputs.push(
|
|
885
|
+
parser !== "buffered" ? `{type: ${statusName}<${interfaceName}>, parser: ${parser}}` : `${statusName}<${interfaceName}>`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const responseContent = get(response, ["content"]);
|
|
890
|
+
const isJson = responseContent && responseContent["application/json"];
|
|
891
|
+
const responseSchema = isJson ? typeScriptDeserialzer.handle(
|
|
892
|
+
responseContent["application/json"].schema,
|
|
893
|
+
true
|
|
894
|
+
) : "void";
|
|
895
|
+
responses.push({
|
|
896
|
+
name: interfaceName,
|
|
897
|
+
schema: responseSchema
|
|
898
|
+
});
|
|
899
|
+
const statusGroup = +status.slice(0, 1);
|
|
900
|
+
if (statusCode >= 400 || statusGroup >= 4) {
|
|
901
|
+
endpointImports[statusCodeToResponseMap[status] ?? "APIError"] = {
|
|
902
|
+
moduleSpecifier: utils.makeImport("../http/response"),
|
|
903
|
+
namedImports: [{ name: statusCodeToResponseMap[status] ?? "APIError" }]
|
|
904
|
+
};
|
|
905
|
+
endpointImports[interfaceName] = {
|
|
906
|
+
isTypeOnly: true,
|
|
907
|
+
moduleSpecifier: `../outputs/${utils.makeImport(spinalcase(operationName))}`,
|
|
908
|
+
namedImports: [{ isTypeOnly: true, name: interfaceName }]
|
|
909
|
+
};
|
|
910
|
+
} else if (statusCode >= 200 && statusCode < 300 || statusCode >= 2 || statusGroup <= 3) {
|
|
911
|
+
endpointImports[statusName] = {
|
|
912
|
+
moduleSpecifier: utils.makeImport("../http/response"),
|
|
913
|
+
namedImports: [
|
|
914
|
+
{
|
|
915
|
+
isTypeOnly: false,
|
|
916
|
+
name: statusName
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
};
|
|
920
|
+
endpointImports[interfaceName] = {
|
|
921
|
+
defaultImport: void 0,
|
|
922
|
+
isTypeOnly: true,
|
|
923
|
+
moduleSpecifier: `../outputs/${utils.makeImport(spinalcase(operationName))}`,
|
|
924
|
+
namedImports: [{ isTypeOnly: true, name: interfaceName }],
|
|
925
|
+
namespaceImport: void 0
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return { schemas, imports, endpointImports, responses, outputs };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// packages/typescript/src/lib/generator.ts
|
|
916
932
|
function generateCode(config) {
|
|
917
|
-
const commonSchemas = {};
|
|
918
933
|
const commonZod = /* @__PURE__ */ new Map();
|
|
919
934
|
const commonZodImports = [];
|
|
920
935
|
const zodDeserialzer = new ZodDeserialzer(config.spec, (model, schema) => {
|
|
@@ -929,14 +944,15 @@ function generateCode(config) {
|
|
|
929
944
|
});
|
|
930
945
|
const groups = {};
|
|
931
946
|
const outputs = {};
|
|
947
|
+
const endpoints = {};
|
|
932
948
|
forEachOperation(config, (entry, operation) => {
|
|
933
949
|
console.log(`Processing ${entry.method} ${entry.path}`);
|
|
934
|
-
|
|
935
|
-
|
|
950
|
+
groups[entry.groupName] ??= [];
|
|
951
|
+
endpoints[entry.groupName] ??= [];
|
|
936
952
|
const inputs = {};
|
|
937
953
|
const additionalProperties = [];
|
|
938
954
|
for (const param of operation.parameters ?? []) {
|
|
939
|
-
if (
|
|
955
|
+
if (isRef5(param)) {
|
|
940
956
|
throw new Error(`Found reference in parameter ${param.$ref}`);
|
|
941
957
|
}
|
|
942
958
|
if (!param.schema) {
|
|
@@ -964,7 +980,7 @@ function generateCode(config) {
|
|
|
964
980
|
})
|
|
965
981
|
)
|
|
966
982
|
);
|
|
967
|
-
const
|
|
983
|
+
const schemas = {};
|
|
968
984
|
const shortContenTypeMap = {
|
|
969
985
|
"application/json": "json",
|
|
970
986
|
"application/x-www-form-urlencoded": "urlencoded",
|
|
@@ -974,9 +990,9 @@ function generateCode(config) {
|
|
|
974
990
|
};
|
|
975
991
|
let outgoingContentType;
|
|
976
992
|
if (operation.requestBody && Object.keys(operation.requestBody).length) {
|
|
977
|
-
const content =
|
|
993
|
+
const content = isRef5(operation.requestBody) ? get2(followRef4(config.spec, operation.requestBody.$ref), ["content"]) : operation.requestBody.content;
|
|
978
994
|
for (const type in content) {
|
|
979
|
-
const ctSchema =
|
|
995
|
+
const ctSchema = isRef5(content[type].schema) ? followRef4(config.spec, content[type].schema.$ref) : content[type].schema;
|
|
980
996
|
if (!ctSchema) {
|
|
981
997
|
console.warn(`Schema not found for ${type}`);
|
|
982
998
|
continue;
|
|
@@ -992,7 +1008,7 @@ function generateCode(config) {
|
|
|
992
1008
|
)
|
|
993
1009
|
});
|
|
994
1010
|
Object.assign(inputs, bodyInputs(config, ctSchema));
|
|
995
|
-
|
|
1011
|
+
schemas[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
|
|
996
1012
|
}
|
|
997
1013
|
if (content["application/json"]) {
|
|
998
1014
|
outgoingContentType = "json";
|
|
@@ -1011,7 +1027,7 @@ function generateCode(config) {
|
|
|
1011
1027
|
}),
|
|
1012
1028
|
{}
|
|
1013
1029
|
);
|
|
1014
|
-
|
|
1030
|
+
schemas[shortContenTypeMap["application/json"]] = zodDeserialzer.handle(
|
|
1015
1031
|
{
|
|
1016
1032
|
type: "object",
|
|
1017
1033
|
required: additionalProperties.filter((p) => p.required).map((p) => p.name),
|
|
@@ -1020,90 +1036,152 @@ function generateCode(config) {
|
|
|
1020
1036
|
true
|
|
1021
1037
|
);
|
|
1022
1038
|
}
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1039
|
+
const endpoint = toEndpoint(
|
|
1040
|
+
entry.groupName,
|
|
1041
|
+
config.spec,
|
|
1042
|
+
operation,
|
|
1043
|
+
{
|
|
1044
|
+
outgoingContentType,
|
|
1045
|
+
name: entry.name,
|
|
1046
|
+
type: "http",
|
|
1047
|
+
trigger: entry,
|
|
1048
|
+
schemas,
|
|
1049
|
+
inputs
|
|
1050
|
+
},
|
|
1051
|
+
{ makeImport: config.makeImport }
|
|
1052
|
+
);
|
|
1026
1053
|
const output = [`import z from 'zod';`];
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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) {
|
|
1054
|
+
const responses = endpoint.responses.flatMap((it) => it.responses);
|
|
1055
|
+
const responsesImports = endpoint.responses.flatMap(
|
|
1056
|
+
(it) => Object.values(it.imports)
|
|
1057
|
+
);
|
|
1058
|
+
if (responses.length) {
|
|
1069
1059
|
output.push(
|
|
1070
|
-
`export type ${
|
|
1071
|
-
responses,
|
|
1072
|
-
(it) => it
|
|
1073
|
-
).join(" | ")};`
|
|
1060
|
+
...responses.map((it) => `export type ${it.name} = ${it.schema};`)
|
|
1074
1061
|
);
|
|
1075
1062
|
} else {
|
|
1076
|
-
output.push(
|
|
1077
|
-
`export type ${pascalcase(entry.name + " output")} = ${responses[0]};`
|
|
1078
|
-
);
|
|
1079
|
-
}
|
|
1080
|
-
output.push(
|
|
1081
|
-
...useImports(output.join(""), Object.values(responsesImports))
|
|
1082
|
-
);
|
|
1083
|
-
if (!foundResponse) {
|
|
1084
|
-
output.push(`export type ${pascalcase(entry.name + " output")} = void`);
|
|
1063
|
+
output.push(`export type ${pascalcase2(entry.name + " output")} = void;`);
|
|
1085
1064
|
}
|
|
1065
|
+
output.unshift(...useImports(output.join(""), ...responsesImports));
|
|
1086
1066
|
outputs[`${spinalcase2(entry.name)}.ts`] = output.join("\n");
|
|
1087
|
-
|
|
1067
|
+
endpoints[entry.groupName].push(endpoint);
|
|
1068
|
+
groups[entry.groupName].push({
|
|
1088
1069
|
name: entry.name,
|
|
1089
1070
|
type: "http",
|
|
1090
1071
|
inputs,
|
|
1091
|
-
errors: errors.length ? errors : ["ServerError"],
|
|
1092
1072
|
outgoingContentType,
|
|
1093
|
-
schemas
|
|
1094
|
-
parser,
|
|
1095
|
-
formatOutput: () => ({
|
|
1096
|
-
import: pascalcase(entry.name + " output"),
|
|
1097
|
-
use: pascalcase(entry.name + " output")
|
|
1098
|
-
}),
|
|
1073
|
+
schemas,
|
|
1099
1074
|
trigger: entry
|
|
1100
1075
|
});
|
|
1101
1076
|
});
|
|
1102
|
-
|
|
1077
|
+
const commonSchemas = Object.values(endpoints).reduce(
|
|
1078
|
+
(acc, endpoint) => ({
|
|
1079
|
+
...acc,
|
|
1080
|
+
...endpoint.reduce(
|
|
1081
|
+
(acc2, { responses }) => ({
|
|
1082
|
+
...acc2,
|
|
1083
|
+
...responses.reduce(
|
|
1084
|
+
(acc3, it) => ({ ...acc3, ...it.schemas }),
|
|
1085
|
+
{}
|
|
1086
|
+
)
|
|
1087
|
+
}),
|
|
1088
|
+
{}
|
|
1089
|
+
)
|
|
1090
|
+
}),
|
|
1091
|
+
{}
|
|
1092
|
+
);
|
|
1093
|
+
const allSchemas = Object.keys(endpoints).map((it) => ({
|
|
1094
|
+
import: `import ${camelcase2(it)} from './${config.makeImport(spinalcase2(it))}';`,
|
|
1095
|
+
use: ` ...${camelcase2(it)}`
|
|
1096
|
+
}));
|
|
1097
|
+
const imports = [
|
|
1098
|
+
'import z from "zod";',
|
|
1099
|
+
`import type { ParseError } from '${config.makeImport("../http/parser")}';`,
|
|
1100
|
+
`import type { ServerError } from '${config.makeImport("../http/response")}';`,
|
|
1101
|
+
`import type { OutputType, Parser, Type } from '../http/send-request.ts';`
|
|
1102
|
+
];
|
|
1103
|
+
return {
|
|
1104
|
+
groups,
|
|
1105
|
+
commonSchemas,
|
|
1106
|
+
commonZod,
|
|
1107
|
+
outputs,
|
|
1108
|
+
clientFiles: {},
|
|
1109
|
+
endpoints: {
|
|
1110
|
+
[`${join("api", config.makeImport("schemas"))}`]: `
|
|
1111
|
+
${imports.join("\n")}
|
|
1112
|
+
${allSchemas.map((it) => it.import).join("\n")}
|
|
1113
|
+
|
|
1114
|
+
const schemas = {
|
|
1115
|
+
${allSchemas.map((it) => it.use).join(",\n")}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
type Output<T extends OutputType> = T extends {
|
|
1120
|
+
parser: Parser;
|
|
1121
|
+
type: Type<any>;
|
|
1122
|
+
}
|
|
1123
|
+
? InstanceType<T['type']>
|
|
1124
|
+
: T extends Type<any>
|
|
1125
|
+
? InstanceType<T>
|
|
1126
|
+
: never;
|
|
1127
|
+
|
|
1128
|
+
export type Endpoints = {
|
|
1129
|
+
[K in keyof typeof schemas]: {
|
|
1130
|
+
input: z.infer<(typeof schemas)[K]['schema']>;
|
|
1131
|
+
output: (typeof schemas)[K]['output'] extends [
|
|
1132
|
+
infer Single extends OutputType,
|
|
1133
|
+
]
|
|
1134
|
+
? Output<Single>
|
|
1135
|
+
: (typeof schemas)[K]['output'] extends readonly [
|
|
1136
|
+
...infer Tuple extends OutputType[],
|
|
1137
|
+
]
|
|
1138
|
+
? { [I in keyof Tuple]: Output<Tuple[I]> }[number]
|
|
1139
|
+
: never;
|
|
1140
|
+
error: ServerError | ParseError<(typeof schemas)[K]['schema']>;
|
|
1141
|
+
};
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
export default schemas;
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
`.trim(),
|
|
1148
|
+
...Object.fromEntries(
|
|
1149
|
+
Object.entries(endpoints).map(([name, endpoint]) => {
|
|
1150
|
+
const imps = importsToString(
|
|
1151
|
+
...mergeImports(
|
|
1152
|
+
...endpoint.flatMap(
|
|
1153
|
+
(it) => it.responses.flatMap(
|
|
1154
|
+
(it2) => Object.values(it2.endpointImports)
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1158
|
+
);
|
|
1159
|
+
return [
|
|
1160
|
+
[
|
|
1161
|
+
join("api", config.makeImport(spinalcase2(name))),
|
|
1162
|
+
`${[
|
|
1163
|
+
...imps,
|
|
1164
|
+
// ...imports,
|
|
1165
|
+
`import z from 'zod';`,
|
|
1166
|
+
`import { toRequest, json, urlencoded, nobody, formdata, createUrl } from '${config.makeImport("../http/request")}';`,
|
|
1167
|
+
`import { chunked, buffered } from "${config.makeImport("../http/parse-response")}";`,
|
|
1168
|
+
`import * as ${camelcase2(name)} from '../inputs/${config.makeImport(spinalcase2(name))}';`
|
|
1169
|
+
].join(
|
|
1170
|
+
"\n"
|
|
1171
|
+
)}
|
|
1172
|
+
export default {
|
|
1173
|
+
${endpoint.flatMap((it) => it.schemas).join(",\n")}
|
|
1174
|
+
}`
|
|
1175
|
+
]
|
|
1176
|
+
];
|
|
1177
|
+
}).flat()
|
|
1178
|
+
)
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1103
1181
|
}
|
|
1104
1182
|
function toProps(spec, schemaOrRef, aggregator = []) {
|
|
1105
|
-
if (
|
|
1106
|
-
const schema =
|
|
1183
|
+
if (isRef5(schemaOrRef)) {
|
|
1184
|
+
const schema = followRef4(spec, schemaOrRef.$ref);
|
|
1107
1185
|
return toProps(spec, schema, aggregator);
|
|
1108
1186
|
} else if (schemaOrRef.type === "object") {
|
|
1109
1187
|
for (const [name] of Object.entries(schemaOrRef.properties ?? {})) {
|
|
@@ -1148,22 +1226,22 @@ function bodyInputs(config, ctSchema) {
|
|
|
1148
1226
|
}
|
|
1149
1227
|
|
|
1150
1228
|
// 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)
|
|
1229
|
+
var interceptors_default = "import { type RequestConfig } from './request.ts';\n\nexport interface Interceptor {\n before?: (config: RequestConfig) => Promise<RequestConfig>|RequestConfig;\n after?: (response: Response) => Promise<Response> | Response;\n}\n\nexport const createHeadersInterceptor = (\n defaultHeaders: () => Record<string, string | undefined>,\n requestHeaders: HeadersInit,\n):Interceptor => {\n return {\n before({init, url}) {\n // Priority Levels\n // 1. Headers Input\n // 2. Request Headers\n // 3. Default Headers\n const headers = defaultHeaders();\n\n for (const [key, value] of new Headers(requestHeaders)) {\n // Only set the header if it doesn't already exist and has a value\n // even though these headers are passed at operation level\n // still they are lower priority compared to the headers input\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n for (const [key, value] of Object.entries(headers)) {\n // Only set the header if it doesn't already exist and has a value\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n return {init, url};\n },\n };\n};\n\nexport const createBaseUrlInterceptor = (\n getBaseUrl: () => string,\n): Interceptor => {\n return {\n before({ init, url }) {\n const baseUrl = getBaseUrl();\n if (url.protocol === 'local:') {\n return {\n init,\n url: new URL(url.href.replace('local://', baseUrl))\n };\n }\n return { init, url };\n },\n };\n};\n\nexport const logInterceptor: Interceptor = {\n before({ url, init }) {\n console.dir('Request:', { url, init });\n return { url, init };\n },\n after(response) {\n console.log('Response:', response);\n return response;\n },\n};\n\n/**\n * Creates an interceptor that logs detailed information about requests and responses.\n * @param options Configuration options for the logger\n * @returns An interceptor object with before and after handlers\n */\nexport const createDetailedLogInterceptor = (options?: {\n logLevel?: 'debug' | 'info' | 'warn' | 'error';\n includeRequestBody?: boolean;\n includeResponseBody?: boolean;\n}) => {\n const logLevel = options?.logLevel || 'info';\n const includeRequestBody = options?.includeRequestBody || false;\n const includeResponseBody = options?.includeResponseBody || false;\n\n return {\n async before(request: Request) {\n const logData = {\n url: request.url,\n method: request.method,\n contentType: request.headers.get('Content-Type'),\n headers: Object.fromEntries([...request.headers.entries()]),\n };\n\n console[logLevel]('\u{1F680} Outgoing Request:', logData);\n\n if (includeRequestBody) {\n try {\n // Clone the request to avoid consuming the body stream\n const clonedRequest = request.clone();\n if (clonedRequest.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedRequest.json().catch(() => null);\n console[logLevel]('Request Body:', body);\n } else {\n const body = await clonedRequest.text().catch(() => null);\n console[logLevel]('Request Body:', body);\n }\n } catch (error) {\n console.error('Could not log request body:', error);\n }\n }\n\n return request;\n },\n\n async after(response: Response) {\n const logData = {\n status: response.status,\n statusText: response.statusText,\n url: response.url,\n headers: Object.fromEntries([...response.headers.entries()]),\n };\n\n console[logLevel]('\u{1F4E5} Incoming Response:', logData);\n\n if (includeResponseBody && response.body) {\n try {\n // Clone the response to avoid consuming the body stream\n const clonedResponse = response.clone();\n if (clonedResponse.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedResponse.json().catch(() => null);\n console[logLevel]('Response Body:', body);\n } else {\n const body = await clonedResponse.text().catch(() => null);\n if (body) {\n console[logLevel]('Response Body:', body.substring(0, 500) + (body.length > 500 ? '...' : ''));\n } else {\n console[logLevel]('No response body');\n }\n }\n } catch (error) {\n console.error('Could not log response body:', error);\n }\n }\n\n return response;\n },\n };\n};\n";
|
|
1152
1230
|
|
|
1153
1231
|
// 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
|
|
1232
|
+
var parse_response_default = 'import { parse } from "fast-content-type-parse";\n\nexport async function handleError(response: Response) {\n try {\n if (response.status >= 400 && response.status < 500) {\n const body = (await response.json()) as Record<string, any>;\n return {\n status: response.status,\n body: body,\n };\n }\n return new Error(\n `An error occurred while fetching the data. Status: ${response.status}`,\n );\n } catch (error) {\n return error as any;\n }\n}\n\nasync function handleChunkedResponse(response: Response, contentType: string) {\n const { type } = parse(contentType);\n\n switch (type) {\n case "application/json": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return JSON.parse(buffer);\n }\n case "text/html":\n case "text/plain": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return buffer;\n }\n default:\n return response.body;\n }\n}\n\nexport function chunked(response: Response) {\n return response.body!;\n}\n\nexport async function buffered(response: Response) {\n const contentType = response.headers.get("Content-Type");\n if (!contentType) {\n throw new Error("Content-Type header is missing");\n }\n\n if (response.status === 204) {\n return null;\n }\n\n const { type } = parse(contentType);\n switch (type) {\n case "application/json":\n return response.json();\n case "text/plain":\n return response.text();\n case "text/html":\n return response.text();\n case "text/xml":\n case "application/xml":\n return response.text();\n case "application/x-www-form-urlencoded": {\n const text = await response.text();\n return Object.fromEntries(new URLSearchParams(text));\n }\n case "multipart/form-data":\n return response.formData();\n default:\n throw new Error(`Unsupported content type: ${contentType}`);\n }\n}\n';
|
|
1155
1233
|
|
|
1156
1234
|
// packages/typescript/src/lib/http/parser.txt
|
|
1157
|
-
var parser_default =
|
|
1235
|
+
var parser_default = 'import { z } from "zod";\n\nexport class ParseError<T extends z.ZodType<any, any, any>> {\n public data: z.typeToFlattenedError<T, z.ZodIssue>;\n constructor(data: z.typeToFlattenedError<T, z.ZodIssue>) {\n this.data = data;\n }\n}\n\nexport function parse<T extends z.ZodType<any, any, any>>(schema: T, input: unknown) {\n const result = schema.safeParse(input);\n if (!result.success) {\n const error = result.error.flatten((issue) => issue);\n return [null, new ParseError(error)];\n }\n return [result.data as z.infer<T>, null];\n}\n';
|
|
1158
1236
|
|
|
1159
1237
|
// packages/typescript/src/lib/http/request.txt
|
|
1160
1238
|
var request_default = "type Init = Omit<RequestInit, 'headers'> & { headers: Headers; };\nexport type RequestConfig = { init: Init; url: URL };\nexport type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\nexport type ContentType = 'xml' | 'json' | 'urlencoded' | 'multipart' | 'formdata';\nexport type Endpoint =\n | `${ContentType} ${Method} ${string}`\n | `${Method} ${string}`;\n\nexport type BodyInit =\n | ArrayBuffer\n | Blob\n | FormData\n | URLSearchParams\n | null\n | string;\n\nexport function createUrl(path: string, query: URLSearchParams) {\n const url = new URL(path, `local://`);\n url.search = query.toString();\n return url;\n}\n\nfunction template(\n templateString: string,\n templateVariables: Record<string, any>,\n): string {\n const nargs = /{([0-9a-zA-Z_]+)}/g;\n return templateString.replace(nargs, (match, key: string, index: number) => {\n // Handle escaped double braces\n if (\n templateString[index - 1] === '{' &&\n templateString[index + match.length] === '}'\n ) {\n return key;\n }\n\n const result = key in templateVariables ? templateVariables[key] : null;\n return result === null || result === undefined ? '' : String(result);\n });\n}\n\ntype Input = Record<string, any>;\ntype Props = {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n};\n\nabstract class Serializer {\n protected input: Input;\n protected props: Props;\n\n constructor(\n input: Input,\n props: Props,\n ) {\n this.input = input;\n this.props = props;\n }\n\n abstract getBody(): BodyInit | null;\n abstract getHeaders(): Record<string, string>;\n serialize(): Serialized {\n const headers = new Headers({});\n for (const header of this.props.inputHeaders) {\n headers.set(header, this.input[header]);\n }\n\n const query = new URLSearchParams();\n for (const key of this.props.inputQuery) {\n const value = this.input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = this.props.inputParams.reduce<Record<string, any>>(\n (acc, key) => {\n acc[key] = this.input[key];\n return acc;\n },\n {},\n );\n\n return {\n body: this.getBody(),\n query,\n params,\n headers: this.getHeaders(),\n };\n }\n}\n\ninterface Serialized {\n body: BodyInit | null;\n query: URLSearchParams;\n params: Record<string, any>;\n headers: Record<string, string>;\n}\n\nclass JsonSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body: Record<string, any> = {};\n for (const prop of this.props.inputBody) {\n body[prop] = this.input[prop];\n }\n return JSON.stringify(body);\n }\n getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n}\n\nclass UrlencodedSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new URLSearchParams();\n for (const prop of this.props.inputBody) {\n body.set(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass NoBodySerializer extends Serializer {\n getBody(): BodyInit | null {\n return null;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass FormDataSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new FormData();\n for (const prop of this.props.inputBody) {\n body.append(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nexport function json(input: Input, props: Props) {\n return new JsonSerializer(input, props).serialize();\n}\nexport function urlencoded(input: Input, props: Props) {\n return new UrlencodedSerializer(input, props).serialize();\n}\nexport function nobody(input: Input, props: Props) {\n return new NoBodySerializer(input, props).serialize();\n}\nexport function formdata(input: Input, props: Props) {\n return new FormDataSerializer(input, props).serialize();\n}\n\nexport function toRequest<T extends Endpoint>(\n endpoint: T,\n input: Serialized,\n): RequestConfig {\n const [method, path] = endpoint.split(' ');\n const pathVariable = template(path, input.params);\n\n return {\n url: createUrl(pathVariable, input.query),\n init: {\n method: method,\n headers: new Headers(input.headers),\n body: method === 'GET' ? undefined : input.body,\n },\n }\n}\n";
|
|
1161
1239
|
|
|
1162
1240
|
// packages/typescript/src/lib/http/response.txt
|
|
1163
|
-
var response_default = "export
|
|
1241
|
+
var response_default = "export class APIResponse<Body = unknown, Status extends number = number> {\n static status: number;\n status: Status;\n data: Body;\n\n constructor(status: Status, data: Body) {\n this.status = status;\n this.data = data;\n }\n}\n\nexport class APIError<Body, Status extends number = number> extends APIResponse<\n Body,\n Status\n> {}\n\n// 2xx Success\nexport class Ok<T> extends APIResponse<T, 200> {\n static status = 200;\n}\nexport class Created<T> extends APIResponse<T, 201> {}\nexport class Accepted<T> extends APIResponse<T, 202> {}\nexport class NoContent extends APIResponse<null, 204> {}\n\n// 4xx Client Errors\nexport class BadRequest<T> extends APIError<T, 400> {}\nexport class Unauthorized<T = { message: string }> extends APIError<T, 401> {}\nexport class PaymentRequired<T = { message: string }> extends APIError<\n T,\n 402\n> {}\nexport class Forbidden<T = { message: string }> extends APIError<T, 403> {}\nexport class NotFound<T = { message: string }> extends APIError<T, 404> {}\nexport class MethodNotAllowed<T = { message: string }> extends APIError<\n T,\n 405\n> {}\nexport class NotAcceptable<T = { message: string }> extends APIError<T, 406> {}\nexport class Conflict<T = { message: string }> extends APIError<T, 409> {}\nexport class Gone<T = { message: string }> extends APIError<T, 410> {}\nexport class UnprocessableEntity<\n T = { message: string; errors?: Record<string, string[]> },\n> extends APIError<T, 422> {}\nexport class TooManyRequests<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 429> {}\nexport class PayloadTooLarge<T = { message: string }> extends APIError<\n T,\n 413\n> {}\nexport class UnsupportedMediaType<T = { message: string }> extends APIError<\n T,\n 415\n> {}\n\n// 5xx Server Errors\nexport class InternalServerError<T = { message: string }> extends APIError<\n T,\n 500\n> {}\nexport class NotImplemented<T = { message: string }> extends APIError<T, 501> {}\nexport class BadGateway<T = { message: string }> extends APIError<T, 502> {}\nexport class ServiceUnavailable<\n T = { message: string; retryAfter?: string },\n> extends APIError<T, 503> {}\nexport class GatewayTimeout<T = { message: string }> extends APIError<T, 504> {}\n\nexport type ClientError =\n | BadRequest<{ message: string }>\n | Unauthorized\n | PaymentRequired\n | Forbidden\n | NotFound\n | MethodNotAllowed\n | NotAcceptable\n | Conflict\n | Gone\n | UnprocessableEntity\n | TooManyRequests;\n\nexport type ServerError =\n | InternalServerError\n | NotImplemented\n | BadGateway\n | ServiceUnavailable\n | GatewayTimeout;\n\nexport type ProblematicResponse = ClientError | ServerError;\n";
|
|
1164
1242
|
|
|
1165
1243
|
// 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
|
|
1244
|
+
var send_request_default = "export interface Type<T> {\n new (...args: any[]): T;\n}\nexport type Parser = (\n response: Response,\n) => Promise<unknown> | ReadableStream<any>;\nexport type OutputType =\n | Type<APIResponse>\n | { parser: Parser; type: Type<APIResponse> };\ntype Constructor<T> = new (...args: any[]) => T;\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (input: any) => RequestConfig;\n output: OutputType[];\n}\n\nexport const fetchType = z\n .function()\n .args(z.instanceof(Request))\n .returns(z.promise(z.instanceof(Response)))\n .optional();\n\nexport async function sendRequest(\n input: unknown,\n route: RequestSchema,\n options: {\n fetch?: z.infer<typeof fetchType>;\n interceptors?: Interceptor[];\n signal?: AbortSignal;\n },\n) {\n const { interceptors = [] } = options;\n const [parsedInput, parseError] = parse(route.schema, input);\n if (parseError) {\n return [null as never, { ...parseError, kind: 'parse' } as never] as const;\n }\n\n let config = route.toRequest(parsedInput as never);\n for (const interceptor of interceptors) {\n if (interceptor.before) {\n config = await interceptor.before(config);\n }\n }\n\n let response = await (options.fetch ?? fetch)(\n new Request(config.url, config.init),\n {\n ...config.init,\n signal: options.signal,\n },\n );\n\n for (let i = interceptors.length - 1; i >= 0; i--) {\n const interceptor = interceptors[i];\n if (interceptor.after) {\n response = await interceptor.after(response.clone());\n }\n }\n\n let output: Constructor<APIResponse> | null = APIResponse;\n let parser: Parser = buffered;\n for (const outputType of route.output) {\n if ('parser' in outputType) {\n parser = outputType.parser;\n if (isTypeOf(outputType.type, APIResponse)) {\n if (response.status === outputType.type.status) {\n output = outputType.type;\n break;\n }\n }\n } else if (isTypeOf(outputType, APIResponse)) {\n if (response.status === outputType.status) {\n output = outputType;\n break;\n }\n }\n }\n\n const data = new output(response.status, await parser(response));\n if (response.ok) {\n return [data as never, null] as const;\n }\n return [null as never, data as never] as const;\n}\n\nexport function isTypeOf<T extends Type<APIResponse>>(\n instance: any,\n baseType: T,\n): instance is T {\n if (instance === baseType) {\n return true;\n }\n const prototype = Object.getPrototypeOf(instance);\n if (prototype === null) {\n return false;\n }\n return isTypeOf(prototype, baseType);\n}\n";
|
|
1167
1245
|
|
|
1168
1246
|
// packages/typescript/src/lib/generate.ts
|
|
1169
1247
|
function security(spec) {
|
|
@@ -1191,21 +1269,14 @@ async function generate(spec, settings) {
|
|
|
1191
1269
|
const makeImport = (moduleSpecifier) => {
|
|
1192
1270
|
return settings.useTsExtension ? `${moduleSpecifier}.ts` : moduleSpecifier;
|
|
1193
1271
|
};
|
|
1194
|
-
const { commonSchemas, groups, outputs, commonZod } = generateCode({
|
|
1272
|
+
const { commonSchemas, endpoints, groups, outputs, commonZod, clientFiles } = generateCode({
|
|
1195
1273
|
spec,
|
|
1196
1274
|
style: "github",
|
|
1197
1275
|
makeImport
|
|
1198
1276
|
});
|
|
1199
|
-
const output = settings.mode === "full" ?
|
|
1277
|
+
const output = settings.mode === "full" ? join2(settings.output, "src") : settings.output;
|
|
1200
1278
|
const options = security(spec);
|
|
1201
1279
|
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
1280
|
const inputFiles = generateInputs(groups, commonZod, makeImport);
|
|
1210
1281
|
await writeFiles(output, {
|
|
1211
1282
|
"outputs/.gitkeep": "",
|
|
@@ -1213,24 +1284,33 @@ async function generate(spec, settings) {
|
|
|
1213
1284
|
"models/.getkeep": ""
|
|
1214
1285
|
// 'README.md': readme,
|
|
1215
1286
|
});
|
|
1216
|
-
await writeFiles(
|
|
1287
|
+
await writeFiles(join2(output, "http"), {
|
|
1217
1288
|
"interceptors.ts": interceptors_default,
|
|
1218
1289
|
"parse-response.ts": parse_response_default,
|
|
1219
1290
|
"send-request.ts": `import z from 'zod';
|
|
1220
1291
|
import type { Interceptor } from './${makeImport("interceptors")}';
|
|
1221
|
-
import {
|
|
1292
|
+
import { buffered } from './${makeImport("parse-response")}';
|
|
1222
1293
|
import { parse } from './${makeImport("parser")}';
|
|
1223
|
-
import type { RequestConfig } from '
|
|
1294
|
+
import type { RequestConfig } from './${makeImport("request")}';
|
|
1295
|
+
import { APIResponse } from './${makeImport("response")}';
|
|
1296
|
+
|
|
1224
1297
|
${send_request_default}`,
|
|
1225
1298
|
"response.ts": response_default,
|
|
1226
1299
|
"parser.ts": parser_default,
|
|
1227
1300
|
"request.ts": request_default
|
|
1228
1301
|
});
|
|
1229
|
-
await writeFiles(
|
|
1302
|
+
await writeFiles(join2(output, "outputs"), outputs);
|
|
1230
1303
|
const modelsImports = Object.entries(commonSchemas).map(([name]) => name);
|
|
1231
1304
|
await writeFiles(output, {
|
|
1305
|
+
"client.ts": client_default({
|
|
1306
|
+
name: clientName,
|
|
1307
|
+
servers: (spec.servers ?? []).map((server) => server.url) || [],
|
|
1308
|
+
options,
|
|
1309
|
+
makeImport
|
|
1310
|
+
}),
|
|
1232
1311
|
...clientFiles,
|
|
1233
1312
|
...inputFiles,
|
|
1313
|
+
...endpoints,
|
|
1234
1314
|
...Object.fromEntries(
|
|
1235
1315
|
Object.entries(commonSchemas).map(([name, schema]) => [
|
|
1236
1316
|
`models/${name}.ts`,
|
|
@@ -1245,29 +1325,36 @@ ${send_request_default}`,
|
|
|
1245
1325
|
)
|
|
1246
1326
|
});
|
|
1247
1327
|
const folders = [
|
|
1248
|
-
getFolderExports(
|
|
1328
|
+
getFolderExports(join2(output, "outputs"), settings.useTsExtension),
|
|
1249
1329
|
getFolderExports(
|
|
1250
|
-
|
|
1330
|
+
join2(output, "inputs"),
|
|
1251
1331
|
settings.useTsExtension,
|
|
1252
1332
|
["ts"],
|
|
1253
|
-
(dirent) => dirent.isDirectory() && dirent.name
|
|
1333
|
+
(dirent) => dirent.isDirectory() && ["schemas"].includes(dirent.name)
|
|
1254
1334
|
),
|
|
1255
|
-
getFolderExports(
|
|
1335
|
+
getFolderExports(join2(output, "api"), settings.useTsExtension),
|
|
1336
|
+
getFolderExports(
|
|
1337
|
+
join2(output, "http"),
|
|
1338
|
+
settings.useTsExtension,
|
|
1339
|
+
["ts"],
|
|
1340
|
+
(dirent) => dirent.name !== "response.ts"
|
|
1341
|
+
)
|
|
1256
1342
|
];
|
|
1257
1343
|
if (modelsImports.length) {
|
|
1258
1344
|
folders.push(
|
|
1259
|
-
getFolderExports(
|
|
1345
|
+
getFolderExports(join2(output, "models"), settings.useTsExtension)
|
|
1260
1346
|
);
|
|
1261
1347
|
}
|
|
1262
|
-
const [outputIndex, inputsIndex, httpIndex, modelsIndex] = await Promise.all(folders);
|
|
1348
|
+
const [outputIndex, inputsIndex, apiIndex, httpIndex, modelsIndex] = await Promise.all(folders);
|
|
1263
1349
|
await writeFiles(output, {
|
|
1350
|
+
"api/index.ts": apiIndex,
|
|
1264
1351
|
"outputs/index.ts": outputIndex,
|
|
1265
1352
|
"inputs/index.ts": inputsIndex || null,
|
|
1266
1353
|
"http/index.ts": httpIndex,
|
|
1267
1354
|
...modelsImports.length ? { "models/index.ts": modelsIndex } : {}
|
|
1268
1355
|
});
|
|
1269
1356
|
await writeFiles(output, {
|
|
1270
|
-
"index.ts": await getFolderExports(output, settings.useTsExtension)
|
|
1357
|
+
"index.ts": await getFolderExports(output, settings.useTsExtension, ["ts"])
|
|
1271
1358
|
});
|
|
1272
1359
|
if (settings.mode === "full") {
|
|
1273
1360
|
await writeFiles(settings.output, {
|