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