@haddowg/json-api-codegen 1.6.0 → 1.7.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.
@@ -374,11 +374,63 @@ var DescriptorBuilder = class {
374
374
  const contentType = rawContentType(op);
375
375
  if (contentType !== void 0) action.contentType = contentType;
376
376
  }
377
+ const rawOutput = this.rawOutput(op);
378
+ if (rawOutput.length > 0) action.rawOutput = rawOutput;
379
+ const requestContent = Object.entries(op.requestBody?.content ?? {});
380
+ if (requestContent.length > 1) action.inputMedia = requestContent.map(([mediaType, media]) => ({
381
+ mediaType,
382
+ kind: this.inputMediaKind(mediaType, media.schema)
383
+ }));
377
384
  out[name] = action;
378
385
  }
379
386
  return sortRecord(out);
380
387
  }
381
388
  /**
389
+ * The non-JSON:API media types an action answers with under its 2xx responses, each once, in
390
+ * the document's order, with how the client reads its body.
391
+ */
392
+ rawOutput(op) {
393
+ const out = [];
394
+ for (const [code, response] of Object.entries(op.responses ?? {})) {
395
+ if (!code.startsWith("2")) continue;
396
+ for (const [mediaType, media] of Object.entries(response.content ?? {})) {
397
+ if (isJsonApiMediaType(mediaType) || out.some((entry) => entry.mediaType === mediaType)) continue;
398
+ out.push({
399
+ mediaType,
400
+ kind: this.rawMediaKind(mediaType, media.schema)
401
+ });
402
+ }
403
+ }
404
+ return out;
405
+ }
406
+ /**
407
+ * How the client reads or sends a non-JSON:API body: `binary` for `format: binary` (or no schema
408
+ * on a type that isn't text or JSON), `text` for any other string schema (or no schema on a
409
+ * `text/*` type), `json` for any other schema (or no schema on a JSON type).
410
+ */
411
+ rawMediaKind(mediaType, schema) {
412
+ const resolved = this.resolveSchema(schema);
413
+ if (resolved === void 0) {
414
+ if (mediaType.startsWith("text/")) return "text";
415
+ return isJsonMediaType(mediaType) ? "json" : "binary";
416
+ }
417
+ if (resolved.type === "string") return resolved.format === "binary" ? "binary" : "text";
418
+ return "json";
419
+ }
420
+ /** How the client sends one of an action's several request media types. */
421
+ inputMediaKind(mediaType, schema) {
422
+ if (isJsonApiMediaType(mediaType)) return "document";
423
+ if (mediaType.startsWith("multipart/")) return "multipart";
424
+ if (mediaType === "application/x-www-form-urlencoded") return "form";
425
+ return this.rawMediaKind(mediaType, schema);
426
+ }
427
+ /** A schema with a component `$ref` followed, else the schema as is. */
428
+ resolveSchema(schema) {
429
+ if (schema === void 0 || typeof schema.$ref !== "string") return schema;
430
+ const name = refName$1(schema.$ref);
431
+ return name === void 0 ? schema : this.schemas[name] ?? schema;
432
+ }
433
+ /**
382
434
  * The wire type (and cardinality) of a document component's primary `data` — a `$ref` to a
383
435
  * resource, an inline object carrying `properties.type.const` (a create-request body), or an
384
436
  * array of resource/identifier refs (a collection). `undefined` when the type can't be resolved.
@@ -767,6 +819,15 @@ function paramItemsEnum(op, paramName) {
767
819
  function isAtomicMediaType(mediaType) {
768
820
  return mediaType.startsWith(JSON_API_MEDIA_TYPE$1) && mediaType.includes(ATOMIC_EXT);
769
821
  }
822
+ /** Whether a media type is JSON:API, with or without `ext` / `profile` parameters. */
823
+ function isJsonApiMediaType(mediaType) {
824
+ return mediaType.split(";")[0].trim().toLowerCase() === JSON_API_MEDIA_TYPE$1;
825
+ }
826
+ /** Whether a non-JSON:API media type carries JSON: `application/json` or a `+json` suffix. */
827
+ function isJsonMediaType(mediaType) {
828
+ const essence = mediaType.split(";")[0].trim().toLowerCase();
829
+ return essence === "application/json" || essence.endsWith("+json");
830
+ }
770
831
  /** The HTTP methods an action operation may be advertised under, in resolution preference order. */
771
832
  const ACTION_METHODS = [
772
833
  "post",
@@ -905,7 +966,7 @@ function stampedMenu(schema, stamp, warn) {
905
966
  /** Below this the document predates the structures this codegen reads. */
906
967
  const MINIMUM_CONTRACT = 1;
907
968
  /** Above this the document carries structures this codegen does not read. */
908
- const MAXIMUM_CONTRACT = 9;
969
+ const MAXIMUM_CONTRACT = 11;
909
970
  /**
910
971
  * Check the document's `info.x-generator.contract` against the range this codegen reads. A
911
972
  * document older than the range throws; a newer one returns a warning, because its new
@@ -915,8 +976,108 @@ const MAXIMUM_CONTRACT = 9;
915
976
  function checkContract(doc) {
916
977
  const contract = doc.info?.["x-generator"]?.contract;
917
978
  if (typeof contract !== "number") return;
918
- if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 9). Use an older codegen release, or regenerate the document from a newer server.`);
919
- if (contract > 9) return `the document declares contract ${contract} and this codegen reads up to 9. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
979
+ if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 11). Use an older codegen release, or regenerate the document from a newer server.`);
980
+ if (contract > 11) return `the document declares contract ${contract} and this codegen reads up to 11. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
981
+ }
982
+ //#endregion
983
+ //#region src/source.ts
984
+ /**
985
+ * Helpers for writing strings read from the OpenAPI document into generated TypeScript. Every
986
+ * name, literal and comment the emitters take from the document goes through one of these.
987
+ */
988
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
989
+ /** Line terminators as JavaScript counts them. */
990
+ const LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/;
991
+ /** A string as a TS string literal. U+2028/U+2029 are escaped too, which JSON leaves raw. */
992
+ function stringLiteral(value) {
993
+ return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
994
+ }
995
+ /** True when a string is a bare identifier. */
996
+ function isIdentifier(value) {
997
+ return IDENTIFIER.test(value);
998
+ }
999
+ /**
1000
+ * A property key for an object literal or an interface: bare when it's an identifier, quoted
1001
+ * otherwise. `__proto__` is written as a computed key, which a runtime literal keeps as an own
1002
+ * property instead of setting the prototype.
1003
+ */
1004
+ function propertyKey(name) {
1005
+ if (name === "__proto__") return `[${stringLiteral(name)}]`;
1006
+ return isIdentifier(name) ? name : stringLiteral(name);
1007
+ }
1008
+ /** Text that can sit inside a block comment: any `*\/` is broken up so it can't close it. */
1009
+ function commentText(text) {
1010
+ return text.replace(/\*\//g, "* /");
1011
+ }
1012
+ /** {@link commentText} split into lines, for a multi-line block comment. */
1013
+ function commentLines(text) {
1014
+ return commentText(text).split(LINE_BREAK);
1015
+ }
1016
+ /** {@link commentText} on one line, for a single comment line such as the provenance header. */
1017
+ function commentLine(text) {
1018
+ return commentText(text).split(LINE_BREAK).join(" ");
1019
+ }
1020
+ /** Words a type alias can't be named: reserved words and the built-in type names. */
1021
+ const RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([
1022
+ "any",
1023
+ "bigint",
1024
+ "boolean",
1025
+ "break",
1026
+ "case",
1027
+ "catch",
1028
+ "class",
1029
+ "const",
1030
+ "continue",
1031
+ "debugger",
1032
+ "default",
1033
+ "delete",
1034
+ "do",
1035
+ "else",
1036
+ "enum",
1037
+ "export",
1038
+ "extends",
1039
+ "false",
1040
+ "finally",
1041
+ "for",
1042
+ "function",
1043
+ "if",
1044
+ "implements",
1045
+ "import",
1046
+ "in",
1047
+ "instanceof",
1048
+ "interface",
1049
+ "let",
1050
+ "never",
1051
+ "new",
1052
+ "null",
1053
+ "number",
1054
+ "object",
1055
+ "package",
1056
+ "private",
1057
+ "protected",
1058
+ "public",
1059
+ "return",
1060
+ "static",
1061
+ "string",
1062
+ "super",
1063
+ "switch",
1064
+ "symbol",
1065
+ "this",
1066
+ "throw",
1067
+ "true",
1068
+ "try",
1069
+ "typeof",
1070
+ "undefined",
1071
+ "unknown",
1072
+ "var",
1073
+ "void",
1074
+ "while",
1075
+ "with",
1076
+ "yield"
1077
+ ]);
1078
+ /** True when `name` can be used as is for an exported type. */
1079
+ function isTypeName(name) {
1080
+ return isIdentifier(name) && !RESERVED_TYPE_NAMES.has(name);
920
1081
  }
921
1082
  //#endregion
922
1083
  //#region src/error-codes.ts
@@ -981,24 +1142,20 @@ function readErrorCatalogue(doc) {
981
1142
  /** A TSDoc block for one code: its description, then each example detail as an `@example`. */
982
1143
  function codeDoc(entry, indent) {
983
1144
  const lines = [];
984
- if (entry.description !== void 0) lines.push(...entry.description.split("\n"));
1145
+ if (entry.description !== void 0) lines.push(...commentLines(entry.description));
985
1146
  for (const example of entry.examples) {
986
1147
  if (lines.length > 0) lines.push("");
987
1148
  const text = typeof example === "string" ? example : JSON.stringify(example);
988
- lines.push(...`@example ${text}`.split("\n"));
1149
+ lines.push(...commentLines(`@example ${text}`));
989
1150
  }
990
1151
  if (lines.length === 0) return;
991
- if (lines.length === 1) return `${indent}/** ${lines[0].replace(/\*\//g, "* /")} */`;
1152
+ if (lines.length === 1) return `${indent}/** ${lines[0]} */`;
992
1153
  return [
993
1154
  `${indent}/**`,
994
- ...lines.map((line) => `${indent} * ${line.replace(/\*\//g, "* /")}`.trimEnd()),
1155
+ ...lines.map((line) => `${indent} * ${line}`.trimEnd()),
995
1156
  `${indent} */`
996
1157
  ].join("\n");
997
1158
  }
998
- /** A bare object key when the code is a valid identifier, else a quoted one. */
999
- function codeKey(code) {
1000
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(code) ? code : JSON.stringify(code);
1001
- }
1002
1159
  /**
1003
1160
  * Emit `ErrorCodes`, a const object keyed by every documented code with its TSDoc on the key
1004
1161
  * (editors show TSDoc on an object key, not on a string-literal union member), the `ErrorCode`
@@ -1021,7 +1178,7 @@ function emitErrorCodes(catalogue) {
1021
1178
  "export const ErrorCodes = {",
1022
1179
  ...catalogue.codes.flatMap((entry) => {
1023
1180
  const doc = codeDoc(entry, " ");
1024
- const member = ` ${codeKey(entry.code)}: ${JSON.stringify(entry.code)},`;
1181
+ const member = ` ${propertyKey(entry.code)}: ${stringLiteral(entry.code)},`;
1025
1182
  return doc === void 0 ? [member] : [doc, member];
1026
1183
  }),
1027
1184
  "} as const",
@@ -1033,7 +1190,7 @@ function emitErrorCodes(catalogue) {
1033
1190
  "export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]"
1034
1191
  ].join("\n");
1035
1192
  const statusLines = [...catalogue.byStatus].map(([status, codes]) => {
1036
- const members = codes.map((c) => JSON.stringify(c));
1193
+ const members = codes.map((c) => stringLiteral(c));
1037
1194
  const line = ` ${status}: ${members.length === 0 ? "never" : members.join(" | ")}`;
1038
1195
  return line.length <= 100 ? line : ` ${status}:\n${members.map((m) => ` | ${m}`).join("\n")}`;
1039
1196
  });
@@ -1090,12 +1247,12 @@ function deriveProvenance(doc) {
1090
1247
  }
1091
1248
  /**
1092
1249
  * Render the provenance record as the JSDoc header lines stamped into a generated artifact
1093
- * (each already prefixed with ` * `). Deterministic: no timestamp, so re-generating from the
1250
+ * (each already prefixed with ` * `), with the document's text kept to one comment line. Deterministic: no timestamp, so re-generating from the
1094
1251
  * same spec yields byte-identical output.
1095
1252
  */
1096
1253
  function provenanceLines(provenance) {
1097
- const lines = [` * Source spec: ${provenance.source}`, ` * Spec hash: ${provenance.hash}`];
1098
- if (provenance.server !== void 0) lines.splice(1, 0, ` * Server: ${provenance.server}`);
1254
+ const lines = [` * Source spec: ${commentLine(provenance.source)}`, ` * Spec hash: ${provenance.hash}`];
1255
+ if (provenance.server !== void 0) lines.splice(1, 0, ` * Server: ${commentLine(provenance.server)}`);
1099
1256
  return lines;
1100
1257
  }
1101
1258
  //#endregion
@@ -1140,17 +1297,25 @@ function isSchema(value) {
1140
1297
  function pascalCase(value) {
1141
1298
  return value.split(/[^a-zA-Z0-9]+/).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1142
1299
  }
1143
- /** Emit a JSON-string literal (used for keys that aren't bare identifiers and enum members). */
1144
- function quote(value) {
1145
- return JSON.stringify(value);
1146
- }
1147
- /** True when a string is a safe bare object-key identifier. */
1148
- function isIdentifier(value) {
1149
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
1150
- }
1151
- function objectKey(value) {
1152
- return isIdentifier(value) ? value : quote(value);
1300
+ /** The identifier stem for a wire type's interfaces, `Type`-prefixed when it would start with a digit. */
1301
+ function typeStem(type) {
1302
+ const pascal = pascalCase(type);
1303
+ return /^[A-Za-z]/.test(pascal) ? pascal : `Type${pascal}`;
1153
1304
  }
1305
+ /** Names the generated module declares or relies on, which an enum alias must not take. */
1306
+ const MODULE_NAMES = [
1307
+ "ActionTypes",
1308
+ "ApiDescriptor",
1309
+ "Attributes",
1310
+ "ClientOptions",
1311
+ "ErrorCode",
1312
+ "ErrorCodeByStatus",
1313
+ "ErrorCodeGuard",
1314
+ "ErrorCodes",
1315
+ "Record",
1316
+ "ResourceMap",
1317
+ "WriteAttributes"
1318
+ ];
1154
1319
  /**
1155
1320
  * Render a description as a JSDoc comment at the given indent — single-line when it fits,
1156
1321
  * a block otherwise. Returns `undefined` for empty text. Neutralises any `*\/` so a
@@ -1159,7 +1324,7 @@ function objectKey(value) {
1159
1324
  function jsDoc(text, indent) {
1160
1325
  const trimmed = text?.trim();
1161
1326
  if (!trimmed) return;
1162
- const lines = trimmed.replace(/\*\//g, "* /").split("\n");
1327
+ const lines = commentLines(trimmed);
1163
1328
  if (lines.length === 1) return `${indent}/** ${lines[0]} */`;
1164
1329
  return [
1165
1330
  `${indent}/**`,
@@ -1214,6 +1379,34 @@ function memberDoc(schema) {
1214
1379
  if (tags.length === 0) return description;
1215
1380
  return description ? `${description}\n\n${tags.join("\n")}` : tags.join("\n");
1216
1381
  }
1382
+ /** An `ActionTypes` media map: `{ 'text/csv': Alias; … }`. */
1383
+ function mediaMap(aliases) {
1384
+ return `{ ${[...aliases].map(([mediaType, alias]) => `${stringLiteral(mediaType)}: ${alias}`).join("; ")} }`;
1385
+ }
1386
+ /** The schema of `mediaType` under an operation's first 2xx response that lists it. */
1387
+ function firstSuccessSchema(op, mediaType) {
1388
+ for (const [code, response] of Object.entries(op?.responses ?? {})) {
1389
+ const media = code.startsWith("2") ? response.content?.[mediaType] : void 0;
1390
+ if (media !== void 0) return media.schema;
1391
+ }
1392
+ }
1393
+ /**
1394
+ * An action's summary and description as JSDoc, leaving out the prose the projector writes for an
1395
+ * action whose author gave none ("Invoke the `name` action", "Invokes the `name` custom action on
1396
+ * …"), which says nothing the name doesn't.
1397
+ */
1398
+ function actionDoc(name, op) {
1399
+ const summary = op?.summary?.trim();
1400
+ const description = op?.description?.trim();
1401
+ const parts = [];
1402
+ if (summary && summary !== `Invoke the \`${name}\` action`) parts.push(summary);
1403
+ const generated = new RegExp(`^Invokes the \`${escapeRegExp(name)}\` custom action on [^.]*\\.$`);
1404
+ if (description && !generated.test(description)) parts.push(description);
1405
+ return parts.length === 0 ? void 0 : parts.join("\n\n");
1406
+ }
1407
+ function escapeRegExp(value) {
1408
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1409
+ }
1217
1410
  var Emitter = class {
1218
1411
  descriptor;
1219
1412
  atomic;
@@ -1229,6 +1422,10 @@ var Emitter = class {
1229
1422
  * already-named attribute type. Populated as interfaces are emitted.
1230
1423
  */
1231
1424
  emittedInterfaces = /* @__PURE__ */ new Set();
1425
+ /** Enum component name -> the alias name it's emitted under. */
1426
+ enumNames = /* @__PURE__ */ new Map();
1427
+ /** Type names already spoken for: the module's own, the per-type interfaces and enum aliases. */
1428
+ takenNames;
1232
1429
  /** Component names mid-expansion in {@link tsType} — a cycle guard for self/mutually-referential `$ref`s. */
1233
1430
  expanding = /* @__PURE__ */ new Set();
1234
1431
  /**
@@ -1237,6 +1434,10 @@ var Emitter = class {
1237
1434
  * type-map only references aliases that exist (a `none` input/output contributes neither).
1238
1435
  */
1239
1436
  actionAliasNames = /* @__PURE__ */ new Map();
1437
+ /** Raw body components already emitted as a named alias, by component name. */
1438
+ rawBodyAliases = /* @__PURE__ */ new Map();
1439
+ /** JSDoc for entries of the `resourceMap` literal, keyed by their path joined with `/`. */
1440
+ literalDocs = /* @__PURE__ */ new Map();
1240
1441
  /** Provenance of the source document, stamped into the generated header for drift tracking. */
1241
1442
  provenance;
1242
1443
  doc;
@@ -1247,6 +1448,50 @@ var Emitter = class {
1247
1448
  this.schemas = this.doc.components?.schemas ?? {};
1248
1449
  this.baseByType = this.indexBases();
1249
1450
  this.provenance = deriveProvenance(source);
1451
+ this.takenNames = new Set(MODULE_NAMES);
1452
+ for (const type of Object.keys(descriptor)) {
1453
+ const stem = typeStem(type);
1454
+ for (const suffix of [
1455
+ "Attributes",
1456
+ "CreateAttributes",
1457
+ "UpdateAttributes"
1458
+ ]) this.takenNames.add(`${stem}${suffix}`);
1459
+ for (const [name, action] of Object.entries(descriptor[type]?.actions ?? {})) {
1460
+ this.takenNames.add(`${stem}${pascalCase(name)}Input`);
1461
+ this.takenNames.add(`${stem}${pascalCase(name)}Output`);
1462
+ for (const { mediaType } of action.rawOutput ?? []) this.takenNames.add(`${stem}${pascalCase(name)}${pascalCase(mediaType)}Output`);
1463
+ for (const { mediaType } of action.inputMedia ?? []) this.takenNames.add(`${stem}${pascalCase(name)}${pascalCase(mediaType)}Input`);
1464
+ const single = action.contentType;
1465
+ if (single !== void 0 && (single.startsWith("multipart/") || single === "application/x-www-form-urlencoded")) this.takenNames.add(`${stem}${pascalCase(name)}${pascalCase(single)}Input`);
1466
+ const doc = actionDoc(name, this.actionOperation(action));
1467
+ if (doc !== void 0) this.literalDocs.set(`${type}/actions/${name}`, doc);
1468
+ }
1469
+ }
1470
+ }
1471
+ /** The operation an action is invoked over: its declared method, else `POST`. */
1472
+ actionOperation(action) {
1473
+ const method = (action.method ?? "POST").toLowerCase();
1474
+ return this.doc.paths?.[action.path]?.[method];
1475
+ }
1476
+ /**
1477
+ * The alias an enum component is emitted under: its own name when that's a usable type name,
1478
+ * else one derived from it (with a warning), kept clear of every other name in the module.
1479
+ */
1480
+ enumName(component) {
1481
+ const known = this.enumNames.get(component);
1482
+ if (known !== void 0) return known;
1483
+ let name = component;
1484
+ if (!isTypeName(component) || this.takenNames.has(component)) {
1485
+ const pascal = pascalCase(component);
1486
+ let stem = /^[A-Za-z]/.test(pascal) ? pascal : `Enum${pascal}`;
1487
+ if (!isTypeName(stem) || this.takenNames.has(stem)) stem = `${stem}Enum`;
1488
+ name = stem;
1489
+ for (let n = 2; this.takenNames.has(name); n++) name = `${stem}${n}`;
1490
+ console.warn(`Enum component ${stringLiteral(component)} can't be used as a TypeScript type name; emitting it as ${name}.`);
1491
+ }
1492
+ this.takenNames.add(name);
1493
+ this.enumNames.set(component, name);
1494
+ return name;
1250
1495
  }
1251
1496
  /** Map each declared wire type back to the `Base` of its resource schema. */
1252
1497
  indexBases() {
@@ -1307,7 +1552,7 @@ var Emitter = class {
1307
1552
  const base = this.baseByType.get(type);
1308
1553
  const props = base ? this.schemas[`${base}Attributes`]?.properties : void 0;
1309
1554
  const fields = Object.keys(this.descriptor[type].attributes);
1310
- return this.renderInterface(`${pascalCase(type)}Attributes`, props, fields, () => true);
1555
+ return this.renderInterface(`${typeStem(type)}Attributes`, props, fields, () => true);
1311
1556
  }
1312
1557
  /**
1313
1558
  * The per-type WRITE attribute interfaces — `<Pascal>CreateAttributes` and
@@ -1319,7 +1564,7 @@ var Emitter = class {
1319
1564
  writeAttributeInterfaces(type) {
1320
1565
  const base = this.baseByType.get(type);
1321
1566
  if (base === void 0) return [];
1322
- const pascal = pascalCase(type);
1567
+ const pascal = typeStem(type);
1323
1568
  const out = [];
1324
1569
  const create = this.schemas[`${base}CreateAttributes`];
1325
1570
  if (create !== void 0) {
@@ -1346,7 +1591,7 @@ var Emitter = class {
1346
1591
  for (const type of Object.keys(this.descriptor)) {
1347
1592
  const actions = this.descriptor[type]?.actions;
1348
1593
  if (actions === void 0) continue;
1349
- const pascal = pascalCase(type);
1594
+ const pascal = typeStem(type);
1350
1595
  for (const [actionName, action] of Object.entries(actions)) {
1351
1596
  const member = `${pascal}${pascalCase(actionName)}`;
1352
1597
  const names = {};
@@ -1360,7 +1605,35 @@ var Emitter = class {
1360
1605
  out.push(`export type ${member}Output = ${outputType}`);
1361
1606
  names.output = `${member}Output`;
1362
1607
  }
1363
- if (names.input !== void 0 || names.output !== void 0) {
1608
+ const op = this.actionOperation(action);
1609
+ for (const { mediaType, kind } of action.rawOutput ?? []) if (kind === "json") {
1610
+ const schema = firstSuccessSchema(op, mediaType);
1611
+ names.responses ??= /* @__PURE__ */ new Map();
1612
+ names.responses.set(mediaType, this.rawBodyAlias(schema, `${member}${pascalCase(mediaType)}Output`, out));
1613
+ }
1614
+ const singleKind = !(action.input === "raw" && action.inputMedia === void 0) ? void 0 : action.contentType?.startsWith("multipart/") === true ? "multipart" : action.contentType === "application/x-www-form-urlencoded" ? "form" : void 0;
1615
+ const listed = action.inputMedia ?? (singleKind === void 0 || action.contentType === void 0 ? [] : [{
1616
+ mediaType: action.contentType,
1617
+ kind: singleKind
1618
+ }]);
1619
+ for (const { mediaType, kind } of listed) {
1620
+ const schema = op?.requestBody?.content?.[mediaType]?.schema;
1621
+ if (kind === "json") {
1622
+ names.requests ??= /* @__PURE__ */ new Map();
1623
+ names.requests.set(mediaType, this.rawBodyAlias(schema, `${member}${pascalCase(mediaType)}Input`, out));
1624
+ } else if (kind === "multipart" || kind === "form") {
1625
+ const fields = this.formFieldsType(schema, kind);
1626
+ if (fields !== void 0) {
1627
+ const alias = `${member}${pascalCase(mediaType)}Input`;
1628
+ const doc = schema === void 0 ? void 0 : jsDoc(schema.description, "");
1629
+ const declaration = `export type ${alias} = ${fields}`;
1630
+ out.push(doc === void 0 ? declaration : `${doc}\n${declaration}`);
1631
+ names.requests ??= /* @__PURE__ */ new Map();
1632
+ names.requests.set(mediaType, alias);
1633
+ }
1634
+ }
1635
+ }
1636
+ if (names.input !== void 0 || names.output !== void 0 || names.responses !== void 0 || names.requests !== void 0) {
1364
1637
  let byAction = this.actionAliasNames.get(type);
1365
1638
  if (byAction === void 0) {
1366
1639
  byAction = /* @__PURE__ */ new Map();
@@ -1373,6 +1646,83 @@ var Emitter = class {
1373
1646
  return out;
1374
1647
  }
1375
1648
  /**
1649
+ * The alias a JSON-like raw body is typed by. A `$ref` to a component is emitted once under the
1650
+ * component's own name when that name is free; anything else gets `fallback`. The alias source
1651
+ * is pushed onto `out` the first time.
1652
+ */
1653
+ rawBodyAlias(schema, fallback, out) {
1654
+ const target = typeof schema?.$ref === "string" ? refName(schema.$ref) : void 0;
1655
+ const resolved = target === void 0 ? void 0 : this.schemas[target];
1656
+ if (target !== void 0 && resolved !== void 0) {
1657
+ const known = this.rawBodyAliases.get(target);
1658
+ if (known !== void 0) return known;
1659
+ if (isTypeName(target) && !this.takenNames.has(target) && !Array.isArray(resolved.enum)) {
1660
+ this.takenNames.add(target);
1661
+ this.rawBodyAliases.set(target, target);
1662
+ out.push(this.rawBodyDeclaration(target, resolved));
1663
+ this.emittedInterfaces.add(target);
1664
+ return target;
1665
+ }
1666
+ }
1667
+ out.push(this.rawBodyDeclaration(fallback, schema));
1668
+ return fallback;
1669
+ }
1670
+ /**
1671
+ * The typed fields of a multipart or URL-encoded request body whose object schema lists
1672
+ * `properties`, which the client encodes into a `FormData` or `URLSearchParams`. `undefined`
1673
+ * when the schema lists none, so the body stays a raw `FormData` or `URLSearchParams`.
1674
+ */
1675
+ formFieldsType(schema, kind) {
1676
+ let target = schema;
1677
+ if (typeof target?.$ref === "string") target = this.schemas[refName(target.$ref) ?? ""];
1678
+ const props = target?.properties;
1679
+ if (target === void 0 || props === void 0) return;
1680
+ const required = new Set(target.required ?? []);
1681
+ const members = [];
1682
+ const valueTypes = [];
1683
+ for (const [name, prop] of Object.entries(props)) {
1684
+ if (!isSchema(prop)) continue;
1685
+ const type = this.formFieldType(prop, kind);
1686
+ valueTypes.push(type);
1687
+ members.push(`${propertyKey(name)}${required.has(name) ? "" : "?"}: ${type}`);
1688
+ }
1689
+ if (members.length === 0) return;
1690
+ const extra = target["additionalProperties"];
1691
+ if (extra === true || isSchema(extra)) {
1692
+ const additional = isSchema(extra) ? this.formFieldType(extra, kind) : this.formFieldType({}, kind);
1693
+ const all = [...new Set([...valueTypes, additional].flatMap((type) => type.split(" | ")))];
1694
+ members.push(`[field: string]: ${all.join(" | ")} | undefined`);
1695
+ }
1696
+ return `{ ${members.join("; ")} }`;
1697
+ }
1698
+ /**
1699
+ * One form field's type: `Blob | File` for a binary part of a multipart body, an array of
1700
+ * its item type for an array, else the scalar's own type. A field with no usable type takes
1701
+ * any scalar a form can carry.
1702
+ */
1703
+ formFieldType(schema, kind) {
1704
+ const any = kind === "multipart" ? "string | number | boolean | Blob" : "string | number | boolean";
1705
+ let target = schema;
1706
+ if (typeof target.$ref === "string") {
1707
+ const resolved = this.schemas[refName(target.$ref) ?? ""];
1708
+ if (resolved !== void 0 && !Array.isArray(resolved.enum)) target = resolved;
1709
+ }
1710
+ if (kind === "multipart" && target.format === "binary") return "Blob | File";
1711
+ if (target.type === "array") {
1712
+ const item = target.items === void 0 ? any : this.formFieldType(target.items, kind);
1713
+ return item.includes(" ") ? `(${item})[]` : `${item}[]`;
1714
+ }
1715
+ const type = this.tsType(target);
1716
+ return type === "unknown" || type.includes("{") || type.startsWith("Record<") ? any : type;
1717
+ }
1718
+ /** `export type Name = …` for a raw body schema, with its description as JSDoc. */
1719
+ rawBodyDeclaration(name, schema) {
1720
+ const type = schema === void 0 ? "unknown" : this.tsType(schema);
1721
+ const doc = schema === void 0 ? void 0 : jsDoc(schema.description, "");
1722
+ const declaration = `export type ${name} = ${type}`;
1723
+ return doc === void 0 ? declaration : `${doc}\n${declaration}`;
1724
+ }
1725
+ /**
1376
1726
  * Emit the `ActionTypes` interface wiring the emitted per-action aliases onto the typed
1377
1727
  * action surface — `type -> actionName -> { input?; output? }`, the client's fourth type
1378
1728
  * argument. So `client.albums.id(id).actions.reissue(body)` takes `AlbumsReissueInput` and
@@ -1390,9 +1740,11 @@ var Emitter = class {
1390
1740
  const members = [];
1391
1741
  if (names.input !== void 0) members.push(`input: ${names.input}`);
1392
1742
  if (names.output !== void 0) members.push(`output: ${names.output}`);
1393
- actionLines.push(` ${objectKey(actionName)}: { ${members.join("; ")} }`);
1743
+ if (names.responses !== void 0) members.push(`responses: ${mediaMap(names.responses)}`);
1744
+ if (names.requests !== void 0) members.push(`requests: ${mediaMap(names.requests)}`);
1745
+ actionLines.push(` ${propertyKey(actionName)}: { ${members.join("; ")} }`);
1394
1746
  }
1395
- typeLines.push(` ${objectKey(type)}: {\n${actionLines.join("\n")}\n }`);
1747
+ typeLines.push(` ${propertyKey(type)}: {\n${actionLines.join("\n")}\n }`);
1396
1748
  }
1397
1749
  if (typeLines.length === 0) return "export interface ActionTypes {}";
1398
1750
  return `export interface ActionTypes {\n${typeLines.join("\n")}\n}`;
@@ -1440,10 +1792,10 @@ var Emitter = class {
1440
1792
  if (schema.oneOf !== void 0) for (const arm of schema.oneOf) {
1441
1793
  const kind = isSchema(arm.properties?.["kind"]) ? arm.properties["kind"].const : void 0;
1442
1794
  const members = this.pageMemberLimits(arm, " ");
1443
- if (typeof kind === "string" && members.length > 0) body.push(` ${objectKey(kind)}: {\n${members.join("\n")}\n },`);
1795
+ if (typeof kind === "string" && members.length > 0) body.push(` ${propertyKey(kind)}: {\n${members.join("\n")}\n },`);
1444
1796
  }
1445
1797
  else body = this.pageMemberLimits(schema, " ");
1446
- if (body.length > 0) typeLines.push(` ${objectKey(type)}: {\n${body.join("\n")}\n },`);
1798
+ if (body.length > 0) typeLines.push(` ${propertyKey(type)}: {\n${body.join("\n")}\n },`);
1447
1799
  }
1448
1800
  return `${[
1449
1801
  "/**",
@@ -1468,7 +1820,7 @@ var Emitter = class {
1468
1820
  if (limits.length === 0) continue;
1469
1821
  const doc = jsDoc(memberDoc(prop), indent);
1470
1822
  if (doc !== void 0) lines.push(doc);
1471
- lines.push(`${indent}${objectKey(name)}: { ${limits.join(", ")} },`);
1823
+ lines.push(`${indent}${propertyKey(name)}: { ${limits.join(", ")} },`);
1472
1824
  }
1473
1825
  return lines;
1474
1826
  }
@@ -1485,7 +1837,7 @@ var Emitter = class {
1485
1837
  const doc = isSchema(schema) ? jsDoc(memberDoc(schema), " ") : void 0;
1486
1838
  if (doc) lines.push(doc);
1487
1839
  const optional = required(field) ? "" : "?";
1488
- lines.push(` ${objectKey(field)}${optional}: ${tsType}`);
1840
+ lines.push(` ${propertyKey(field)}${optional}: ${tsType}`);
1489
1841
  }
1490
1842
  this.emittedInterfaces.add(name);
1491
1843
  if (lines.length === 0) return `export interface ${name} {}`;
@@ -1498,7 +1850,7 @@ var Emitter = class {
1498
1850
  const resolved = target ? this.schemas[target] : void 0;
1499
1851
  if (target && resolved && Array.isArray(resolved.enum)) {
1500
1852
  this.usedEnums.set(target, resolved);
1501
- return target;
1853
+ return this.enumName(target);
1502
1854
  }
1503
1855
  if (target && this.emittedInterfaces.has(target)) return target;
1504
1856
  if (resolved === void 0 || target === void 0 || this.expanding.has(target)) return "unknown";
@@ -1523,7 +1875,7 @@ var Emitter = class {
1523
1875
  }
1524
1876
  /** The non-nullable TS type for the declared JSON type(s). */
1525
1877
  baseTsType(schema, types) {
1526
- if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum.map((value) => quote(String(value))).join(" | ");
1878
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum.map((value) => stringLiteral(String(value))).join(" | ");
1527
1879
  if (types.length === 0) return "unknown";
1528
1880
  const mapped = types.map((t) => this.scalarOrComposite(schema, t));
1529
1881
  return [...new Set(mapped)].join(" | ");
@@ -1548,7 +1900,7 @@ var Emitter = class {
1548
1900
  for (const [name, prop] of Object.entries(props)) {
1549
1901
  if (!isSchema(prop)) continue;
1550
1902
  const optional = required.has(name) ? "" : "?";
1551
- members.push(`${objectKey(name)}${optional}: ${this.tsType(prop)}`);
1903
+ members.push(`${propertyKey(name)}${optional}: ${this.tsType(prop)}`);
1552
1904
  }
1553
1905
  if (members.length === 0) return "Record<string, unknown>";
1554
1906
  return `{ ${members.join("; ")} }`;
@@ -1565,7 +1917,8 @@ var Emitter = class {
1565
1917
  if (this.usedEnums.size === 0) return;
1566
1918
  return [...this.usedEnums.keys()].sort().map((name) => {
1567
1919
  const schema = this.usedEnums.get(name);
1568
- const alias = `export type ${name} = ${(schema.enum ?? []).map((value) => quote(String(value))).join(" | ")}`;
1920
+ const union = (schema.enum ?? []).map((value) => stringLiteral(String(value))).join(" | ");
1921
+ const alias = `export type ${this.enumName(name)} = ${union}`;
1569
1922
  const doc = jsDoc(this.enumDoc(schema), "");
1570
1923
  return doc ? `${doc}\n${alias}` : alias;
1571
1924
  }).join("\n\n");
@@ -1588,7 +1941,7 @@ var Emitter = class {
1588
1941
  * argument to the runtime factory).
1589
1942
  */
1590
1943
  attributesMap() {
1591
- return `export interface Attributes {\n${Object.keys(this.descriptor).map((type) => ` ${objectKey(type)}: ${pascalCase(type)}Attributes`).join("\n")}\n}`;
1944
+ return `export interface Attributes {\n${Object.keys(this.descriptor).map((type) => ` ${propertyKey(type)}: ${typeStem(type)}Attributes`).join("\n")}\n}`;
1592
1945
  }
1593
1946
  /**
1594
1947
  * Emit the `WriteAttributes` interface mapping each writable wire type to its
@@ -1603,21 +1956,21 @@ var Emitter = class {
1603
1956
  const hasCreate = base !== void 0 && this.schemas[`${base}CreateAttributes`] !== void 0;
1604
1957
  const hasUpdate = base !== void 0 && this.schemas[`${base}UpdateAttributes`] !== void 0;
1605
1958
  if (!hasCreate && !hasUpdate) continue;
1606
- const pascal = pascalCase(type);
1959
+ const pascal = typeStem(type);
1607
1960
  const members = [];
1608
1961
  if (hasCreate) members.push(`create: ${pascal}CreateAttributes`);
1609
1962
  if (hasUpdate) members.push(`update: ${pascal}UpdateAttributes`);
1610
- lines.push(` ${objectKey(type)}: { ${members.join("; ")} }`);
1963
+ lines.push(` ${propertyKey(type)}: { ${members.join("; ")} }`);
1611
1964
  }
1612
1965
  if (lines.length === 0) return "export interface WriteAttributes {}";
1613
1966
  return `export interface WriteAttributes {\n${lines.join("\n")}\n}`;
1614
1967
  }
1615
1968
  /** Emit the descriptor as a literal `as const satisfies ApiDescriptor`. */
1616
1969
  resourceMap() {
1617
- return `export const resourceMap = ${this.literal(this.descriptor, 0)} as const satisfies ApiDescriptor`;
1970
+ return `export const resourceMap = ${this.literal(this.descriptor, 0, "")} as const satisfies ApiDescriptor`;
1618
1971
  }
1619
1972
  /** Render a JSON-serialisable descriptor value as deterministic TS object-literal source. */
1620
- literal(value, depth) {
1973
+ literal(value, depth, path) {
1621
1974
  const indent = " ".repeat(depth);
1622
1975
  const inner = " ".repeat(depth + 1);
1623
1976
  if (Array.isArray(value)) {
@@ -1627,9 +1980,14 @@ var Emitter = class {
1627
1980
  if (value !== null && typeof value === "object") {
1628
1981
  const entries = Object.entries(value);
1629
1982
  if (entries.length === 0) return "{}";
1630
- return `{\n${entries.map(([key, v]) => `${inner}${objectKey(key)}: ${this.literal(v, depth + 1)}`).join(",\n")}\n${indent}}`;
1983
+ return `{\n${entries.map(([key, v]) => {
1984
+ const childPath = path === void 0 ? void 0 : path === "" ? key : `${path}/${key}`;
1985
+ const line = `${inner}${propertyKey(key)}: ${this.literal(v, depth + 1, childPath)}`;
1986
+ const doc = childPath === void 0 ? void 0 : this.literalDocs.get(childPath);
1987
+ return doc === void 0 ? line : `${jsDoc(doc, inner)}\n${line}`;
1988
+ }).join(",\n")}\n${indent}}`;
1631
1989
  }
1632
- return JSON.stringify(value);
1990
+ return typeof value === "string" ? stringLiteral(value) : JSON.stringify(value);
1633
1991
  }
1634
1992
  boundFactory() {
1635
1993
  return [
@@ -1688,7 +2046,7 @@ function header(bundle) {
1688
2046
  ].join("\n");
1689
2047
  }
1690
2048
  function body(bundle) {
1691
- return `export const schemas = ${JSON.stringify(bundle, null, 2)} as const`;
2049
+ return `export const schemas = ${JSON.stringify(bundle, null, 2).replace(/^(\s*)"__proto__":/gm, "$1[\"__proto__\"]:").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")} as const`;
1692
2050
  }
1693
2051
  //#endregion
1694
2052
  //#region src/reader.ts
@@ -1832,4 +2190,4 @@ async function check(config) {
1832
2190
  //#endregion
1833
2191
  export { checkContract as _, readSchemas as a, buildDescriptor as b, detectVerbCollisions as c, hashJson as d, provenanceLines as f, MINIMUM_CONTRACT as g, MAXIMUM_CONTRACT as h, readDocument as i, emit as l, readErrorCatalogue as m, generate as n, emitSchemas as o, emitErrorCodes as p, schemasOutputPath as r, Emitter as s, check as t, deriveProvenance as u, DescriptorBuilder as v, buildAtomic as y };
1834
2192
 
1835
- //# sourceMappingURL=src-C2INS1Wd.mjs.map
2193
+ //# sourceMappingURL=src-DhZyiWge.mjs.map