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