@contractkit/plugin-typescript 0.29.0 → 0.31.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/.turbo/turbo-build$colon$ci.log +4 -4
- package/.turbo/turbo-test$colon$ci.log +19 -19
- package/CHANGELOG.md +35 -0
- package/README.md +8 -1
- package/dist/codegen-mcp.d.ts.map +1 -1
- package/dist/codegen-operation.d.ts +9 -1
- package/dist/codegen-operation.d.ts.map +1 -1
- package/dist/codegen-sdk.d.ts +8 -0
- package/dist/codegen-sdk.d.ts.map +1 -1
- package/dist/index.js +487 -168
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/codegen-mcp.ts +15 -7
- package/src/codegen-operation.ts +243 -167
- package/src/codegen-sdk.ts +274 -49
- package/src/index.ts +1 -1
- package/tests/codegen-operation.test.ts +288 -7
- package/tests/codegen-sdk.test.ts +184 -6
- package/tests/helpers.ts +20 -1
package/dist/index.js
CHANGED
|
@@ -1105,7 +1105,7 @@ function pascalToDotCase(name) {
|
|
|
1105
1105
|
__name(pascalToDotCase, "pascalToDotCase");
|
|
1106
1106
|
|
|
1107
1107
|
// src/codegen-operation.ts
|
|
1108
|
-
import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType } from "@contractkit/core";
|
|
1108
|
+
import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType, emittedResponses } from "@contractkit/core";
|
|
1109
1109
|
import { basename, dirname as dirname2, relative as relative2 } from "path";
|
|
1110
1110
|
function bodyParserToken(contentType) {
|
|
1111
1111
|
switch (classifyContentType(contentType)) {
|
|
@@ -1182,17 +1182,59 @@ function generateOp(root, options = {}) {
|
|
|
1182
1182
|
const types = collectTypes(root, options.modelsWithInput, options.modelsWithOutput);
|
|
1183
1183
|
const services = collectServices(root);
|
|
1184
1184
|
const routerName = deriveRouterName(root.file);
|
|
1185
|
-
const
|
|
1185
|
+
const lines = [];
|
|
1186
|
+
lines.push("");
|
|
1187
|
+
lines.push("/**");
|
|
1188
|
+
const relFile = options.outPath ? relative2(dirname2(options.outPath), root.file) : root.file;
|
|
1189
|
+
lines.push(` * generated from [${basename(root.file)}](file://./${relFile})`);
|
|
1190
|
+
lines.push("*/");
|
|
1191
|
+
lines.push(`export const ${routerName} = ServerKitRouter();`);
|
|
1192
|
+
lines.push("");
|
|
1193
|
+
const includeInternal = options.includeInternal ?? true;
|
|
1194
|
+
for (const route of root.routes) {
|
|
1195
|
+
for (const op of route.operations) {
|
|
1196
|
+
if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
|
|
1197
|
+
lines.push(...generateHandler(route, op, root, options));
|
|
1198
|
+
lines.push("");
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const handlerBody = lines.join("\n");
|
|
1202
|
+
const references = /* @__PURE__ */ __name((symbol) => new RegExp(`\\b${symbol}\\b`).test(handlerBody), "references");
|
|
1203
|
+
const helpers = [];
|
|
1204
|
+
if (references("_ZodBinary")) {
|
|
1205
|
+
helpers.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
|
|
1206
|
+
}
|
|
1207
|
+
if (references("_ZodDatetime")) {
|
|
1208
|
+
helpers.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
|
|
1209
|
+
}
|
|
1210
|
+
if (references("_ZodInterval")) {
|
|
1211
|
+
helpers.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
|
|
1212
|
+
}
|
|
1213
|
+
const needsZodJson = references("_ZodJson");
|
|
1214
|
+
if (needsZodJson || references("_JsonValue")) {
|
|
1215
|
+
helpers.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
|
|
1216
|
+
}
|
|
1217
|
+
if (needsZodJson) {
|
|
1218
|
+
helpers.push(`const _ZodJson: z.ZodType<_JsonValue> = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(_ZodJson), z.record(z.string(), _ZodJson)]));`);
|
|
1219
|
+
}
|
|
1220
|
+
const generated = [
|
|
1221
|
+
...helpers.length ? [
|
|
1222
|
+
"",
|
|
1223
|
+
...helpers
|
|
1224
|
+
] : [],
|
|
1225
|
+
...lines
|
|
1226
|
+
].join("\n");
|
|
1227
|
+
const uses = /* @__PURE__ */ __name((symbol) => new RegExp(`\\b${symbol}\\b`).test(generated), "uses");
|
|
1186
1228
|
const body = [];
|
|
1187
|
-
const needsSignature = fileNeedsSignature(root);
|
|
1188
|
-
const needsPolicy = fileNeedsPolicy(root);
|
|
1189
1229
|
const koaImports = [
|
|
1190
1230
|
"ServerKitRouter",
|
|
1191
|
-
"bodyParserMiddleware"
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1231
|
+
"bodyParserMiddleware",
|
|
1232
|
+
"requirePolicy",
|
|
1233
|
+
"requireSignature"
|
|
1234
|
+
].filter(uses);
|
|
1235
|
+
if (koaImports.length > 0) {
|
|
1236
|
+
body.push(`import { ${koaImports.join(", ")} } from '@maroonedsoftware/koa';`);
|
|
1237
|
+
}
|
|
1196
1238
|
for (const svc of services) {
|
|
1197
1239
|
const modulePath = root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate);
|
|
1198
1240
|
body.push(`import { ${svc} } from '${modulePath}';`);
|
|
@@ -1200,49 +1242,20 @@ function generateOp(root, options = {}) {
|
|
|
1200
1242
|
if (types.length > 0) {
|
|
1201
1243
|
body.push(...generateTypeImports(types, root.file, options));
|
|
1202
1244
|
}
|
|
1203
|
-
const luxonImports = [
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1245
|
+
const luxonImports = [
|
|
1246
|
+
"DateTime",
|
|
1247
|
+
"Duration",
|
|
1248
|
+
"Interval"
|
|
1249
|
+
].filter(uses);
|
|
1207
1250
|
if (luxonImports.length > 0) {
|
|
1208
1251
|
body.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
|
|
1209
1252
|
}
|
|
1210
|
-
if (
|
|
1253
|
+
if (uses("parseAndValidate")) {
|
|
1211
1254
|
body.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
|
|
1212
1255
|
}
|
|
1213
|
-
if (
|
|
1256
|
+
if (uses("MultipartBody")) {
|
|
1214
1257
|
body.push(`import { MultipartBody } from '@maroonedsoftware/multipart';`);
|
|
1215
1258
|
}
|
|
1216
|
-
const helpers = [];
|
|
1217
|
-
if (opNeedsScalar(root, "binary")) {
|
|
1218
|
-
helpers.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
|
|
1219
|
-
}
|
|
1220
|
-
if (opNeedsScalar(root, "datetime")) {
|
|
1221
|
-
helpers.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
|
|
1222
|
-
}
|
|
1223
|
-
if (opNeedsScalar(root, "interval")) {
|
|
1224
|
-
helpers.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
|
|
1225
|
-
}
|
|
1226
|
-
if (opNeedsScalar(root, "json")) {
|
|
1227
|
-
helpers.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
|
|
1228
|
-
helpers.push(`const _ZodJson: z.ZodType<_JsonValue> = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(_ZodJson), z.record(z.string(), _ZodJson)]));`);
|
|
1229
|
-
}
|
|
1230
|
-
const lines = [];
|
|
1231
|
-
lines.push("");
|
|
1232
|
-
lines.push("/**");
|
|
1233
|
-
const relFile = options.outPath ? relative2(dirname2(options.outPath), root.file) : root.file;
|
|
1234
|
-
lines.push(` * generated from [${basename(root.file)}](file://./${relFile})`);
|
|
1235
|
-
lines.push("*/");
|
|
1236
|
-
lines.push(`export const ${routerName} = ServerKitRouter();`);
|
|
1237
|
-
lines.push("");
|
|
1238
|
-
const includeInternal = options.includeInternal ?? true;
|
|
1239
|
-
for (const route of root.routes) {
|
|
1240
|
-
for (const op of route.operations) {
|
|
1241
|
-
if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
|
|
1242
|
-
lines.push(...generateHandler(route, op, root, options));
|
|
1243
|
-
lines.push("");
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
1259
|
const allContent = [
|
|
1247
1260
|
...body,
|
|
1248
1261
|
...helpers.length ? [
|
|
@@ -1328,50 +1341,161 @@ function generateHandler(route, op, root, options) {
|
|
|
1328
1341
|
lines.push("");
|
|
1329
1342
|
}
|
|
1330
1343
|
}
|
|
1331
|
-
const
|
|
1344
|
+
const emitted = emittedResponses(op);
|
|
1332
1345
|
const serviceParts = inferService(op, route, file);
|
|
1333
|
-
const
|
|
1346
|
+
const call = `await service.${serviceParts.methodName}(${buildArgs(route, op)})`;
|
|
1347
|
+
if (emitted.length > 1) {
|
|
1348
|
+
lines.push(...generateMultiStatusResult(emitted, serviceParts.className, call, options));
|
|
1349
|
+
} else {
|
|
1350
|
+
lines.push(...generateSingleStatusResult(emitted[0], op, serviceParts.className, call, options));
|
|
1351
|
+
}
|
|
1352
|
+
lines.push(`});`);
|
|
1353
|
+
return lines;
|
|
1354
|
+
}
|
|
1355
|
+
__name(generateHandler, "generateHandler");
|
|
1356
|
+
function generateSingleStatusResult(resp, op, className, call, options) {
|
|
1357
|
+
const lines = [];
|
|
1358
|
+
const bodies = resp ? resp.bodies : [];
|
|
1359
|
+
const respHeaders = resp?.headers ?? [];
|
|
1334
1360
|
const hasRespHeaders = respHeaders.length > 0;
|
|
1335
|
-
const headersAnnotation = hasRespHeaders ?
|
|
1336
|
-
if (
|
|
1337
|
-
const { annotation, prelude } = formatTypeAnnotation(
|
|
1338
|
-
if (prelude) {
|
|
1339
|
-
|
|
1340
|
-
}
|
|
1341
|
-
lines.push(` const service = ctx.container.get(${serviceParts.className});`);
|
|
1361
|
+
const headersAnnotation = hasRespHeaders ? renderHeadersAnnotation(respHeaders, options.modelsWithOutput) : "";
|
|
1362
|
+
if (bodies.length === 1) {
|
|
1363
|
+
const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput);
|
|
1364
|
+
if (prelude) lines.push(` ${prelude}`);
|
|
1365
|
+
lines.push(` const service = ctx.container.get(${className});`);
|
|
1342
1366
|
if (hasRespHeaders) {
|
|
1343
|
-
lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } =
|
|
1367
|
+
lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = ${call};`);
|
|
1344
1368
|
} else {
|
|
1345
|
-
lines.push(` const result: ${annotation} =
|
|
1369
|
+
lines.push(` const result: ${annotation} = ${call};`);
|
|
1346
1370
|
}
|
|
1371
|
+
} else if (bodies.length > 1) {
|
|
1372
|
+
const { members, preludes } = renderResponseMembers(resp, options, {
|
|
1373
|
+
includeStatus: false,
|
|
1374
|
+
varPrefix: "result"
|
|
1375
|
+
});
|
|
1376
|
+
for (const prelude of preludes) lines.push(` ${prelude}`);
|
|
1377
|
+
lines.push(` const service = ctx.container.get(${className});`);
|
|
1378
|
+
lines.push(` const result: ${members.join(" | ")} = ${call};`);
|
|
1347
1379
|
} else {
|
|
1348
|
-
lines.push(` const service = ctx.container.get(${
|
|
1380
|
+
lines.push(` const service = ctx.container.get(${className});`);
|
|
1349
1381
|
if (hasRespHeaders) {
|
|
1350
|
-
lines.push(` const result: { headers: ${headersAnnotation} } =
|
|
1382
|
+
lines.push(` const result: { headers: ${headersAnnotation} } = ${call};`);
|
|
1351
1383
|
} else {
|
|
1352
|
-
lines.push(`
|
|
1384
|
+
lines.push(` ${call};`);
|
|
1353
1385
|
}
|
|
1354
1386
|
}
|
|
1355
1387
|
lines.push("");
|
|
1356
|
-
lines.push(` ctx.status = ${
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
if (h.optional) {
|
|
1361
|
-
lines.push(` if (${accessor} !== undefined) ctx.set('${h.name}', String(${accessor}));`);
|
|
1362
|
-
} else {
|
|
1363
|
-
lines.push(` ctx.set('${h.name}', String(${accessor}));`);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
if (primaryResponse2?.bodyType && primaryResponse2.contentType) {
|
|
1368
|
-
lines.push(` ctx.type = '${primaryResponse2.contentType}';`);
|
|
1388
|
+
lines.push(` ctx.status = ${resp?.statusCode ?? op.responses[0]?.statusCode ?? 200};`);
|
|
1389
|
+
lines.push(...headerSetLines(respHeaders, " "));
|
|
1390
|
+
if (bodies.length === 1) {
|
|
1391
|
+
lines.push(` ctx.type = '${bodies[0].contentType}';`);
|
|
1369
1392
|
lines.push(` ctx.body = ${hasRespHeaders ? "result.body" : "result"};`);
|
|
1393
|
+
} else if (bodies.length > 1) {
|
|
1394
|
+
lines.push(` ctx.type = result.contentType;`);
|
|
1395
|
+
lines.push(` ctx.body = result.body;`);
|
|
1370
1396
|
}
|
|
1371
|
-
lines.push(`});`);
|
|
1372
1397
|
return lines;
|
|
1373
1398
|
}
|
|
1374
|
-
__name(
|
|
1399
|
+
__name(generateSingleStatusResult, "generateSingleStatusResult");
|
|
1400
|
+
function generateMultiStatusResult(emitted, className, call, options) {
|
|
1401
|
+
const lines = [];
|
|
1402
|
+
const members = [];
|
|
1403
|
+
const preludes = [];
|
|
1404
|
+
for (const resp of emitted) {
|
|
1405
|
+
const rendered = renderResponseMembers(resp, options, {
|
|
1406
|
+
includeStatus: true,
|
|
1407
|
+
varPrefix: `result${resp.statusCode}`
|
|
1408
|
+
});
|
|
1409
|
+
members.push(...rendered.members);
|
|
1410
|
+
preludes.push(...rendered.preludes);
|
|
1411
|
+
}
|
|
1412
|
+
for (const prelude of preludes) lines.push(` ${prelude}`);
|
|
1413
|
+
lines.push(` const service = ctx.container.get(${className});`);
|
|
1414
|
+
lines.push(` const result:`);
|
|
1415
|
+
for (const member of members) lines.push(` | ${member}`);
|
|
1416
|
+
lines.push(` = ${call};`);
|
|
1417
|
+
lines.push("");
|
|
1418
|
+
lines.push(` ctx.status = result.status;`);
|
|
1419
|
+
lines.push(` switch (result.status) {`);
|
|
1420
|
+
for (const resp of emitted) {
|
|
1421
|
+
lines.push(` case ${resp.statusCode}:`);
|
|
1422
|
+
lines.push(...headerSetLines(resp.headers ?? [], " "));
|
|
1423
|
+
if (resp.bodies.length > 0) {
|
|
1424
|
+
lines.push(` ctx.type = result.contentType;`);
|
|
1425
|
+
lines.push(` ctx.body = result.body;`);
|
|
1426
|
+
}
|
|
1427
|
+
lines.push(` break;`);
|
|
1428
|
+
}
|
|
1429
|
+
lines.push(` }`);
|
|
1430
|
+
return lines;
|
|
1431
|
+
}
|
|
1432
|
+
__name(generateMultiStatusResult, "generateMultiStatusResult");
|
|
1433
|
+
function renderResponseMembers(resp, options, opts) {
|
|
1434
|
+
const bodies = resp.bodies;
|
|
1435
|
+
const headers = resp.headers ?? [];
|
|
1436
|
+
const leading = opts.includeStatus ? [
|
|
1437
|
+
`status: ${resp.statusCode}`
|
|
1438
|
+
] : [];
|
|
1439
|
+
const trailing = headers.length > 0 ? [
|
|
1440
|
+
`headers: ${renderHeadersAnnotation(headers, options.modelsWithOutput)}`
|
|
1441
|
+
] : [];
|
|
1442
|
+
const preludes = [];
|
|
1443
|
+
if (bodies.length === 0) {
|
|
1444
|
+
return {
|
|
1445
|
+
members: [
|
|
1446
|
+
`{ ${[
|
|
1447
|
+
...leading,
|
|
1448
|
+
...trailing
|
|
1449
|
+
].join("; ")} }`
|
|
1450
|
+
],
|
|
1451
|
+
preludes
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
const uniform = bodies.every((b) => bodyTypesStructurallyEqual(b.bodyType, bodies[0].bodyType));
|
|
1455
|
+
if (uniform) {
|
|
1456
|
+
const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput, `${opts.varPrefix}Type`);
|
|
1457
|
+
if (prelude) preludes.push(prelude);
|
|
1458
|
+
const contentType = bodies.map((b) => `'${b.contentType}'`).join(" | ");
|
|
1459
|
+
return {
|
|
1460
|
+
members: [
|
|
1461
|
+
`{ ${[
|
|
1462
|
+
...leading,
|
|
1463
|
+
`contentType: ${contentType}`,
|
|
1464
|
+
`body: ${annotation}`,
|
|
1465
|
+
...trailing
|
|
1466
|
+
].join("; ")} }`
|
|
1467
|
+
],
|
|
1468
|
+
preludes
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const members = bodies.map((b, i) => {
|
|
1472
|
+
const { annotation, prelude } = formatTypeAnnotation(b.bodyType, options.modelsWithOutput, `${opts.varPrefix}Type${i}`);
|
|
1473
|
+
if (prelude) preludes.push(prelude);
|
|
1474
|
+
return `{ ${[
|
|
1475
|
+
...leading,
|
|
1476
|
+
`contentType: '${b.contentType}'`,
|
|
1477
|
+
`body: ${annotation}`,
|
|
1478
|
+
...trailing
|
|
1479
|
+
].join("; ")} }`;
|
|
1480
|
+
});
|
|
1481
|
+
return {
|
|
1482
|
+
members,
|
|
1483
|
+
preludes
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
__name(renderResponseMembers, "renderResponseMembers");
|
|
1487
|
+
function renderHeadersAnnotation(headers, modelsWithOutput) {
|
|
1488
|
+
const fields = headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput, "server")}`);
|
|
1489
|
+
return `{ ${fields.join("; ")} }`;
|
|
1490
|
+
}
|
|
1491
|
+
__name(renderHeadersAnnotation, "renderHeadersAnnotation");
|
|
1492
|
+
function headerSetLines(headers, indent) {
|
|
1493
|
+
return headers.map((h) => {
|
|
1494
|
+
const accessor = `result.headers[${JSON.stringify(headerNameToProperty(h.name))}]`;
|
|
1495
|
+
return h.optional ? `${indent}if (${accessor} !== undefined) ctx.set('${h.name}', String(${accessor}));` : `${indent}ctx.set('${h.name}', String(${accessor}));`;
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
__name(headerSetLines, "headerSetLines");
|
|
1375
1499
|
function inferService(op, route, file) {
|
|
1376
1500
|
if (op.service) {
|
|
1377
1501
|
const [cls = "", method] = op.service.split(".");
|
|
@@ -1465,9 +1589,9 @@ function serverTsScalar(name) {
|
|
|
1465
1589
|
}
|
|
1466
1590
|
}
|
|
1467
1591
|
__name(serverTsScalar, "serverTsScalar");
|
|
1468
|
-
function formatTypeAnnotation(bodyType, modelsWithOutput) {
|
|
1592
|
+
function formatTypeAnnotation(bodyType, modelsWithOutput, varName = "resultType") {
|
|
1469
1593
|
if (bodyType.kind === "array") {
|
|
1470
|
-
const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput);
|
|
1594
|
+
const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput, varName);
|
|
1471
1595
|
return {
|
|
1472
1596
|
annotation: `${inner.annotation}[]`,
|
|
1473
1597
|
prelude: inner.prelude
|
|
@@ -1484,8 +1608,8 @@ function formatTypeAnnotation(bodyType, modelsWithOutput) {
|
|
|
1484
1608
|
};
|
|
1485
1609
|
const schema = renderType(bodyType);
|
|
1486
1610
|
return {
|
|
1487
|
-
annotation:
|
|
1488
|
-
prelude: `const
|
|
1611
|
+
annotation: `z.infer<typeof ${varName}>`,
|
|
1612
|
+
prelude: `const ${varName} = ${schema};`
|
|
1489
1613
|
};
|
|
1490
1614
|
}
|
|
1491
1615
|
__name(formatTypeAnnotation, "formatTypeAnnotation");
|
|
@@ -1571,9 +1695,9 @@ function collectTypes(root, modelsWithInput, modelsWithOutput) {
|
|
|
1571
1695
|
}
|
|
1572
1696
|
}
|
|
1573
1697
|
for (const resp of op.responses) {
|
|
1574
|
-
|
|
1575
|
-
collectTypeNodeRefs(
|
|
1576
|
-
collectOutputTypeNodeRefs(
|
|
1698
|
+
for (const body of resp.bodies) {
|
|
1699
|
+
collectTypeNodeRefs(body.bodyType, types);
|
|
1700
|
+
collectOutputTypeNodeRefs(body.bodyType, types, modelsWithOutput);
|
|
1577
1701
|
}
|
|
1578
1702
|
if (resp.headers) {
|
|
1579
1703
|
for (const h of resp.headers) {
|
|
@@ -1716,28 +1840,6 @@ function collectTypeNodeRefs(type, out) {
|
|
|
1716
1840
|
}
|
|
1717
1841
|
}
|
|
1718
1842
|
__name(collectTypeNodeRefs, "collectTypeNodeRefs");
|
|
1719
|
-
function paramSourceNeedsDateTime(source) {
|
|
1720
|
-
if (!source) return false;
|
|
1721
|
-
if (source.kind === "ref") return false;
|
|
1722
|
-
if (source.kind === "params") return source.nodes.some((p) => typeNeedsDateTime(p.type));
|
|
1723
|
-
return typeNeedsDateTime(source.node);
|
|
1724
|
-
}
|
|
1725
|
-
__name(paramSourceNeedsDateTime, "paramSourceNeedsDateTime");
|
|
1726
|
-
function opNeedsDateTime(root) {
|
|
1727
|
-
return root.routes.some((route) => paramSourceNeedsDateTime(route.params) || route.operations.some((op) => !!op.request?.bodies.some((b) => typeNeedsDateTime(b.bodyType)) || op.responses.some((r) => r.bodyType && typeNeedsDateTime(r.bodyType)) || paramSourceNeedsDateTime(op.query) || paramSourceNeedsDateTime(op.headers)));
|
|
1728
|
-
}
|
|
1729
|
-
__name(opNeedsDateTime, "opNeedsDateTime");
|
|
1730
|
-
function paramSourceNeedsScalar(source, name) {
|
|
1731
|
-
if (!source) return false;
|
|
1732
|
-
if (source.kind === "ref") return false;
|
|
1733
|
-
if (source.kind === "params") return source.nodes.some((p) => typeNeedsScalar(p.type, name));
|
|
1734
|
-
return typeNeedsScalar(source.node, name);
|
|
1735
|
-
}
|
|
1736
|
-
__name(paramSourceNeedsScalar, "paramSourceNeedsScalar");
|
|
1737
|
-
function opNeedsScalar(root, name) {
|
|
1738
|
-
return root.routes.some((route) => paramSourceNeedsScalar(route.params, name) || route.operations.some((op) => !!op.request?.bodies.some((b) => typeNeedsScalar(b.bodyType, name)) || op.responses.some((r) => r.bodyType && typeNeedsScalar(r.bodyType, name)) || paramSourceNeedsScalar(op.query, name) || paramSourceNeedsScalar(op.headers, name)));
|
|
1739
|
-
}
|
|
1740
|
-
__name(opNeedsScalar, "opNeedsScalar");
|
|
1741
1843
|
function collectServices(root) {
|
|
1742
1844
|
const services = /* @__PURE__ */ new Set();
|
|
1743
1845
|
const inferredService = `${deriveBaseName(root.file)}Service`;
|
|
@@ -1755,29 +1857,6 @@ function collectServices(root) {
|
|
|
1755
1857
|
].sort();
|
|
1756
1858
|
}
|
|
1757
1859
|
__name(collectServices, "collectServices");
|
|
1758
|
-
function hasParamSource(source) {
|
|
1759
|
-
if (!source) return false;
|
|
1760
|
-
if (source.kind === "ref") return true;
|
|
1761
|
-
if (source.kind === "params") return source.nodes.length > 0;
|
|
1762
|
-
return true;
|
|
1763
|
-
}
|
|
1764
|
-
__name(hasParamSource, "hasParamSource");
|
|
1765
|
-
function routeNeedsValidation(root) {
|
|
1766
|
-
return root.routes.some((r) => hasParamSource(r.params) || r.operations.some((op) => !!op.request || hasParamSource(op.query) || hasParamSource(op.headers)));
|
|
1767
|
-
}
|
|
1768
|
-
__name(routeNeedsValidation, "routeNeedsValidation");
|
|
1769
|
-
function fileNeedsPolicy(root) {
|
|
1770
|
-
return root.routes.some((route) => route.operations.some((op) => resolveSecurity(route, op, root) !== SECURITY_NONE));
|
|
1771
|
-
}
|
|
1772
|
-
__name(fileNeedsPolicy, "fileNeedsPolicy");
|
|
1773
|
-
function fileNeedsSignature(root) {
|
|
1774
|
-
return root.routes.some((route) => route.operations.some((op) => !!op.signature));
|
|
1775
|
-
}
|
|
1776
|
-
__name(fileNeedsSignature, "fileNeedsSignature");
|
|
1777
|
-
function fileUsesMultipart(root) {
|
|
1778
|
-
return root.routes.some((route) => route.operations.some((op) => (op.request?.bodies ?? []).some((b) => b.contentType === "multipart/form-data")));
|
|
1779
|
-
}
|
|
1780
|
-
__name(fileUsesMultipart, "fileUsesMultipart");
|
|
1781
1860
|
function isValidIdentifier2(name) {
|
|
1782
1861
|
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
1783
1862
|
}
|
|
@@ -1814,7 +1893,7 @@ __name(deriveTypeImportPath, "deriveTypeImportPath");
|
|
|
1814
1893
|
import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs } from "@contractkit/core";
|
|
1815
1894
|
|
|
1816
1895
|
// src/codegen-sdk.ts
|
|
1817
|
-
import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2 } from "@contractkit/core";
|
|
1896
|
+
import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses } from "@contractkit/core";
|
|
1818
1897
|
import { basename as basename2, dirname as dirname3, relative as relative3 } from "path";
|
|
1819
1898
|
function jsonOrFormSerialize(varName, contentType) {
|
|
1820
1899
|
if (contentType === "application/x-www-form-urlencoded") {
|
|
@@ -1891,16 +1970,17 @@ function generateSdk(root, options = {}) {
|
|
|
1891
1970
|
if (sdkNeedsBigIntReplacer(root, includeInternal)) valueImports.push("bigIntReplacer");
|
|
1892
1971
|
if (sdkNeedsBigIntReviver(root, includeInternal)) valueImports.push("parseJson");
|
|
1893
1972
|
if (sdkNeedsQueryString(root, includeInternal)) valueImports.push("buildQueryString");
|
|
1973
|
+
if (sdkNeedsReadContentType(root, includeInternal)) valueImports.push("readContentType");
|
|
1894
1974
|
if (valueImports.length > 0) {
|
|
1895
1975
|
lines.push(`import { ${valueImports.join(", ")} } from '${rel}';`);
|
|
1896
1976
|
}
|
|
1897
1977
|
} else {
|
|
1898
1978
|
lines.push("");
|
|
1899
|
-
lines.push("export class SdkError extends Error {");
|
|
1979
|
+
lines.push("export class SdkError<TBody = unknown> extends Error {");
|
|
1900
1980
|
lines.push(" constructor(");
|
|
1901
1981
|
lines.push(" public readonly status: number,");
|
|
1902
1982
|
lines.push(" public readonly statusText: string,");
|
|
1903
|
-
lines.push(" public readonly body:
|
|
1983
|
+
lines.push(" public readonly body: TBody,");
|
|
1904
1984
|
lines.push(" public readonly headers: Headers,");
|
|
1905
1985
|
lines.push(" ) {");
|
|
1906
1986
|
lines.push(" super(`${status} ${statusText}`);");
|
|
@@ -1908,7 +1988,16 @@ function generateSdk(root, options = {}) {
|
|
|
1908
1988
|
lines.push(" }");
|
|
1909
1989
|
lines.push("}");
|
|
1910
1990
|
lines.push("");
|
|
1911
|
-
lines.push("export
|
|
1991
|
+
lines.push("export interface SdkRequestInit extends RequestInit {");
|
|
1992
|
+
lines.push(" /**");
|
|
1993
|
+
lines.push(" * Statuses this operation declares as values rather than errors \u2014 a 304 from");
|
|
1994
|
+
lines.push(" * conditional-GET middleware, or an error status the service returns deliberately.");
|
|
1995
|
+
lines.push(" * Anything else at or above 400 still throws SdkError.");
|
|
1996
|
+
lines.push(" */");
|
|
1997
|
+
lines.push(" expectStatuses?: number[];");
|
|
1998
|
+
lines.push("}");
|
|
1999
|
+
lines.push("");
|
|
2000
|
+
lines.push("export type SdkFetch = (url: string, init: SdkRequestInit) => Promise<Response>;");
|
|
1912
2001
|
lines.push("");
|
|
1913
2002
|
lines.push("export interface SdkOptions {");
|
|
1914
2003
|
lines.push(" baseUrl: string;");
|
|
@@ -1918,9 +2007,13 @@ function generateSdk(root, options = {}) {
|
|
|
1918
2007
|
lines.push(" requestIdFactory?: () => string;");
|
|
1919
2008
|
lines.push("}");
|
|
1920
2009
|
lines.push("");
|
|
2010
|
+
lines.push("export function readContentType(res: Response): string {");
|
|
2011
|
+
lines.push(" return res.headers.get('content-type')?.split(';')[0]?.trim() ?? '';");
|
|
2012
|
+
lines.push("}");
|
|
2013
|
+
lines.push("");
|
|
1921
2014
|
lines.push("export function createSdkFetch(options: SdkOptions): SdkFetch {");
|
|
1922
2015
|
lines.push(" const getRequestId = options.requestIdFactory ?? (() => crypto.randomUUID());");
|
|
1923
|
-
lines.push(" return async (url: string, init:
|
|
2016
|
+
lines.push(" return async (url: string, init: SdkRequestInit): Promise<Response> => {");
|
|
1924
2017
|
lines.push(" const baseHeaders = typeof options.headers === 'function'");
|
|
1925
2018
|
lines.push(" ? await options.headers()");
|
|
1926
2019
|
lines.push(" : options.headers ?? {};");
|
|
@@ -1928,7 +2021,7 @@ function generateSdk(root, options = {}) {
|
|
|
1928
2021
|
lines.push(" ...init,");
|
|
1929
2022
|
lines.push(" headers: { ...baseHeaders, 'X-Request-ID': getRequestId(), ...init.headers as Record<string, string> },");
|
|
1930
2023
|
lines.push(" });");
|
|
1931
|
-
lines.push(" if (!res.ok) {");
|
|
2024
|
+
lines.push(" if (!res.ok && !(init.expectStatuses ?? []).includes(res.status)) {");
|
|
1932
2025
|
lines.push(" const text = await res.text();");
|
|
1933
2026
|
lines.push(" let body: unknown;");
|
|
1934
2027
|
lines.push(" try { body = JSON.parse(text); } catch { body = text; }");
|
|
@@ -1959,6 +2052,11 @@ function generateSdk(root, options = {}) {
|
|
|
1959
2052
|
lines.push(JSON_VALUE_TYPE_DECL);
|
|
1960
2053
|
}
|
|
1961
2054
|
lines.push("");
|
|
2055
|
+
const errorAliases = generateErrorBodyAliases(root, options);
|
|
2056
|
+
if (errorAliases.length > 0) {
|
|
2057
|
+
lines.push(...errorAliases);
|
|
2058
|
+
lines.push("");
|
|
2059
|
+
}
|
|
1962
2060
|
lines.push("/**");
|
|
1963
2061
|
const relFile = options.outPath ? relative3(dirname3(options.outPath), root.file) : root.file;
|
|
1964
2062
|
lines.push(` * generated from [${basename2(root.file)}](file://./${relFile})`);
|
|
@@ -2006,19 +2104,37 @@ function generateMethod(route, op, file, options) {
|
|
|
2006
2104
|
const { modelsWithInput, modelsWithOutput } = options;
|
|
2007
2105
|
const params = buildMethodParams(route, op, modelsWithInput);
|
|
2008
2106
|
const paramStr = params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
|
|
2009
|
-
const
|
|
2010
|
-
const
|
|
2011
|
-
const
|
|
2012
|
-
const
|
|
2013
|
-
const
|
|
2107
|
+
const observable = observableResponses(op);
|
|
2108
|
+
const thrown = thrownResponses(op);
|
|
2109
|
+
const isMultiStatus = observable.length > 1;
|
|
2110
|
+
const primaryResponse = observable[0];
|
|
2111
|
+
const primaryBodies = primaryResponse ? primaryResponse.bodies : [];
|
|
2112
|
+
const isVoid = primaryBodies.length === 0;
|
|
2113
|
+
const respHeaders = primaryResponse?.headers ?? [];
|
|
2014
2114
|
const hasRespHeaders = respHeaders.length > 0;
|
|
2015
|
-
const headersShape = hasRespHeaders ?
|
|
2016
|
-
|
|
2115
|
+
const headersShape = hasRespHeaders ? renderSdkHeadersShape(respHeaders, modelsWithOutput) : "";
|
|
2116
|
+
let returnMembers;
|
|
2117
|
+
let returnType = "";
|
|
2118
|
+
if (isMultiStatus) {
|
|
2119
|
+
returnMembers = observable.flatMap((r) => sdkResponseMembers(r, modelsWithOutput, true));
|
|
2120
|
+
} else if (primaryBodies.length > 1) {
|
|
2121
|
+
returnMembers = sdkResponseMembers(primaryResponse, modelsWithOutput, false);
|
|
2122
|
+
} else {
|
|
2123
|
+
const dataType = isVoid ? "void" : sdkDataType(primaryBodies[0], modelsWithOutput);
|
|
2124
|
+
returnType = hasRespHeaders ? isVoid ? `{ headers: ${headersShape} }` : `{ data: ${dataType}; headers: ${headersShape} }` : dataType;
|
|
2125
|
+
}
|
|
2126
|
+
if (returnMembers?.length === 1) {
|
|
2127
|
+
returnType = returnMembers[0];
|
|
2128
|
+
returnMembers = void 0;
|
|
2129
|
+
}
|
|
2130
|
+
const expectStatuses = observable.filter((r) => r.statusCode < 200 || r.statusCode >= 300).map((r) => r.statusCode);
|
|
2017
2131
|
const desc = op.description ?? route.description;
|
|
2018
|
-
|
|
2132
|
+
const errorBodyName = thrown.some((r) => r.bodies.length > 0) ? errorBodyTypeName(route, op) : void 0;
|
|
2133
|
+
if (op.name || desc || errorBodyName) {
|
|
2019
2134
|
const tags = [];
|
|
2020
2135
|
if (op.name) tags.push(`@name ${op.name}`);
|
|
2021
2136
|
if (desc) tags.push(`@description ${desc}`);
|
|
2137
|
+
if (errorBodyName) tags.push(`@throws {SdkError<${errorBodyName}>} on ${thrown.map((r) => r.statusCode).join(", ")}`);
|
|
2022
2138
|
const contentLines = tags.flatMap((t) => escapeJsDocLines(t));
|
|
2023
2139
|
if (contentLines.length === 1) {
|
|
2024
2140
|
lines.push(` /** ${contentLines[0]} */`);
|
|
@@ -2028,7 +2144,13 @@ function generateMethod(route, op, file, options) {
|
|
|
2028
2144
|
lines.push(` */`);
|
|
2029
2145
|
}
|
|
2030
2146
|
}
|
|
2031
|
-
|
|
2147
|
+
if (returnMembers) {
|
|
2148
|
+
lines.push(` async ${methodName}(${paramStr}): Promise<`);
|
|
2149
|
+
for (const member of returnMembers) lines.push(` | ${member}`);
|
|
2150
|
+
lines.push(` > {`);
|
|
2151
|
+
} else {
|
|
2152
|
+
lines.push(` async ${methodName}(${paramStr}): Promise<${returnType}> {`);
|
|
2153
|
+
}
|
|
2032
2154
|
const urlExpr = buildUrlExpression(route.path, route.params);
|
|
2033
2155
|
const hasQuery = !!op.query;
|
|
2034
2156
|
let fetchUrl = urlExpr;
|
|
@@ -2088,7 +2210,9 @@ function generateMethod(route, op, file, options) {
|
|
|
2088
2210
|
fetchArgs.push("headers: customHeaders");
|
|
2089
2211
|
}
|
|
2090
2212
|
}
|
|
2091
|
-
|
|
2213
|
+
if (expectStatuses.length > 0) fetchArgs.push(`expectStatuses: [${expectStatuses.join(", ")}]`);
|
|
2214
|
+
const needsResult = isMultiStatus || !isVoid || hasRespHeaders;
|
|
2215
|
+
const resultPrefix = needsResult ? "const result = " : "";
|
|
2092
2216
|
if (fetchArgs.length === 2 && !hasBody && !hasOpHeaders && !hasQuery) {
|
|
2093
2217
|
lines.push(` ${resultPrefix}await this.fetch(\`${fetchUrl}\`, { method: '${httpMethod}' });`);
|
|
2094
2218
|
} else {
|
|
@@ -2098,22 +2222,185 @@ function generateMethod(route, op, file, options) {
|
|
|
2098
2222
|
}
|
|
2099
2223
|
lines.push(` });`);
|
|
2100
2224
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2225
|
+
if (isMultiStatus) {
|
|
2226
|
+
const [fallback, ...rest] = observable;
|
|
2227
|
+
lines.push(` switch (result.status) {`);
|
|
2228
|
+
for (const resp of rest) {
|
|
2229
|
+
lines.push(` case ${resp.statusCode}:`);
|
|
2230
|
+
lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true));
|
|
2231
|
+
}
|
|
2232
|
+
lines.push(` default:`);
|
|
2233
|
+
lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true));
|
|
2234
|
+
lines.push(` }`);
|
|
2235
|
+
} else if (primaryBodies.length > 1) {
|
|
2236
|
+
lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false));
|
|
2237
|
+
} else if (hasRespHeaders) {
|
|
2238
|
+
const headerEntries = sdkHeaderEntries(respHeaders);
|
|
2104
2239
|
if (isVoid) {
|
|
2105
2240
|
lines.push(` return { headers: { ${headerEntries} } };`);
|
|
2106
2241
|
} else {
|
|
2107
|
-
lines.push(` const data = ${
|
|
2242
|
+
lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
|
|
2108
2243
|
lines.push(` return { data, headers: { ${headerEntries} } };`);
|
|
2109
2244
|
}
|
|
2110
2245
|
} else if (!isVoid) {
|
|
2111
|
-
lines.push(` return ${
|
|
2246
|
+
lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
|
|
2112
2247
|
}
|
|
2113
2248
|
lines.push(" }");
|
|
2114
2249
|
return lines;
|
|
2115
2250
|
}
|
|
2116
2251
|
__name(generateMethod, "generateMethod");
|
|
2252
|
+
function sdkDataType(body, modelsWithOutput) {
|
|
2253
|
+
const category = classifyContentType2(body.contentType);
|
|
2254
|
+
if (category === "text") return "string";
|
|
2255
|
+
if (category === "binary") return "Blob";
|
|
2256
|
+
return renderOutputTsType(body.bodyType, modelsWithOutput);
|
|
2257
|
+
}
|
|
2258
|
+
__name(sdkDataType, "sdkDataType");
|
|
2259
|
+
function sdkReadExpr(body, modelsWithOutput) {
|
|
2260
|
+
const category = classifyContentType2(body.contentType);
|
|
2261
|
+
if (category === "text") return "await result.text()";
|
|
2262
|
+
if (category === "binary") return "await result.blob()";
|
|
2263
|
+
return `await parseJson<${renderOutputTsType(body.bodyType, modelsWithOutput)}>(result)`;
|
|
2264
|
+
}
|
|
2265
|
+
__name(sdkReadExpr, "sdkReadExpr");
|
|
2266
|
+
function renderSdkHeadersShape(headers, modelsWithOutput) {
|
|
2267
|
+
const fields = headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`);
|
|
2268
|
+
return `{ ${fields.join("; ")} }`;
|
|
2269
|
+
}
|
|
2270
|
+
__name(renderSdkHeadersShape, "renderSdkHeadersShape");
|
|
2271
|
+
function sdkHeaderEntries(headers) {
|
|
2272
|
+
return headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
|
|
2273
|
+
}
|
|
2274
|
+
__name(sdkHeaderEntries, "sdkHeaderEntries");
|
|
2275
|
+
function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
|
|
2276
|
+
const bodies = resp.bodies;
|
|
2277
|
+
const headers = resp.headers ?? [];
|
|
2278
|
+
const leading = includeStatus ? [
|
|
2279
|
+
`status: ${resp.statusCode}`
|
|
2280
|
+
] : [];
|
|
2281
|
+
const trailing = headers.length > 0 ? [
|
|
2282
|
+
`headers: ${renderSdkHeadersShape(headers, modelsWithOutput)}`
|
|
2283
|
+
] : [];
|
|
2284
|
+
if (bodies.length === 0) {
|
|
2285
|
+
return [
|
|
2286
|
+
`{ ${[
|
|
2287
|
+
...leading,
|
|
2288
|
+
...trailing
|
|
2289
|
+
].join("; ")} }`
|
|
2290
|
+
];
|
|
2291
|
+
}
|
|
2292
|
+
const dataTypes = bodies.map((b) => sdkDataType(b, modelsWithOutput));
|
|
2293
|
+
if (dataTypes.every((t) => t === dataTypes[0])) {
|
|
2294
|
+
const contentType = bodies.map((b) => `'${b.contentType}'`).join(" | ");
|
|
2295
|
+
return [
|
|
2296
|
+
`{ ${[
|
|
2297
|
+
...leading,
|
|
2298
|
+
`contentType: ${contentType}`,
|
|
2299
|
+
`data: ${dataTypes[0]}`,
|
|
2300
|
+
...trailing
|
|
2301
|
+
].join("; ")} }`
|
|
2302
|
+
];
|
|
2303
|
+
}
|
|
2304
|
+
return bodies.map((b, i) => `{ ${[
|
|
2305
|
+
...leading,
|
|
2306
|
+
`contentType: '${b.contentType}'`,
|
|
2307
|
+
`data: ${dataTypes[i]}`,
|
|
2308
|
+
...trailing
|
|
2309
|
+
].join("; ")} }`);
|
|
2310
|
+
}
|
|
2311
|
+
__name(sdkResponseMembers, "sdkResponseMembers");
|
|
2312
|
+
function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
|
|
2313
|
+
const bodies = resp.bodies;
|
|
2314
|
+
const headers = resp.headers ?? [];
|
|
2315
|
+
const leading = includeStatus ? [
|
|
2316
|
+
`status: ${resp.statusCode}`
|
|
2317
|
+
] : [];
|
|
2318
|
+
const trailing = headers.length > 0 ? [
|
|
2319
|
+
`headers: { ${sdkHeaderEntries(headers)} }`
|
|
2320
|
+
] : [];
|
|
2321
|
+
if (bodies.length === 0) {
|
|
2322
|
+
return [
|
|
2323
|
+
`${indent}return { ${[
|
|
2324
|
+
...leading,
|
|
2325
|
+
...trailing
|
|
2326
|
+
].join(", ")} };`
|
|
2327
|
+
];
|
|
2328
|
+
}
|
|
2329
|
+
if (bodies.length === 1) {
|
|
2330
|
+
const fields = [
|
|
2331
|
+
...leading,
|
|
2332
|
+
`contentType: '${bodies[0].contentType}'`,
|
|
2333
|
+
`data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
|
|
2334
|
+
...trailing
|
|
2335
|
+
];
|
|
2336
|
+
return [
|
|
2337
|
+
`${indent}return { ${fields.join(", ")} };`
|
|
2338
|
+
];
|
|
2339
|
+
}
|
|
2340
|
+
const dataTypes = bodies.map((b) => sdkDataType(b, modelsWithOutput));
|
|
2341
|
+
if (dataTypes.every((t) => t === dataTypes[0])) {
|
|
2342
|
+
const cast = bodies.map((b) => `'${b.contentType}'`).join(" | ");
|
|
2343
|
+
const fields = [
|
|
2344
|
+
...leading,
|
|
2345
|
+
`contentType: readContentType(result) as ${cast}`,
|
|
2346
|
+
`data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
|
|
2347
|
+
...trailing
|
|
2348
|
+
];
|
|
2349
|
+
return [
|
|
2350
|
+
`${indent}return { ${fields.join(", ")} };`
|
|
2351
|
+
];
|
|
2352
|
+
}
|
|
2353
|
+
const lines = [
|
|
2354
|
+
`${indent}switch (readContentType(result)) {`
|
|
2355
|
+
];
|
|
2356
|
+
for (const body of bodies.slice(1)) {
|
|
2357
|
+
const fields = [
|
|
2358
|
+
...leading,
|
|
2359
|
+
`contentType: '${body.contentType}'`,
|
|
2360
|
+
`data: ${sdkReadExpr(body, modelsWithOutput)}`,
|
|
2361
|
+
...trailing
|
|
2362
|
+
];
|
|
2363
|
+
lines.push(`${indent} case '${body.contentType}':`);
|
|
2364
|
+
lines.push(`${indent} return { ${fields.join(", ")} };`);
|
|
2365
|
+
}
|
|
2366
|
+
const first = bodies[0];
|
|
2367
|
+
const fallbackFields = [
|
|
2368
|
+
...leading,
|
|
2369
|
+
`contentType: '${first.contentType}'`,
|
|
2370
|
+
`data: ${sdkReadExpr(first, modelsWithOutput)}`,
|
|
2371
|
+
...trailing
|
|
2372
|
+
];
|
|
2373
|
+
lines.push(`${indent} default:`);
|
|
2374
|
+
lines.push(`${indent} return { ${fallbackFields.join(", ")} };`);
|
|
2375
|
+
lines.push(`${indent}}`);
|
|
2376
|
+
return lines;
|
|
2377
|
+
}
|
|
2378
|
+
__name(sdkReturnLines, "sdkReturnLines");
|
|
2379
|
+
function errorBodyTypeName(route, op) {
|
|
2380
|
+
const method = deriveMethodName(op, route);
|
|
2381
|
+
return `${method.charAt(0).toUpperCase()}${method.slice(1)}ErrorBody`;
|
|
2382
|
+
}
|
|
2383
|
+
__name(errorBodyTypeName, "errorBodyTypeName");
|
|
2384
|
+
function generateErrorBodyAliases(root, options) {
|
|
2385
|
+
const includeInternal = options.includeInternal ?? false;
|
|
2386
|
+
const lines = [];
|
|
2387
|
+
for (const route of root.routes) {
|
|
2388
|
+
for (const op of route.operations) {
|
|
2389
|
+
const mods = resolveModifiers2(route, op);
|
|
2390
|
+
if (!includeInternal && mods.includes("internal")) continue;
|
|
2391
|
+
const types = /* @__PURE__ */ new Set();
|
|
2392
|
+
for (const resp of thrownResponses(op)) {
|
|
2393
|
+
for (const body of resp.bodies) types.add(sdkDataType(body, options.modelsWithOutput));
|
|
2394
|
+
}
|
|
2395
|
+
if (types.size === 0) continue;
|
|
2396
|
+
lines.push(`export type ${errorBodyTypeName(route, op)} = ${[
|
|
2397
|
+
...types
|
|
2398
|
+
].join(" | ")};`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
return lines;
|
|
2402
|
+
}
|
|
2403
|
+
__name(generateErrorBodyAliases, "generateErrorBodyAliases");
|
|
2117
2404
|
function buildUrlExpression(path, _) {
|
|
2118
2405
|
return path.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_match, name) => {
|
|
2119
2406
|
return `\${encodeURIComponent(${name})}`;
|
|
@@ -2350,9 +2637,9 @@ function collectTypes2(root, modelsWithInput, modelsWithOutput, includeInternal
|
|
|
2350
2637
|
}
|
|
2351
2638
|
}
|
|
2352
2639
|
for (const resp of op.responses) {
|
|
2353
|
-
|
|
2354
|
-
collectTypeNodeRefs2(
|
|
2355
|
-
collectOutputTypeNodeRefs2(
|
|
2640
|
+
for (const body of resp.bodies) {
|
|
2641
|
+
collectTypeNodeRefs2(body.bodyType, types);
|
|
2642
|
+
collectOutputTypeNodeRefs2(body.bodyType, types, modelsWithOutput);
|
|
2356
2643
|
}
|
|
2357
2644
|
if (resp.headers) {
|
|
2358
2645
|
for (const h of resp.headers) {
|
|
@@ -2454,6 +2741,16 @@ function sdkNeedsQueryString(root, includeInternal = false) {
|
|
|
2454
2741
|
return false;
|
|
2455
2742
|
}
|
|
2456
2743
|
__name(sdkNeedsQueryString, "sdkNeedsQueryString");
|
|
2744
|
+
function sdkNeedsReadContentType(root, includeInternal = false) {
|
|
2745
|
+
for (const route of root.routes) {
|
|
2746
|
+
for (const op of route.operations) {
|
|
2747
|
+
if (!includeInternal && resolveModifiers2(route, op).includes("internal")) continue;
|
|
2748
|
+
if (observableResponses(op).some((r) => r.bodies.length > 1)) return true;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
return false;
|
|
2752
|
+
}
|
|
2753
|
+
__name(sdkNeedsReadContentType, "sdkNeedsReadContentType");
|
|
2457
2754
|
function sdkNeedsBigIntReplacer(root, includeInternal = false) {
|
|
2458
2755
|
for (const route of root.routes) {
|
|
2459
2756
|
for (const op of route.operations) {
|
|
@@ -2468,10 +2765,7 @@ function sdkNeedsBigIntReviver(root, includeInternal = false) {
|
|
|
2468
2765
|
for (const route of root.routes) {
|
|
2469
2766
|
for (const op of route.operations) {
|
|
2470
2767
|
if (!includeInternal && resolveModifiers2(route, op).includes("internal")) continue;
|
|
2471
|
-
if (op.responses.some((r) => {
|
|
2472
|
-
if (!r.bodyType) return false;
|
|
2473
|
-
return !r.contentType || classifyContentType2(r.contentType) === "json";
|
|
2474
|
-
})) {
|
|
2768
|
+
if (op.responses.some((r) => r.bodies.some((b) => classifyContentType2(b.contentType) === "json"))) {
|
|
2475
2769
|
return true;
|
|
2476
2770
|
}
|
|
2477
2771
|
}
|
|
@@ -2488,7 +2782,7 @@ function sdkNeedsJson(root, includeInternal = false) {
|
|
|
2488
2782
|
if (src.kind === "params") return src.nodes.some((p) => typeNeedsScalar(p.type, "json"));
|
|
2489
2783
|
return typeNeedsScalar(src.node, "json");
|
|
2490
2784
|
}, "check");
|
|
2491
|
-
if (!!op.request?.bodies.some((b) => typeNeedsScalar(b.bodyType, "json")) || op.responses.some((r) => r.
|
|
2785
|
+
if (!!op.request?.bodies.some((b) => typeNeedsScalar(b.bodyType, "json")) || op.responses.some((r) => r.bodies.some((b) => typeNeedsScalar(b.bodyType, "json"))) || check(op.query) || check(op.headers) || check(route.params)) return true;
|
|
2492
2786
|
}
|
|
2493
2787
|
}
|
|
2494
2788
|
return false;
|
|
@@ -2572,11 +2866,11 @@ function deriveTypeImportPath2(file, template) {
|
|
|
2572
2866
|
__name(deriveTypeImportPath2, "deriveTypeImportPath");
|
|
2573
2867
|
function generateSdkOptions() {
|
|
2574
2868
|
return [
|
|
2575
|
-
"export class SdkError extends Error {",
|
|
2869
|
+
"export class SdkError<TBody = unknown> extends Error {",
|
|
2576
2870
|
" constructor(",
|
|
2577
2871
|
" public readonly status: number,",
|
|
2578
2872
|
" public readonly statusText: string,",
|
|
2579
|
-
" public readonly body:
|
|
2873
|
+
" public readonly body: TBody,",
|
|
2580
2874
|
" public readonly headers: Headers,",
|
|
2581
2875
|
" ) {",
|
|
2582
2876
|
" super(`${status} ${statusText}`);",
|
|
@@ -2584,7 +2878,16 @@ function generateSdkOptions() {
|
|
|
2584
2878
|
" }",
|
|
2585
2879
|
"}",
|
|
2586
2880
|
"",
|
|
2587
|
-
"export
|
|
2881
|
+
"export interface SdkRequestInit extends RequestInit {",
|
|
2882
|
+
" /**",
|
|
2883
|
+
" * Statuses this operation declares as values rather than errors \u2014 a 304 from",
|
|
2884
|
+
" * conditional-GET middleware, or an error status the service returns deliberately.",
|
|
2885
|
+
" * Anything else at or above 400 still throws SdkError.",
|
|
2886
|
+
" */",
|
|
2887
|
+
" expectStatuses?: number[];",
|
|
2888
|
+
"}",
|
|
2889
|
+
"",
|
|
2890
|
+
"export type SdkFetch = (url: string, init: SdkRequestInit) => Promise<Response>;",
|
|
2588
2891
|
"",
|
|
2589
2892
|
"export interface SdkOptions {",
|
|
2590
2893
|
" baseUrl: string;",
|
|
@@ -2610,9 +2913,13 @@ function generateSdkOptions() {
|
|
|
2610
2913
|
"",
|
|
2611
2914
|
JSON_VALUE_TYPE_DECL,
|
|
2612
2915
|
"",
|
|
2916
|
+
"export function readContentType(res: Response): string {",
|
|
2917
|
+
" return res.headers.get('content-type')?.split(';')[0]?.trim() ?? '';",
|
|
2918
|
+
"}",
|
|
2919
|
+
"",
|
|
2613
2920
|
"export function createSdkFetch(options: SdkOptions): SdkFetch {",
|
|
2614
2921
|
" const getRequestId = options.requestIdFactory ?? (() => crypto.randomUUID());",
|
|
2615
|
-
" return async (url: string, init:
|
|
2922
|
+
" return async (url: string, init: SdkRequestInit): Promise<Response> => {",
|
|
2616
2923
|
" const baseHeaders = typeof options.headers === 'function'",
|
|
2617
2924
|
" ? await options.headers()",
|
|
2618
2925
|
" : options.headers ?? {};",
|
|
@@ -2620,7 +2927,7 @@ function generateSdkOptions() {
|
|
|
2620
2927
|
" ...init,",
|
|
2621
2928
|
" headers: { ...baseHeaders, 'X-Request-ID': getRequestId(), ...init.headers as Record<string, string> },",
|
|
2622
2929
|
" });",
|
|
2623
|
-
" if (!res.ok) {",
|
|
2930
|
+
" if (!res.ok && !(init.expectStatuses ?? []).includes(res.status)) {",
|
|
2624
2931
|
" const text = await res.text();",
|
|
2625
2932
|
" let body: unknown;",
|
|
2626
2933
|
" try { body = JSON.parse(text); } catch { body = text; }",
|
|
@@ -2719,6 +3026,7 @@ function generateAreaClient(input) {
|
|
|
2719
3026
|
const { area, outPath, inlineFiles, subareaClients, sdkOptionsPath } = input;
|
|
2720
3027
|
const className = deriveAreaClientClassName(area);
|
|
2721
3028
|
const collectedMethodLines = [];
|
|
3029
|
+
const collectedErrorAliases = /* @__PURE__ */ new Set();
|
|
2722
3030
|
const seenMethods = /* @__PURE__ */ new Set();
|
|
2723
3031
|
const typesByImportPath = /* @__PURE__ */ new Map();
|
|
2724
3032
|
const unresolvedTypes = /* @__PURE__ */ new Set();
|
|
@@ -2726,6 +3034,7 @@ function generateAreaClient(input) {
|
|
|
2726
3034
|
let needsBigIntReplacer = false;
|
|
2727
3035
|
let needsBigIntReviver = false;
|
|
2728
3036
|
let needsQueryString = false;
|
|
3037
|
+
let needsReadContentType = false;
|
|
2729
3038
|
for (const inline of inlineFiles) {
|
|
2730
3039
|
const includeInternal = inline.codegenOptions.includeInternal ?? false;
|
|
2731
3040
|
const { lines: methodLines, methodNames } = generateClientMethods(inline.root, inline.codegenOptions);
|
|
@@ -2736,10 +3045,12 @@ function generateAreaClient(input) {
|
|
|
2736
3045
|
seenMethods.add(name);
|
|
2737
3046
|
}
|
|
2738
3047
|
collectedMethodLines.push(...methodLines);
|
|
3048
|
+
for (const alias of generateErrorBodyAliases(inline.root, inline.codegenOptions)) collectedErrorAliases.add(alias);
|
|
2739
3049
|
if (sdkNeedsJson(inline.root, includeInternal)) needsJson = true;
|
|
2740
3050
|
if (sdkNeedsBigIntReplacer(inline.root, includeInternal)) needsBigIntReplacer = true;
|
|
2741
3051
|
if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
|
|
2742
3052
|
if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
|
|
3053
|
+
if (sdkNeedsReadContentType(inline.root, includeInternal)) needsReadContentType = true;
|
|
2743
3054
|
const typesForFile = collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
|
|
2744
3055
|
const { modelOutPaths } = inline.codegenOptions;
|
|
2745
3056
|
if (modelOutPaths) {
|
|
@@ -2767,6 +3078,7 @@ function generateAreaClient(input) {
|
|
|
2767
3078
|
if (needsBigIntReplacer) valueImports.push("bigIntReplacer");
|
|
2768
3079
|
if (needsBigIntReviver) valueImports.push("parseJson");
|
|
2769
3080
|
if (needsQueryString) valueImports.push("buildQueryString");
|
|
3081
|
+
if (needsReadContentType) valueImports.push("readContentType");
|
|
2770
3082
|
if (valueImports.length > 0) {
|
|
2771
3083
|
lines.push(`import { ${valueImports.join(", ")} } from '${sdkOptionsRel}';`);
|
|
2772
3084
|
}
|
|
@@ -2791,6 +3103,10 @@ function generateAreaClient(input) {
|
|
|
2791
3103
|
lines.push(`import { ${sc.client.className} } from '${sc.client.importPath}';`);
|
|
2792
3104
|
}
|
|
2793
3105
|
lines.push("");
|
|
3106
|
+
if (collectedErrorAliases.size > 0) {
|
|
3107
|
+
lines.push(...collectedErrorAliases);
|
|
3108
|
+
lines.push("");
|
|
3109
|
+
}
|
|
2794
3110
|
lines.push(`export class ${className} {`);
|
|
2795
3111
|
for (const sc of subareaClients) {
|
|
2796
3112
|
lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
|
|
@@ -3087,7 +3403,7 @@ function renderOutputField(field, outputCase, modelsWithOutput, target) {
|
|
|
3087
3403
|
__name(renderOutputField, "renderOutputField");
|
|
3088
3404
|
|
|
3089
3405
|
// src/codegen-mcp.ts
|
|
3090
|
-
import { resolveModifiers as resolveModifiers3 } from "@contractkit/core";
|
|
3406
|
+
import { resolveModifiers as resolveModifiers3, emittedResponses as emittedResponses2 } from "@contractkit/core";
|
|
3091
3407
|
import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
|
|
3092
3408
|
function mcpConfig(op) {
|
|
3093
3409
|
return op.mcp && typeof op.mcp === "object" ? op.mcp : void 0;
|
|
@@ -3213,12 +3529,15 @@ function argsSchemaExpr(props) {
|
|
|
3213
3529
|
return `z.object({ ${fields} })`;
|
|
3214
3530
|
}
|
|
3215
3531
|
__name(argsSchemaExpr, "argsSchemaExpr");
|
|
3216
|
-
function
|
|
3217
|
-
|
|
3532
|
+
function primaryResponseBody(op) {
|
|
3533
|
+
for (const resp of emittedResponses2(op)) {
|
|
3534
|
+
if (resp.bodies[0]) return resp.bodies[0].bodyType;
|
|
3535
|
+
}
|
|
3536
|
+
return void 0;
|
|
3218
3537
|
}
|
|
3219
|
-
__name(
|
|
3538
|
+
__name(primaryResponseBody, "primaryResponseBody");
|
|
3220
3539
|
function outputSchemaExpr(op) {
|
|
3221
|
-
const body =
|
|
3540
|
+
const body = primaryResponseBody(op);
|
|
3222
3541
|
if (!body) return void 0;
|
|
3223
3542
|
if (body.kind === "ref") return body.name;
|
|
3224
3543
|
if (body.kind === "inlineObject") return renderType(body);
|
|
@@ -3287,7 +3606,7 @@ function collectSchemaIds(ops, modelsWithInput) {
|
|
|
3287
3606
|
}
|
|
3288
3607
|
walkSourceRefs(op.query, ids, modelsWithInput);
|
|
3289
3608
|
walkSourceRefs(op.headers, ids, modelsWithInput);
|
|
3290
|
-
const body =
|
|
3609
|
+
const body = primaryResponseBody(op);
|
|
3291
3610
|
if (body && (body.kind === "ref" || body.kind === "inlineObject")) walkTypeRefs(body, ids, "read");
|
|
3292
3611
|
}
|
|
3293
3612
|
return ids;
|
|
@@ -3391,7 +3710,7 @@ function renderToolClass(plan, file, options) {
|
|
|
3391
3710
|
const props = buildArgsProps(route, op, options.modelsWithInput);
|
|
3392
3711
|
const destructure = props.map((p) => p.key);
|
|
3393
3712
|
const callArgs = buildArgs(route, op);
|
|
3394
|
-
const isVoid = !
|
|
3713
|
+
const isVoid = !primaryResponseBody(op);
|
|
3395
3714
|
const structured = !!outExpr;
|
|
3396
3715
|
lines.push(" async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {");
|
|
3397
3716
|
if (destructure.length > 0) {
|
|
@@ -3803,7 +4122,7 @@ function collectOpRootRefs(root, modelMap) {
|
|
|
3803
4122
|
for (const body of op.request.bodies) seeds.push(body.bodyType);
|
|
3804
4123
|
}
|
|
3805
4124
|
for (const resp of op.responses) {
|
|
3806
|
-
|
|
4125
|
+
for (const body of resp.bodies) seeds.push(body.bodyType);
|
|
3807
4126
|
if (resp.headers) {
|
|
3808
4127
|
for (const h of resp.headers) seeds.push(h.type);
|
|
3809
4128
|
}
|