@contractkit/plugin-typescript 0.30.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 +22 -0
- package/README.md +8 -1
- package/dist/codegen-mcp.d.ts.map +1 -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 +449 -104
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/codegen-mcp.ts +15 -7
- package/src/codegen-operation.ts +207 -96
- package/src/codegen-sdk.ts +274 -49
- package/src/index.ts +1 -1
- package/tests/codegen-operation.test.ts +225 -4
- 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,20 +1182,6 @@ 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 helpers = [];
|
|
1186
|
-
if (opNeedsScalar(root, "binary")) {
|
|
1187
|
-
helpers.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
|
|
1188
|
-
}
|
|
1189
|
-
if (opNeedsScalar(root, "datetime")) {
|
|
1190
|
-
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' }));`);
|
|
1191
|
-
}
|
|
1192
|
-
if (opNeedsScalar(root, "interval")) {
|
|
1193
|
-
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()!);`);
|
|
1194
|
-
}
|
|
1195
|
-
if (opNeedsScalar(root, "json")) {
|
|
1196
|
-
helpers.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
|
|
1197
|
-
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)]));`);
|
|
1198
|
-
}
|
|
1199
1185
|
const lines = [];
|
|
1200
1186
|
lines.push("");
|
|
1201
1187
|
lines.push("/**");
|
|
@@ -1212,6 +1198,25 @@ function generateOp(root, options = {}) {
|
|
|
1212
1198
|
lines.push("");
|
|
1213
1199
|
}
|
|
1214
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
|
+
}
|
|
1215
1220
|
const generated = [
|
|
1216
1221
|
...helpers.length ? [
|
|
1217
1222
|
"",
|
|
@@ -1336,50 +1341,161 @@ function generateHandler(route, op, root, options) {
|
|
|
1336
1341
|
lines.push("");
|
|
1337
1342
|
}
|
|
1338
1343
|
}
|
|
1339
|
-
const
|
|
1344
|
+
const emitted = emittedResponses(op);
|
|
1340
1345
|
const serviceParts = inferService(op, route, file);
|
|
1341
|
-
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 ?? [];
|
|
1342
1360
|
const hasRespHeaders = respHeaders.length > 0;
|
|
1343
|
-
const headersAnnotation = hasRespHeaders ?
|
|
1344
|
-
if (
|
|
1345
|
-
const { annotation, prelude } = formatTypeAnnotation(
|
|
1346
|
-
if (prelude) {
|
|
1347
|
-
|
|
1348
|
-
}
|
|
1349
|
-
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});`);
|
|
1350
1366
|
if (hasRespHeaders) {
|
|
1351
|
-
lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } =
|
|
1367
|
+
lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = ${call};`);
|
|
1352
1368
|
} else {
|
|
1353
|
-
lines.push(` const result: ${annotation} =
|
|
1369
|
+
lines.push(` const result: ${annotation} = ${call};`);
|
|
1354
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};`);
|
|
1355
1379
|
} else {
|
|
1356
|
-
lines.push(` const service = ctx.container.get(${
|
|
1380
|
+
lines.push(` const service = ctx.container.get(${className});`);
|
|
1357
1381
|
if (hasRespHeaders) {
|
|
1358
|
-
lines.push(` const result: { headers: ${headersAnnotation} } =
|
|
1382
|
+
lines.push(` const result: { headers: ${headersAnnotation} } = ${call};`);
|
|
1359
1383
|
} else {
|
|
1360
|
-
lines.push(`
|
|
1384
|
+
lines.push(` ${call};`);
|
|
1361
1385
|
}
|
|
1362
1386
|
}
|
|
1363
1387
|
lines.push("");
|
|
1364
|
-
lines.push(` ctx.status = ${
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
if (h.optional) {
|
|
1369
|
-
lines.push(` if (${accessor} !== undefined) ctx.set('${h.name}', String(${accessor}));`);
|
|
1370
|
-
} else {
|
|
1371
|
-
lines.push(` ctx.set('${h.name}', String(${accessor}));`);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
if (primaryResponse2?.bodyType && primaryResponse2.contentType) {
|
|
1376
|
-
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}';`);
|
|
1377
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;`);
|
|
1378
1396
|
}
|
|
1379
|
-
lines.push(`});`);
|
|
1380
1397
|
return lines;
|
|
1381
1398
|
}
|
|
1382
|
-
__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");
|
|
1383
1499
|
function inferService(op, route, file) {
|
|
1384
1500
|
if (op.service) {
|
|
1385
1501
|
const [cls = "", method] = op.service.split(".");
|
|
@@ -1473,9 +1589,9 @@ function serverTsScalar(name) {
|
|
|
1473
1589
|
}
|
|
1474
1590
|
}
|
|
1475
1591
|
__name(serverTsScalar, "serverTsScalar");
|
|
1476
|
-
function formatTypeAnnotation(bodyType, modelsWithOutput) {
|
|
1592
|
+
function formatTypeAnnotation(bodyType, modelsWithOutput, varName = "resultType") {
|
|
1477
1593
|
if (bodyType.kind === "array") {
|
|
1478
|
-
const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput);
|
|
1594
|
+
const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput, varName);
|
|
1479
1595
|
return {
|
|
1480
1596
|
annotation: `${inner.annotation}[]`,
|
|
1481
1597
|
prelude: inner.prelude
|
|
@@ -1492,8 +1608,8 @@ function formatTypeAnnotation(bodyType, modelsWithOutput) {
|
|
|
1492
1608
|
};
|
|
1493
1609
|
const schema = renderType(bodyType);
|
|
1494
1610
|
return {
|
|
1495
|
-
annotation:
|
|
1496
|
-
prelude: `const
|
|
1611
|
+
annotation: `z.infer<typeof ${varName}>`,
|
|
1612
|
+
prelude: `const ${varName} = ${schema};`
|
|
1497
1613
|
};
|
|
1498
1614
|
}
|
|
1499
1615
|
__name(formatTypeAnnotation, "formatTypeAnnotation");
|
|
@@ -1579,9 +1695,9 @@ function collectTypes(root, modelsWithInput, modelsWithOutput) {
|
|
|
1579
1695
|
}
|
|
1580
1696
|
}
|
|
1581
1697
|
for (const resp of op.responses) {
|
|
1582
|
-
|
|
1583
|
-
collectTypeNodeRefs(
|
|
1584
|
-
collectOutputTypeNodeRefs(
|
|
1698
|
+
for (const body of resp.bodies) {
|
|
1699
|
+
collectTypeNodeRefs(body.bodyType, types);
|
|
1700
|
+
collectOutputTypeNodeRefs(body.bodyType, types, modelsWithOutput);
|
|
1585
1701
|
}
|
|
1586
1702
|
if (resp.headers) {
|
|
1587
1703
|
for (const h of resp.headers) {
|
|
@@ -1724,17 +1840,6 @@ function collectTypeNodeRefs(type, out) {
|
|
|
1724
1840
|
}
|
|
1725
1841
|
}
|
|
1726
1842
|
__name(collectTypeNodeRefs, "collectTypeNodeRefs");
|
|
1727
|
-
function paramSourceNeedsScalar(source, name) {
|
|
1728
|
-
if (!source) return false;
|
|
1729
|
-
if (source.kind === "ref") return false;
|
|
1730
|
-
if (source.kind === "params") return source.nodes.some((p) => typeNeedsScalar(p.type, name));
|
|
1731
|
-
return typeNeedsScalar(source.node, name);
|
|
1732
|
-
}
|
|
1733
|
-
__name(paramSourceNeedsScalar, "paramSourceNeedsScalar");
|
|
1734
|
-
function opNeedsScalar(root, name) {
|
|
1735
|
-
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)));
|
|
1736
|
-
}
|
|
1737
|
-
__name(opNeedsScalar, "opNeedsScalar");
|
|
1738
1843
|
function collectServices(root) {
|
|
1739
1844
|
const services = /* @__PURE__ */ new Set();
|
|
1740
1845
|
const inferredService = `${deriveBaseName(root.file)}Service`;
|
|
@@ -1788,7 +1893,7 @@ __name(deriveTypeImportPath, "deriveTypeImportPath");
|
|
|
1788
1893
|
import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs } from "@contractkit/core";
|
|
1789
1894
|
|
|
1790
1895
|
// src/codegen-sdk.ts
|
|
1791
|
-
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";
|
|
1792
1897
|
import { basename as basename2, dirname as dirname3, relative as relative3 } from "path";
|
|
1793
1898
|
function jsonOrFormSerialize(varName, contentType) {
|
|
1794
1899
|
if (contentType === "application/x-www-form-urlencoded") {
|
|
@@ -1865,16 +1970,17 @@ function generateSdk(root, options = {}) {
|
|
|
1865
1970
|
if (sdkNeedsBigIntReplacer(root, includeInternal)) valueImports.push("bigIntReplacer");
|
|
1866
1971
|
if (sdkNeedsBigIntReviver(root, includeInternal)) valueImports.push("parseJson");
|
|
1867
1972
|
if (sdkNeedsQueryString(root, includeInternal)) valueImports.push("buildQueryString");
|
|
1973
|
+
if (sdkNeedsReadContentType(root, includeInternal)) valueImports.push("readContentType");
|
|
1868
1974
|
if (valueImports.length > 0) {
|
|
1869
1975
|
lines.push(`import { ${valueImports.join(", ")} } from '${rel}';`);
|
|
1870
1976
|
}
|
|
1871
1977
|
} else {
|
|
1872
1978
|
lines.push("");
|
|
1873
|
-
lines.push("export class SdkError extends Error {");
|
|
1979
|
+
lines.push("export class SdkError<TBody = unknown> extends Error {");
|
|
1874
1980
|
lines.push(" constructor(");
|
|
1875
1981
|
lines.push(" public readonly status: number,");
|
|
1876
1982
|
lines.push(" public readonly statusText: string,");
|
|
1877
|
-
lines.push(" public readonly body:
|
|
1983
|
+
lines.push(" public readonly body: TBody,");
|
|
1878
1984
|
lines.push(" public readonly headers: Headers,");
|
|
1879
1985
|
lines.push(" ) {");
|
|
1880
1986
|
lines.push(" super(`${status} ${statusText}`);");
|
|
@@ -1882,7 +1988,16 @@ function generateSdk(root, options = {}) {
|
|
|
1882
1988
|
lines.push(" }");
|
|
1883
1989
|
lines.push("}");
|
|
1884
1990
|
lines.push("");
|
|
1885
|
-
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>;");
|
|
1886
2001
|
lines.push("");
|
|
1887
2002
|
lines.push("export interface SdkOptions {");
|
|
1888
2003
|
lines.push(" baseUrl: string;");
|
|
@@ -1892,9 +2007,13 @@ function generateSdk(root, options = {}) {
|
|
|
1892
2007
|
lines.push(" requestIdFactory?: () => string;");
|
|
1893
2008
|
lines.push("}");
|
|
1894
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("");
|
|
1895
2014
|
lines.push("export function createSdkFetch(options: SdkOptions): SdkFetch {");
|
|
1896
2015
|
lines.push(" const getRequestId = options.requestIdFactory ?? (() => crypto.randomUUID());");
|
|
1897
|
-
lines.push(" return async (url: string, init:
|
|
2016
|
+
lines.push(" return async (url: string, init: SdkRequestInit): Promise<Response> => {");
|
|
1898
2017
|
lines.push(" const baseHeaders = typeof options.headers === 'function'");
|
|
1899
2018
|
lines.push(" ? await options.headers()");
|
|
1900
2019
|
lines.push(" : options.headers ?? {};");
|
|
@@ -1902,7 +2021,7 @@ function generateSdk(root, options = {}) {
|
|
|
1902
2021
|
lines.push(" ...init,");
|
|
1903
2022
|
lines.push(" headers: { ...baseHeaders, 'X-Request-ID': getRequestId(), ...init.headers as Record<string, string> },");
|
|
1904
2023
|
lines.push(" });");
|
|
1905
|
-
lines.push(" if (!res.ok) {");
|
|
2024
|
+
lines.push(" if (!res.ok && !(init.expectStatuses ?? []).includes(res.status)) {");
|
|
1906
2025
|
lines.push(" const text = await res.text();");
|
|
1907
2026
|
lines.push(" let body: unknown;");
|
|
1908
2027
|
lines.push(" try { body = JSON.parse(text); } catch { body = text; }");
|
|
@@ -1933,6 +2052,11 @@ function generateSdk(root, options = {}) {
|
|
|
1933
2052
|
lines.push(JSON_VALUE_TYPE_DECL);
|
|
1934
2053
|
}
|
|
1935
2054
|
lines.push("");
|
|
2055
|
+
const errorAliases = generateErrorBodyAliases(root, options);
|
|
2056
|
+
if (errorAliases.length > 0) {
|
|
2057
|
+
lines.push(...errorAliases);
|
|
2058
|
+
lines.push("");
|
|
2059
|
+
}
|
|
1936
2060
|
lines.push("/**");
|
|
1937
2061
|
const relFile = options.outPath ? relative3(dirname3(options.outPath), root.file) : root.file;
|
|
1938
2062
|
lines.push(` * generated from [${basename2(root.file)}](file://./${relFile})`);
|
|
@@ -1980,19 +2104,37 @@ function generateMethod(route, op, file, options) {
|
|
|
1980
2104
|
const { modelsWithInput, modelsWithOutput } = options;
|
|
1981
2105
|
const params = buildMethodParams(route, op, modelsWithInput);
|
|
1982
2106
|
const paramStr = params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
|
|
1983
|
-
const
|
|
1984
|
-
const
|
|
1985
|
-
const
|
|
1986
|
-
const
|
|
1987
|
-
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 ?? [];
|
|
1988
2114
|
const hasRespHeaders = respHeaders.length > 0;
|
|
1989
|
-
const headersShape = hasRespHeaders ?
|
|
1990
|
-
|
|
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);
|
|
1991
2131
|
const desc = op.description ?? route.description;
|
|
1992
|
-
|
|
2132
|
+
const errorBodyName = thrown.some((r) => r.bodies.length > 0) ? errorBodyTypeName(route, op) : void 0;
|
|
2133
|
+
if (op.name || desc || errorBodyName) {
|
|
1993
2134
|
const tags = [];
|
|
1994
2135
|
if (op.name) tags.push(`@name ${op.name}`);
|
|
1995
2136
|
if (desc) tags.push(`@description ${desc}`);
|
|
2137
|
+
if (errorBodyName) tags.push(`@throws {SdkError<${errorBodyName}>} on ${thrown.map((r) => r.statusCode).join(", ")}`);
|
|
1996
2138
|
const contentLines = tags.flatMap((t) => escapeJsDocLines(t));
|
|
1997
2139
|
if (contentLines.length === 1) {
|
|
1998
2140
|
lines.push(` /** ${contentLines[0]} */`);
|
|
@@ -2002,7 +2144,13 @@ function generateMethod(route, op, file, options) {
|
|
|
2002
2144
|
lines.push(` */`);
|
|
2003
2145
|
}
|
|
2004
2146
|
}
|
|
2005
|
-
|
|
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
|
+
}
|
|
2006
2154
|
const urlExpr = buildUrlExpression(route.path, route.params);
|
|
2007
2155
|
const hasQuery = !!op.query;
|
|
2008
2156
|
let fetchUrl = urlExpr;
|
|
@@ -2062,7 +2210,9 @@ function generateMethod(route, op, file, options) {
|
|
|
2062
2210
|
fetchArgs.push("headers: customHeaders");
|
|
2063
2211
|
}
|
|
2064
2212
|
}
|
|
2065
|
-
|
|
2213
|
+
if (expectStatuses.length > 0) fetchArgs.push(`expectStatuses: [${expectStatuses.join(", ")}]`);
|
|
2214
|
+
const needsResult = isMultiStatus || !isVoid || hasRespHeaders;
|
|
2215
|
+
const resultPrefix = needsResult ? "const result = " : "";
|
|
2066
2216
|
if (fetchArgs.length === 2 && !hasBody && !hasOpHeaders && !hasQuery) {
|
|
2067
2217
|
lines.push(` ${resultPrefix}await this.fetch(\`${fetchUrl}\`, { method: '${httpMethod}' });`);
|
|
2068
2218
|
} else {
|
|
@@ -2072,22 +2222,185 @@ function generateMethod(route, op, file, options) {
|
|
|
2072
2222
|
}
|
|
2073
2223
|
lines.push(` });`);
|
|
2074
2224
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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);
|
|
2078
2239
|
if (isVoid) {
|
|
2079
2240
|
lines.push(` return { headers: { ${headerEntries} } };`);
|
|
2080
2241
|
} else {
|
|
2081
|
-
lines.push(` const data = ${
|
|
2242
|
+
lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
|
|
2082
2243
|
lines.push(` return { data, headers: { ${headerEntries} } };`);
|
|
2083
2244
|
}
|
|
2084
2245
|
} else if (!isVoid) {
|
|
2085
|
-
lines.push(` return ${
|
|
2246
|
+
lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
|
|
2086
2247
|
}
|
|
2087
2248
|
lines.push(" }");
|
|
2088
2249
|
return lines;
|
|
2089
2250
|
}
|
|
2090
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");
|
|
2091
2404
|
function buildUrlExpression(path, _) {
|
|
2092
2405
|
return path.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_match, name) => {
|
|
2093
2406
|
return `\${encodeURIComponent(${name})}`;
|
|
@@ -2324,9 +2637,9 @@ function collectTypes2(root, modelsWithInput, modelsWithOutput, includeInternal
|
|
|
2324
2637
|
}
|
|
2325
2638
|
}
|
|
2326
2639
|
for (const resp of op.responses) {
|
|
2327
|
-
|
|
2328
|
-
collectTypeNodeRefs2(
|
|
2329
|
-
collectOutputTypeNodeRefs2(
|
|
2640
|
+
for (const body of resp.bodies) {
|
|
2641
|
+
collectTypeNodeRefs2(body.bodyType, types);
|
|
2642
|
+
collectOutputTypeNodeRefs2(body.bodyType, types, modelsWithOutput);
|
|
2330
2643
|
}
|
|
2331
2644
|
if (resp.headers) {
|
|
2332
2645
|
for (const h of resp.headers) {
|
|
@@ -2428,6 +2741,16 @@ function sdkNeedsQueryString(root, includeInternal = false) {
|
|
|
2428
2741
|
return false;
|
|
2429
2742
|
}
|
|
2430
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");
|
|
2431
2754
|
function sdkNeedsBigIntReplacer(root, includeInternal = false) {
|
|
2432
2755
|
for (const route of root.routes) {
|
|
2433
2756
|
for (const op of route.operations) {
|
|
@@ -2442,10 +2765,7 @@ function sdkNeedsBigIntReviver(root, includeInternal = false) {
|
|
|
2442
2765
|
for (const route of root.routes) {
|
|
2443
2766
|
for (const op of route.operations) {
|
|
2444
2767
|
if (!includeInternal && resolveModifiers2(route, op).includes("internal")) continue;
|
|
2445
|
-
if (op.responses.some((r) => {
|
|
2446
|
-
if (!r.bodyType) return false;
|
|
2447
|
-
return !r.contentType || classifyContentType2(r.contentType) === "json";
|
|
2448
|
-
})) {
|
|
2768
|
+
if (op.responses.some((r) => r.bodies.some((b) => classifyContentType2(b.contentType) === "json"))) {
|
|
2449
2769
|
return true;
|
|
2450
2770
|
}
|
|
2451
2771
|
}
|
|
@@ -2462,7 +2782,7 @@ function sdkNeedsJson(root, includeInternal = false) {
|
|
|
2462
2782
|
if (src.kind === "params") return src.nodes.some((p) => typeNeedsScalar(p.type, "json"));
|
|
2463
2783
|
return typeNeedsScalar(src.node, "json");
|
|
2464
2784
|
}, "check");
|
|
2465
|
-
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;
|
|
2466
2786
|
}
|
|
2467
2787
|
}
|
|
2468
2788
|
return false;
|
|
@@ -2546,11 +2866,11 @@ function deriveTypeImportPath2(file, template) {
|
|
|
2546
2866
|
__name(deriveTypeImportPath2, "deriveTypeImportPath");
|
|
2547
2867
|
function generateSdkOptions() {
|
|
2548
2868
|
return [
|
|
2549
|
-
"export class SdkError extends Error {",
|
|
2869
|
+
"export class SdkError<TBody = unknown> extends Error {",
|
|
2550
2870
|
" constructor(",
|
|
2551
2871
|
" public readonly status: number,",
|
|
2552
2872
|
" public readonly statusText: string,",
|
|
2553
|
-
" public readonly body:
|
|
2873
|
+
" public readonly body: TBody,",
|
|
2554
2874
|
" public readonly headers: Headers,",
|
|
2555
2875
|
" ) {",
|
|
2556
2876
|
" super(`${status} ${statusText}`);",
|
|
@@ -2558,7 +2878,16 @@ function generateSdkOptions() {
|
|
|
2558
2878
|
" }",
|
|
2559
2879
|
"}",
|
|
2560
2880
|
"",
|
|
2561
|
-
"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>;",
|
|
2562
2891
|
"",
|
|
2563
2892
|
"export interface SdkOptions {",
|
|
2564
2893
|
" baseUrl: string;",
|
|
@@ -2584,9 +2913,13 @@ function generateSdkOptions() {
|
|
|
2584
2913
|
"",
|
|
2585
2914
|
JSON_VALUE_TYPE_DECL,
|
|
2586
2915
|
"",
|
|
2916
|
+
"export function readContentType(res: Response): string {",
|
|
2917
|
+
" return res.headers.get('content-type')?.split(';')[0]?.trim() ?? '';",
|
|
2918
|
+
"}",
|
|
2919
|
+
"",
|
|
2587
2920
|
"export function createSdkFetch(options: SdkOptions): SdkFetch {",
|
|
2588
2921
|
" const getRequestId = options.requestIdFactory ?? (() => crypto.randomUUID());",
|
|
2589
|
-
" return async (url: string, init:
|
|
2922
|
+
" return async (url: string, init: SdkRequestInit): Promise<Response> => {",
|
|
2590
2923
|
" const baseHeaders = typeof options.headers === 'function'",
|
|
2591
2924
|
" ? await options.headers()",
|
|
2592
2925
|
" : options.headers ?? {};",
|
|
@@ -2594,7 +2927,7 @@ function generateSdkOptions() {
|
|
|
2594
2927
|
" ...init,",
|
|
2595
2928
|
" headers: { ...baseHeaders, 'X-Request-ID': getRequestId(), ...init.headers as Record<string, string> },",
|
|
2596
2929
|
" });",
|
|
2597
|
-
" if (!res.ok) {",
|
|
2930
|
+
" if (!res.ok && !(init.expectStatuses ?? []).includes(res.status)) {",
|
|
2598
2931
|
" const text = await res.text();",
|
|
2599
2932
|
" let body: unknown;",
|
|
2600
2933
|
" try { body = JSON.parse(text); } catch { body = text; }",
|
|
@@ -2693,6 +3026,7 @@ function generateAreaClient(input) {
|
|
|
2693
3026
|
const { area, outPath, inlineFiles, subareaClients, sdkOptionsPath } = input;
|
|
2694
3027
|
const className = deriveAreaClientClassName(area);
|
|
2695
3028
|
const collectedMethodLines = [];
|
|
3029
|
+
const collectedErrorAliases = /* @__PURE__ */ new Set();
|
|
2696
3030
|
const seenMethods = /* @__PURE__ */ new Set();
|
|
2697
3031
|
const typesByImportPath = /* @__PURE__ */ new Map();
|
|
2698
3032
|
const unresolvedTypes = /* @__PURE__ */ new Set();
|
|
@@ -2700,6 +3034,7 @@ function generateAreaClient(input) {
|
|
|
2700
3034
|
let needsBigIntReplacer = false;
|
|
2701
3035
|
let needsBigIntReviver = false;
|
|
2702
3036
|
let needsQueryString = false;
|
|
3037
|
+
let needsReadContentType = false;
|
|
2703
3038
|
for (const inline of inlineFiles) {
|
|
2704
3039
|
const includeInternal = inline.codegenOptions.includeInternal ?? false;
|
|
2705
3040
|
const { lines: methodLines, methodNames } = generateClientMethods(inline.root, inline.codegenOptions);
|
|
@@ -2710,10 +3045,12 @@ function generateAreaClient(input) {
|
|
|
2710
3045
|
seenMethods.add(name);
|
|
2711
3046
|
}
|
|
2712
3047
|
collectedMethodLines.push(...methodLines);
|
|
3048
|
+
for (const alias of generateErrorBodyAliases(inline.root, inline.codegenOptions)) collectedErrorAliases.add(alias);
|
|
2713
3049
|
if (sdkNeedsJson(inline.root, includeInternal)) needsJson = true;
|
|
2714
3050
|
if (sdkNeedsBigIntReplacer(inline.root, includeInternal)) needsBigIntReplacer = true;
|
|
2715
3051
|
if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
|
|
2716
3052
|
if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
|
|
3053
|
+
if (sdkNeedsReadContentType(inline.root, includeInternal)) needsReadContentType = true;
|
|
2717
3054
|
const typesForFile = collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
|
|
2718
3055
|
const { modelOutPaths } = inline.codegenOptions;
|
|
2719
3056
|
if (modelOutPaths) {
|
|
@@ -2741,6 +3078,7 @@ function generateAreaClient(input) {
|
|
|
2741
3078
|
if (needsBigIntReplacer) valueImports.push("bigIntReplacer");
|
|
2742
3079
|
if (needsBigIntReviver) valueImports.push("parseJson");
|
|
2743
3080
|
if (needsQueryString) valueImports.push("buildQueryString");
|
|
3081
|
+
if (needsReadContentType) valueImports.push("readContentType");
|
|
2744
3082
|
if (valueImports.length > 0) {
|
|
2745
3083
|
lines.push(`import { ${valueImports.join(", ")} } from '${sdkOptionsRel}';`);
|
|
2746
3084
|
}
|
|
@@ -2765,6 +3103,10 @@ function generateAreaClient(input) {
|
|
|
2765
3103
|
lines.push(`import { ${sc.client.className} } from '${sc.client.importPath}';`);
|
|
2766
3104
|
}
|
|
2767
3105
|
lines.push("");
|
|
3106
|
+
if (collectedErrorAliases.size > 0) {
|
|
3107
|
+
lines.push(...collectedErrorAliases);
|
|
3108
|
+
lines.push("");
|
|
3109
|
+
}
|
|
2768
3110
|
lines.push(`export class ${className} {`);
|
|
2769
3111
|
for (const sc of subareaClients) {
|
|
2770
3112
|
lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
|
|
@@ -3061,7 +3403,7 @@ function renderOutputField(field, outputCase, modelsWithOutput, target) {
|
|
|
3061
3403
|
__name(renderOutputField, "renderOutputField");
|
|
3062
3404
|
|
|
3063
3405
|
// src/codegen-mcp.ts
|
|
3064
|
-
import { resolveModifiers as resolveModifiers3 } from "@contractkit/core";
|
|
3406
|
+
import { resolveModifiers as resolveModifiers3, emittedResponses as emittedResponses2 } from "@contractkit/core";
|
|
3065
3407
|
import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
|
|
3066
3408
|
function mcpConfig(op) {
|
|
3067
3409
|
return op.mcp && typeof op.mcp === "object" ? op.mcp : void 0;
|
|
@@ -3187,12 +3529,15 @@ function argsSchemaExpr(props) {
|
|
|
3187
3529
|
return `z.object({ ${fields} })`;
|
|
3188
3530
|
}
|
|
3189
3531
|
__name(argsSchemaExpr, "argsSchemaExpr");
|
|
3190
|
-
function
|
|
3191
|
-
|
|
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;
|
|
3192
3537
|
}
|
|
3193
|
-
__name(
|
|
3538
|
+
__name(primaryResponseBody, "primaryResponseBody");
|
|
3194
3539
|
function outputSchemaExpr(op) {
|
|
3195
|
-
const body =
|
|
3540
|
+
const body = primaryResponseBody(op);
|
|
3196
3541
|
if (!body) return void 0;
|
|
3197
3542
|
if (body.kind === "ref") return body.name;
|
|
3198
3543
|
if (body.kind === "inlineObject") return renderType(body);
|
|
@@ -3261,7 +3606,7 @@ function collectSchemaIds(ops, modelsWithInput) {
|
|
|
3261
3606
|
}
|
|
3262
3607
|
walkSourceRefs(op.query, ids, modelsWithInput);
|
|
3263
3608
|
walkSourceRefs(op.headers, ids, modelsWithInput);
|
|
3264
|
-
const body =
|
|
3609
|
+
const body = primaryResponseBody(op);
|
|
3265
3610
|
if (body && (body.kind === "ref" || body.kind === "inlineObject")) walkTypeRefs(body, ids, "read");
|
|
3266
3611
|
}
|
|
3267
3612
|
return ids;
|
|
@@ -3365,7 +3710,7 @@ function renderToolClass(plan, file, options) {
|
|
|
3365
3710
|
const props = buildArgsProps(route, op, options.modelsWithInput);
|
|
3366
3711
|
const destructure = props.map((p) => p.key);
|
|
3367
3712
|
const callArgs = buildArgs(route, op);
|
|
3368
|
-
const isVoid = !
|
|
3713
|
+
const isVoid = !primaryResponseBody(op);
|
|
3369
3714
|
const structured = !!outExpr;
|
|
3370
3715
|
lines.push(" async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {");
|
|
3371
3716
|
if (destructure.length > 0) {
|
|
@@ -3777,7 +4122,7 @@ function collectOpRootRefs(root, modelMap) {
|
|
|
3777
4122
|
for (const body of op.request.bodies) seeds.push(body.bodyType);
|
|
3778
4123
|
}
|
|
3779
4124
|
for (const resp of op.responses) {
|
|
3780
|
-
|
|
4125
|
+
for (const body of resp.bodies) seeds.push(body.bodyType);
|
|
3781
4126
|
if (resp.headers) {
|
|
3782
4127
|
for (const h of resp.headers) seeds.push(h.type);
|
|
3783
4128
|
}
|