@workos/oagen-emitters 0.18.2 → 0.18.4

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.
Files changed (52) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/{plugin-bqfwowQ3.mjs → plugin-Cciic50q.mjs} +457 -101
  6. package/dist/plugin-Cciic50q.mjs.map +1 -0
  7. package/dist/plugin.mjs +1 -1
  8. package/docs/sdk-architecture/rust.md +2 -2
  9. package/package.json +3 -3
  10. package/src/dotnet/fixtures.ts +17 -3
  11. package/src/dotnet/index.ts +2 -1
  12. package/src/dotnet/models.ts +30 -5
  13. package/src/dotnet/naming.ts +10 -0
  14. package/src/dotnet/tests.ts +5 -1
  15. package/src/go/fixtures.ts +4 -2
  16. package/src/go/index.ts +4 -0
  17. package/src/go/models.ts +4 -2
  18. package/src/go/naming.ts +10 -0
  19. package/src/go/resources.ts +19 -6
  20. package/src/kotlin/index.ts +2 -1
  21. package/src/kotlin/models.ts +5 -2
  22. package/src/kotlin/naming.ts +11 -0
  23. package/src/kotlin/tests.ts +5 -1
  24. package/src/node/field-plan.ts +3 -3
  25. package/src/node/index.ts +2 -1
  26. package/src/node/models.ts +40 -1
  27. package/src/node/naming.ts +10 -0
  28. package/src/node/options.ts +45 -1
  29. package/src/node/resources.ts +76 -19
  30. package/src/node/tests.ts +296 -30
  31. package/src/php/index.ts +2 -1
  32. package/src/php/models.ts +11 -5
  33. package/src/php/naming.ts +10 -0
  34. package/src/php/tests.ts +11 -2
  35. package/src/python/fixtures.ts +4 -3
  36. package/src/python/index.ts +2 -1
  37. package/src/python/models.ts +12 -6
  38. package/src/python/naming.ts +10 -0
  39. package/src/python/tests.ts +11 -6
  40. package/src/ruby/index.ts +2 -1
  41. package/src/ruby/models.ts +10 -7
  42. package/src/ruby/naming.ts +10 -0
  43. package/src/ruby/rbi.ts +3 -1
  44. package/src/ruby/tests.ts +4 -1
  45. package/src/rust/index.ts +2 -1
  46. package/src/rust/models.ts +87 -15
  47. package/src/rust/naming.ts +10 -0
  48. package/src/rust/resources.ts +6 -2
  49. package/src/shared/file-header.ts +13 -0
  50. package/test/node/resources.test.ts +31 -2
  51. package/test/rust/models.test.ts +49 -0
  52. package/dist/plugin-bqfwowQ3.mjs.map +0 -1
@@ -3637,6 +3637,29 @@ function isHandOwnedType(ctx, name) {
3637
3637
  if (!configured || configured.length === 0) return false;
3638
3638
  return configured.includes(name);
3639
3639
  }
3640
+ /**
3641
+ * Resolve the Node operation override for an operation, keyed by "METHOD /path".
3642
+ */
3643
+ function operationOverrideFor$1(ctx, op) {
3644
+ return nodeOptions(ctx).operationOverrides?.[`${op.httpMethod.toUpperCase()} ${op.path}`];
3645
+ }
3646
+ /**
3647
+ * `planOperation` plus the Node `responseModel` override. When an operation
3648
+ * override supplies `responseModel`, the resolved response model name is
3649
+ * replaced so the resource and its generated test reference the desired wire
3650
+ * type and deserializer. Use this everywhere the Node emitter would otherwise
3651
+ * call `planOperation(op)` directly so resource and test stay in lockstep.
3652
+ */
3653
+ function planOperationFor(op, ctx) {
3654
+ const plan = planOperation(op);
3655
+ const responseModel = operationOverrideFor$1(ctx, op)?.responseModel;
3656
+ if (responseModel && responseModel !== plan.responseModelName) return {
3657
+ ...plan,
3658
+ responseModelName: responseModel,
3659
+ isModelResponse: true
3660
+ };
3661
+ return plan;
3662
+ }
3640
3663
  //#endregion
3641
3664
  //#region src/node/live-surface.ts
3642
3665
  const SRC_DIR = "src";
@@ -4913,7 +4936,7 @@ function serializerHasBaselineIncompatibility(model, baselineResponse, baselineD
4913
4936
  const irDomainFields = /* @__PURE__ */ new Set();
4914
4937
  for (const field of model.fields) {
4915
4938
  irWireFields.add(wireFieldName(field.name));
4916
- irDomainFields.add(fieldName$6(field.name));
4939
+ irDomainFields.add(fieldName$6(field.domainName ?? field.name));
4917
4940
  }
4918
4941
  for (const [wireField2, fieldDef] of Object.entries(baselineResponse.fields)) {
4919
4942
  if (fieldDef.optional) continue;
@@ -4957,7 +4980,7 @@ function serializerHasBaselineIncompatibility(model, baselineResponse, baselineD
4957
4980
  return false;
4958
4981
  }
4959
4982
  function planDeserializeField(field, baselineDomain, baselineResponse, skipFormatFields, ctx) {
4960
- const domain = fieldName$6(field.name);
4983
+ const domain = fieldName$6(field.domainName ?? field.name);
4961
4984
  const wireAccess = `response.${wireFieldName(field.name)}`;
4962
4985
  const skip = skipFormatFields.has(field.name);
4963
4986
  const baselineDomainField = baselineDomain?.fields?.[domain];
@@ -4993,7 +5016,7 @@ function planDeserializeGuard(field, expr, wireAccess, effectivelyOptional, isNe
4993
5016
  }
4994
5017
  function planSerializeField(field, baselineDomain, baselineResponse, skipFormatFields, ctx) {
4995
5018
  const wire = wireFieldName(field.name);
4996
- const domain = fieldName$6(field.name);
5019
+ const domain = fieldName$6(field.domainName ?? field.name);
4997
5020
  const domainAccess = `model.${domain}`;
4998
5021
  const skip = skipFormatFields.has(field.name);
4999
5022
  const baselineWireField = baselineResponse?.fields?.[wire];
@@ -5533,9 +5556,6 @@ function existingInterfaceBarrelExports(ctx, serviceDir, stem) {
5533
5556
  const escapedStem = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5534
5557
  return new RegExp(`export\\s+(?:type\\s+)?(?:\\*|\\{[^}]+\\})\\s+from\\s+['"]\\./${escapedStem}['"]`).test(content);
5535
5558
  }
5536
- function operationOverrideFor$1(ctx, op) {
5537
- return nodeOptions(ctx).operationOverrides?.[`${op.httpMethod.toUpperCase()} ${op.path}`];
5538
- }
5539
5559
  function baselineMethodFor$1(service, method, ctx) {
5540
5560
  const serviceClass = resolveResourceClassName$3(service, ctx);
5541
5561
  return ctx.apiSurface?.classes?.[serviceClass]?.methods?.[method]?.[0];
@@ -5561,10 +5581,11 @@ function optionsObjectParam$1(method) {
5561
5581
  const [param] = method.params;
5562
5582
  if (param.name !== "options") return void 0;
5563
5583
  if (param.passingStyle && param.passingStyle !== "options_object") return void 0;
5564
- if (!param.type || /^(Record|object|any|unknown)\b/.test(param.type)) return void 0;
5584
+ const type = param.type?.replace(/(?:\s*\|\s*(?:undefined|null))+\s*$/, "").trim();
5585
+ if (!type || /^(Record|object|any|unknown)\b/.test(type)) return void 0;
5565
5586
  return {
5566
5587
  name: "options",
5567
- type: param.type,
5588
+ type,
5568
5589
  optional: param.optional === true,
5569
5590
  generated: false
5570
5591
  };
@@ -5592,10 +5613,21 @@ function optionsObjectShouldBeOptional(op, plan, resolvedOp) {
5592
5613
  function operationHasOptionsInput(op, plan, resolvedOp) {
5593
5614
  return op.pathParams.length > 0 || plan.hasBody || plan.isPaginated || visibleQueryParamsForOptions(op, plan, resolvedOp).length > 0;
5594
5615
  }
5616
+ function isClosedObjectLiteral(type) {
5617
+ const t = type.trim();
5618
+ if (!t.startsWith("{")) return false;
5619
+ let depth = 0;
5620
+ for (let i = 0; i < t.length; i++) {
5621
+ const ch = t[i];
5622
+ if (ch === "{") depth++;
5623
+ else if (ch === "}" && --depth === 0) return i === t.length - 1;
5624
+ }
5625
+ return false;
5626
+ }
5595
5627
  function optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolvedOp) {
5596
5628
  const baseline = optionsObjectParam$1(baselineMethod);
5597
5629
  if (baseline) {
5598
- if (baseline.type.trimStart().startsWith("{") && isNodeOwnedService(ctx, service.name, resolveResourceClassName$3(service, ctx))) {
5630
+ if (isClosedObjectLiteral(baseline.type) && isNodeOwnedService(ctx, service.name, resolveResourceClassName$3(service, ctx))) {
5599
5631
  const body = extractRequestBodyType(op, ctx);
5600
5632
  if (body?.kind === "model") return {
5601
5633
  name: "options",
@@ -5754,14 +5786,14 @@ function generateOptionsInterfaces(service, ctx, specEnumNames) {
5754
5786
  const resolvedLookup = buildResolvedLookup(ctx);
5755
5787
  const plans = service.operations.map((op) => ({
5756
5788
  op,
5757
- plan: planOperation(op),
5789
+ plan: planOperationFor(op, ctx),
5758
5790
  method: resolveMethodName$6(op, service, ctx)
5759
5791
  }));
5760
5792
  for (const { op, plan, method } of plans) {
5761
5793
  const resolvedOp = lookupResolved(op, resolvedLookup);
5762
5794
  const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineMethodFor$1(service, method, ctx), resolvedOp);
5763
5795
  if (!optionInfo?.generated) continue;
5764
- if (baselineTypeSourceFile(ctx, optionInfo.type)) continue;
5796
+ if (op.pathParams.length === 0 && baselineTypeSourceFile(ctx, optionInfo.type)) continue;
5765
5797
  const optionsName = optionInfo.type;
5766
5798
  const optionFileStem = `${fileName$3(optionsName)}.interface`;
5767
5799
  const filePath = `src/${serviceDir}/interfaces/${optionFileStem}.ts`;
@@ -5804,7 +5836,8 @@ function generateOptionsInterfaces(service, ctx, specEnumNames) {
5804
5836
  }
5805
5837
  headerParts.push(` ${name}${opt}: ${type};`);
5806
5838
  };
5807
- for (const param of op.pathParams) pushField(fieldName$6(param.name), true, mapParamType(param.type, specEnumNames), param.description, param.deprecated);
5839
+ const optionsPathFieldMap = operationOverrideFor$1(ctx, op)?.pathFieldMap;
5840
+ for (const param of op.pathParams) pushField(optionsPathFieldMap?.[fieldName$6(param.name)] ?? fieldName$6(param.name), true, mapParamType(param.type, specEnumNames), param.description, param.deprecated);
5808
5841
  for (const param of visibleQueryParamsForOptions(op, plan, resolvedOp)) pushField(fieldName$6(param.name), param.required, mapParamType(param.type, specEnumNames), param.description, param.deprecated);
5809
5842
  if (bodyInfo?.kind === "model") {
5810
5843
  const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
@@ -5876,7 +5909,7 @@ function generateResourceClass(service, ctx) {
5876
5909
  const resourcePath = `src/${serviceDir}/${fileName$3(resolvedName)}.ts`;
5877
5910
  let plans = service.operations.map((op) => ({
5878
5911
  op,
5879
- plan: planOperation(op),
5912
+ plan: planOperationFor(op, ctx),
5880
5913
  method: resolveMethodName$6(op, service, ctx)
5881
5914
  }));
5882
5915
  deduplicateMethodNames(plans, ctx);
@@ -5985,6 +6018,7 @@ function generateResourceClass(service, ctx) {
5985
6018
  const hasCustomEncoding = plans.some((p) => p.op.requestBodyEncoding && p.op.requestBodyEncoding !== "json" && p.plan.hasBody);
5986
6019
  if (hasIdempotentPost || hasCustomEncoding) lines.push("import type { PostOptions } from '../common/interfaces/post-options.interface';");
5987
6020
  const importedTypeNames = /* @__PURE__ */ new Set();
6021
+ if (needsPaginationOptionsImport) importedTypeNames.add("PaginationOptions");
5988
6022
  for (const optionType of optionObjectTypes) {
5989
6023
  if (isValidTypeIdentifier(optionType)) {
5990
6024
  if (importedTypeNames.has(optionType)) continue;
@@ -6543,7 +6577,14 @@ function renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelM
6543
6577
  lines.push(" }");
6544
6578
  return true;
6545
6579
  }
6546
- return false;
6580
+ {
6581
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
6582
+ renderOptionsObjectDestructure(lines, pathBindings);
6583
+ const emptyBodyArg = httpMethodNeedsBody(op.httpMethod) ? ", {}" : "";
6584
+ lines.push(` await this.workos.${op.httpMethod}(${pathStr}${emptyBodyArg}${queryOptionsArg});`);
6585
+ lines.push(" }");
6586
+ return true;
6587
+ }
6547
6588
  }
6548
6589
  function renderOptionsObjectDestructure(lines, pathBindings, restName) {
6549
6590
  if (pathBindings.length > 0 && restName) lines.push(` const { ${pathBindings.join(", ")}, ...${restName} } = options;`);
@@ -6563,7 +6604,8 @@ function bodyArgExprWithParam(bodyExpr, paramName) {
6563
6604
  return paramName === "payload" ? bodyExpr : bodyExpr.replace(/\bpayload\b/g, paramName);
6564
6605
  }
6565
6606
  function buildOptionsObjectPathBindings(op, optionType, ctx) {
6566
- return op.pathParams.map((param) => resolveOptionsObjectField$1(fieldName$6(param.name), optionType, ctx));
6607
+ const pathFieldMap = operationOverrideFor$1(ctx, op)?.pathFieldMap;
6608
+ return op.pathParams.map((param) => resolveOptionsObjectField$1(fieldName$6(param.name), optionType, ctx, pathFieldMap));
6567
6609
  }
6568
6610
  /**
6569
6611
  * Map spec path-param names (e.g. `omId`) to the SDK field name exposed on
@@ -6574,14 +6616,17 @@ function buildOptionsObjectPathBindings(op, optionType, ctx) {
6574
6616
  */
6575
6617
  function buildOptionsObjectPathParamMap(op, optionType, ctx) {
6576
6618
  const map = /* @__PURE__ */ new Map();
6619
+ const pathFieldMap = operationOverrideFor$1(ctx, op)?.pathFieldMap;
6577
6620
  for (const param of op.pathParams) {
6578
6621
  const localName = fieldName$6(param.name);
6579
- const sdkField = resolveOptionsObjectField$1(localName, optionType, ctx);
6622
+ const sdkField = resolveOptionsObjectField$1(localName, optionType, ctx, pathFieldMap);
6580
6623
  if (sdkField !== localName) map.set(param.name, sdkField);
6581
6624
  }
6582
6625
  return map;
6583
6626
  }
6584
- function resolveOptionsObjectField$1(localName, optionType, ctx) {
6627
+ function resolveOptionsObjectField$1(localName, optionType, ctx, pathFieldMap) {
6628
+ const mapped = pathFieldMap?.[localName];
6629
+ if (mapped) return mapped;
6585
6630
  const fields = ctx.apiSurface?.interfaces?.[optionType]?.fields;
6586
6631
  if (!fields) return localName;
6587
6632
  if (fields[localName]) return localName;
@@ -7537,7 +7582,7 @@ function generateModels$7(models, ctx, shared) {
7537
7582
  else {
7538
7583
  lines.push(`export interface ${domainName}${typeParams} {`);
7539
7584
  for (const field of model.fields) {
7540
- const domainFieldName = fieldName$6(field.name);
7585
+ const domainFieldName = fieldName$6(field.domainName ?? field.name);
7541
7586
  if (seenDomainFields.has(domainFieldName)) continue;
7542
7587
  seenDomainFields.add(domainFieldName);
7543
7588
  if (field.description || field.deprecated || field.readOnly || field.writeOnly || field.default !== void 0) {
@@ -7554,7 +7599,10 @@ function generateModels$7(models, ctx, shared) {
7554
7599
  const responseBaselineField = baselineResponse?.fields?.[domainWireField];
7555
7600
  const domainResponseOptionalMismatch = baselineField && !baselineField.optional && responseBaselineField && responseBaselineField.optional;
7556
7601
  const readonlyPrefix = field.readOnly ? "readonly " : "";
7557
- if (baselineField && !domainResponseOptionalMismatch && !hasDateTimeConversion(field.type) && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
7602
+ if (genericDefaults.has(model.name) && baselineField && typeReferencesUnresolvable(baselineField.type, unresolvableNames)) {
7603
+ const opt = baselineField.optional ? "?" : "";
7604
+ lines.push(` ${readonlyPrefix}${domainFieldName}${opt}: ${substituteGenericParam(baselineField.type, unresolvableNames)};`);
7605
+ } else if (baselineField && !domainResponseOptionalMismatch && !hasDateTimeConversion(field.type) && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
7558
7606
  const opt = baselineField.optional ? "?" : "";
7559
7607
  lines.push(` ${readonlyPrefix}${domainFieldName}${opt}: ${baselineField.type};`);
7560
7608
  } else {
@@ -7577,7 +7625,10 @@ function generateModels$7(models, ctx, shared) {
7577
7625
  if (seenWireFields.has(wireField)) continue;
7578
7626
  seenWireFields.add(wireField);
7579
7627
  const baselineField = baselineResponse?.fields?.[wireField];
7580
- if (baselineField && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
7628
+ if (genericDefaults.has(model.name) && baselineField && typeReferencesUnresolvable(baselineField.type, unresolvableNames)) {
7629
+ const opt = baselineField.optional ? "?" : "";
7630
+ lines.push(` ${wireField}${opt}: ${substituteGenericParam(baselineField.type, unresolvableNames)};`);
7631
+ } else if (baselineField && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
7581
7632
  const opt = baselineField.optional ? "?" : "";
7582
7633
  lines.push(` ${wireField}${opt}: ${baselineField.type};`);
7583
7634
  } else {
@@ -8020,6 +8071,19 @@ function hasSpecificIRType(ref) {
8020
8071
  default: return false;
8021
8072
  }
8022
8073
  }
8074
+ /** True when a baseline field type references one of the model's generic
8075
+ * param identifiers (collected as `unresolvableNames` — bare type names that
8076
+ * resolve to nothing importable, which for a generic model are its params). */
8077
+ function typeReferencesUnresolvable(type, unresolvable) {
8078
+ if (unresolvable.size === 0) return false;
8079
+ const names = type.match(/\b[A-Z][a-zA-Z0-9]*\b/g);
8080
+ return !!names && names.some((n) => unresolvable.has(n));
8081
+ }
8082
+ /** Rewrite a baseline field type so references to the model's (renamed)
8083
+ * generic param resolve to the emitted `GenericType` param. */
8084
+ function substituteGenericParam(type, unresolvable) {
8085
+ return type.replace(/\b[A-Z][a-zA-Z0-9]*\b/g, (name) => unresolvable.has(name) ? "GenericType" : name);
8086
+ }
8023
8087
  function renderTypeParams(model, genericDefaults) {
8024
8088
  if (!model.typeParams?.length) {
8025
8089
  if (genericDefaults?.has(model.name)) return "<GenericType extends Record<string, unknown> = Record<string, unknown>>";
@@ -8546,10 +8610,11 @@ function optionsObjectParam(method) {
8546
8610
  const [param] = method.params;
8547
8611
  if (param.name !== "options") return void 0;
8548
8612
  if (param.passingStyle && param.passingStyle !== "options_object") return void 0;
8549
- if (!param.type || /^(Record|object|any|unknown)\b/.test(param.type)) return void 0;
8613
+ const type = param.type?.replace(/(?:\s*\|\s*(?:undefined|null))+\s*$/, "").trim();
8614
+ if (!type || /^(Record|object|any|unknown)\b/.test(type)) return void 0;
8550
8615
  return {
8551
8616
  name: param.name,
8552
- type: param.type
8617
+ type
8553
8618
  };
8554
8619
  }
8555
8620
  function configuredOptionsMethod(ctx, op) {
@@ -8614,6 +8679,7 @@ function generateTests$7(spec, ctx) {
8614
8679
  };
8615
8680
  const ops = isNodeOwnedService(ctx, mountName, resolveResourceClassName$3(mergedService, ctx)) ? operations : uncoveredOperations(mergedService, ctx);
8616
8681
  if (ops.length === 0) continue;
8682
+ const ignoredMethodNames = ignoredResourceMethodNames(ctx, `src/${resolveResourceDir(mergedService, ctx)}/${fileName$3(resolveResourceClassName$3(mergedService, ctx))}.ts`);
8617
8683
  const propName = mountAccessors.get(mountName) ?? servicePropertyName$4(mountName);
8618
8684
  if (ctx.apiSurface && baselineWorkOSProps.size > 0 && !baselineWorkOSProps.has(propName)) continue;
8619
8685
  if (resolveResourceClassName$3(mergedService, ctx) !== mountName) continue;
@@ -8621,23 +8687,31 @@ function generateTests$7(spec, ctx) {
8621
8687
  ...mergedService,
8622
8688
  operations: ops
8623
8689
  } : mergedService;
8624
- files.push(generateServiceTest$3(testService, spec, ctx, modelMap, mountAccessors));
8690
+ files.push(generateServiceTest$3(testService, spec, ctx, modelMap, mountAccessors, ignoredMethodNames));
8625
8691
  }
8626
8692
  const serializerTests = generateSerializerTests(spec, ctx);
8627
8693
  for (const f of serializerTests) files.push(f);
8628
8694
  return files;
8629
8695
  }
8630
- function generateServiceTest$3(service, spec, ctx, modelMap, mountAccessors) {
8696
+ function generateServiceTest$3(service, spec, ctx, modelMap, mountAccessors, ignoredMethodNames = /* @__PURE__ */ new Set()) {
8631
8697
  const resolvedName = resolveResourceClassName$3(service, ctx);
8632
8698
  const serviceDir = resolveResourceDir(service, ctx);
8633
8699
  const serviceClass = resolvedName;
8634
8700
  const serviceProp = mountAccessors?.get(service.name) ?? servicePropertyName$4(resolveServiceName(service, ctx));
8635
8701
  const testPath = `src/${serviceDir}/${fileName$3(resolvedName)}.spec.ts`;
8636
- const plans = service.operations.map((op) => ({
8702
+ const allPlans = service.operations.map((op) => ({
8637
8703
  op,
8638
- plan: planOperation(op),
8704
+ plan: planOperationFor(op, ctx),
8639
8705
  method: resolveMethodName$6(op, service, ctx)
8640
8706
  }));
8707
+ const resolvedLookup = buildResolvedLookup(ctx);
8708
+ const preservedScopes = methodsWithPreservedTestBlocks(ctx, testPath);
8709
+ const plans = allPlans.filter((p) => {
8710
+ if (preservedScopes.has(p.method)) return true;
8711
+ if (ignoredMethodNames.has(p.method)) return false;
8712
+ if (lookupResolved(p.op, resolvedLookup)?.urlBuilder) return false;
8713
+ return true;
8714
+ });
8641
8715
  if (ctx.overlayLookup?.methodByOperation) {
8642
8716
  const methodOrder = /* @__PURE__ */ new Map();
8643
8717
  let pos = 0;
@@ -8684,7 +8758,7 @@ function generateServiceTest$3(service, spec, ctx, modelMap, mountAccessors) {
8684
8758
  lines.push("");
8685
8759
  lines.push("const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');");
8686
8760
  lines.push("");
8687
- const { lines: helperLines, helpers: entityHelperNames } = generateEntityHelpers(plans, modelMap, ctx);
8761
+ const { lines: helperLines, helpers: entityHelperNames } = generateEntityHelpers(service, allPlans, plans, existingTestIgnoreText(ctx, testPath), modelMap, ctx);
8688
8762
  for (const line of helperLines) lines.push(line);
8689
8763
  lines.push(`describe('${serviceClass}', () => {`);
8690
8764
  lines.push(" beforeEach(() => fetch.resetMocks());");
@@ -8700,6 +8774,20 @@ function generateServiceTest$3(service, spec, ctx, modelMap, mountAccessors) {
8700
8774
  lines.push(" });");
8701
8775
  }
8702
8776
  lines.push("});");
8777
+ const body = lines.join("\n");
8778
+ const enumImportLines = [];
8779
+ for (const [name, info] of Object.entries(ctx.apiSurface?.enums ?? {})) {
8780
+ if (!new RegExp(`\\b${name}\\.[A-Za-z_$]`).test(body)) continue;
8781
+ if (new RegExp(`import\\b[^;]*\\b${name}\\b[^;]*from`).test(body)) continue;
8782
+ const sourceFile = info.sourceFile;
8783
+ const spec = sourceFile ? relativeImport(testPath, sourceFile).replace(/\.ts$/, "") : "./interfaces";
8784
+ enumImportLines.push(`import { ${name} } from '${spec}';`);
8785
+ }
8786
+ if (enumImportLines.length > 0) {
8787
+ const anchor = lines.indexOf("import { WorkOS } from '../workos';");
8788
+ const at = anchor >= 0 ? anchor + 1 : 0;
8789
+ lines.splice(at, 0, ...enumImportLines);
8790
+ }
8703
8791
  return {
8704
8792
  path: testPath,
8705
8793
  content: lines.join("\n"),
@@ -8718,11 +8806,81 @@ function pathParamTestValue(param, paramName) {
8718
8806
  if (name) return `test_${fieldName$6(name)}`;
8719
8807
  return "test_id";
8720
8808
  }
8721
- function queryParamTestValue(param, modelMap) {
8809
+ /** Render an example value as a valid TS literal expression.
8810
+ * Objects and nested arrays go through JSON.stringify so map-typed params
8811
+ * (e.g. providerQueryParams) don't coerce to the literal text `[object Object]`.
8812
+ */
8813
+ function renderExampleLiteral(value) {
8814
+ if (typeof value === "string") return `'${value}'`;
8815
+ if (Array.isArray(value)) return `[${value.map(renderExampleLiteral).join(", ")}]`;
8816
+ if (value !== null && typeof value === "object") return JSON.stringify(value);
8817
+ return String(value);
8818
+ }
8819
+ /** Resolve the enum members a query-param test value must satisfy. An enum is
8820
+ * expressed several ways: an inline `EnumRef` with `values`, an `enum`/`model`
8821
+ * ref by name, or — when the IR flattened the param to a bare string but the
8822
+ * hand-written options interface still types the field as an enum — the
8823
+ * baseline options field type (e.g. `ConnectionType`). */
8824
+ /** All valid values for an enum named `name`. The extracted SDK surface is
8825
+ * consulted first: when an options field is typed with a hand-written enum
8826
+ * (e.g. `ConnectionType` from `connection-type.enum.ts`), that enum — not a
8827
+ * same-named IR enum that may carry extra members like `Pending` — is what
8828
+ * the generated test must type-check against. Falls back to the IR spec. */
8829
+ function enumValuesByName(name, ctx) {
8830
+ const members = ctx?.apiSurface?.enums?.[name]?.members;
8831
+ if (members && Object.keys(members).length > 0) return Object.values(members);
8832
+ const specValues = ctx?.spec.enums.find((e) => e.name === name)?.values;
8833
+ if (specValues?.length) return specValues.map((v) => v.value);
8834
+ }
8835
+ function resolveParamEnumValues(param, optionFieldType, ctx) {
8836
+ if (optionFieldType) {
8837
+ const bare = optionFieldType.replace(/\[\]/g, "").replace(/\|\s*(undefined|null)/g, "").trim();
8838
+ if (/^[A-Za-z_$][\w$]*$/.test(bare)) {
8839
+ const values = enumValuesByName(bare, ctx);
8840
+ if (values) return values;
8841
+ }
8842
+ }
8843
+ if ((param.type.kind === "enum" || param.type.kind === "model") && param.type.name) {
8844
+ const values = enumValuesByName(param.type.name, ctx);
8845
+ if (values) return values;
8846
+ }
8847
+ if (param.type.kind === "enum" && param.type.values?.length) return param.type.values;
8848
+ }
8849
+ /** When an options field is typed with a real (nominal) TS `enum` from the SDK
8850
+ * surface, a string literal won't type-check — the value must be a member
8851
+ * reference (`ConnectionType.ADFSSAML`). Returns the enum's name + members so
8852
+ * the caller can both render the reference and import the right declaration. */
8853
+ function resolveRealEnum(param, optionFieldType, ctx) {
8854
+ const candidates = [];
8855
+ if (optionFieldType) {
8856
+ const bare = optionFieldType.replace(/\[\]/g, "").replace(/\|\s*(undefined|null)/g, "").trim();
8857
+ if (/^[A-Za-z_$][\w$]*$/.test(bare)) candidates.push(bare);
8858
+ }
8859
+ if ((param.type.kind === "enum" || param.type.kind === "model") && param.type.name) candidates.push(param.type.name);
8860
+ for (const name of candidates) {
8861
+ const members = ctx?.apiSurface?.enums?.[name]?.members;
8862
+ if (members && Object.keys(members).length > 0) return {
8863
+ name,
8864
+ members
8865
+ };
8866
+ }
8867
+ return null;
8868
+ }
8869
+ function queryParamTestValue(param, modelMap, ctx, optionFieldType) {
8870
+ const realEnum = resolveRealEnum(param, optionFieldType, ctx);
8871
+ if (realEnum) {
8872
+ const entries = Object.entries(realEnum.members);
8873
+ const [memberKey] = (typeof param.example === "string" ? entries.find(([, v]) => v === param.example) : void 0) ?? entries[0];
8874
+ return `${realEnum.name}.${memberKey}`;
8875
+ }
8876
+ const enumValues = resolveParamEnumValues(param, optionFieldType, ctx);
8877
+ if (enumValues?.length) {
8878
+ const valid = typeof param.example === "string" && enumValues.includes(param.example) ? param.example : enumValues[0];
8879
+ return typeof valid === "string" ? `'${valid}'` : String(valid);
8880
+ }
8722
8881
  if (param.example !== void 0) {
8723
- if (Array.isArray(param.example)) return `[${param.example.map((v) => typeof v === "string" ? `'${v}'` : String(v)).join(", ")}]`;
8724
8882
  if (param.type.kind === "primitive" && param.type.format === "date-time" && typeof param.example === "string") return `new Date('${param.example}')`;
8725
- return typeof param.example === "string" ? `'${param.example}'` : String(param.example);
8883
+ return renderExampleLiteral(param.example);
8726
8884
  }
8727
8885
  return fixtureValueForType(param.type, param.name, "Options", modelMap) ?? "'test'";
8728
8886
  }
@@ -8888,9 +9046,10 @@ function buildOptionsObjectTestArg(op, plan, baselineMethod, modelMap, ctx) {
8888
9046
  const optionParam = optionsObjectParam(baselineMethod);
8889
9047
  if (!optionParam) return null;
8890
9048
  const entries = [];
9049
+ const pathFieldMap = ctx ? operationOverrideFor(ctx, op)?.pathFieldMap : void 0;
8891
9050
  for (const param of op.pathParams) {
8892
9051
  const localName = fieldName$6(param.name);
8893
- const optionField = resolveOptionsObjectField(localName, optionParam.type, ctx);
9052
+ const optionField = resolveOptionsObjectField(localName, optionParam.type, ctx, pathFieldMap);
8894
9053
  entries.push(`${optionField}: ${JSON.stringify(pathParamTestValue(param, localName))}`);
8895
9054
  }
8896
9055
  if (plan.isPaginated) entries.push("order: 'desc'");
@@ -8902,7 +9061,8 @@ function buildOptionsObjectTestArg(op, plan, baselineMethod, modelMap, ctx) {
8902
9061
  ].includes(param.name)) : op.queryParams).filter((param) => !param.deprecated);
8903
9062
  for (const param of queryParams) {
8904
9063
  const localName = fieldName$6(param.name);
8905
- const value = queryParamTestValue(param, modelMap);
9064
+ const optionFieldType = ctx?.apiSurface?.interfaces?.[optionParam.type]?.fields?.[localName]?.type;
9065
+ const value = queryParamTestValue(param, modelMap, ctx, optionFieldType);
8906
9066
  entries.push(`${localName}: ${value}`);
8907
9067
  }
8908
9068
  if (plan.hasBody) {
@@ -8930,7 +9090,9 @@ function objectLiteralEntries(literal) {
8930
9090
  const body = trimmed.slice(1, -1).trim();
8931
9091
  return body ? body.split(",").map((entry) => entry.trim()) : [];
8932
9092
  }
8933
- function resolveOptionsObjectField(localName, optionType, ctx) {
9093
+ function resolveOptionsObjectField(localName, optionType, ctx, pathFieldMap) {
9094
+ const mapped = pathFieldMap?.[localName];
9095
+ if (mapped) return mapped;
8934
9096
  const fields = ctx?.apiSurface?.interfaces?.[optionType]?.fields;
8935
9097
  if (!fields) return localName;
8936
9098
  if (fields[localName]) return localName;
@@ -8945,20 +9107,85 @@ function resolveOptionsObjectField(localName, optionType, ctx) {
8945
9107
  * Generate per-entity assertion helper functions for models used in 2+ tests.
8946
9108
  * Returns { lines, helpers } where helpers is a Set of helper function names.
8947
9109
  */
8948
- function generateEntityHelpers(plans, modelMap, ctx) {
8949
- const modelUsage = /* @__PURE__ */ new Map();
8950
- for (const { op, plan } of plans) {
8951
- let modelName = null;
9110
+ /** Describe-scope names (i.e. method names) that have an `@oagen-ignore` block
9111
+ * nested inside them in the existing test file. Such a `describe` must keep
9112
+ * being emitted even when the method is hand-owned — otherwise the engine has
9113
+ * no `describe` to re-nest the preserved block under and orphans it to the top
9114
+ * of the file (out of `beforeEach` scope), which hangs at runtime. Mirrors the
9115
+ * engine's `findContainingDescribeScope`. */
9116
+ function methodsWithPreservedTestBlocks(ctx, relPath) {
9117
+ const scopes = /* @__PURE__ */ new Set();
9118
+ const root = ctx.outputDir ?? ctx.targetDir;
9119
+ if (!root) return scopes;
9120
+ let content;
9121
+ try {
9122
+ content = fs.readFileSync(path.join(root, relPath), "utf8");
9123
+ } catch {
9124
+ return scopes;
9125
+ }
9126
+ const lines = content.split("\n");
9127
+ const indentOf = (l) => l.length - l.trimStart().length;
9128
+ for (let i = 0; i < lines.length; i++) {
9129
+ if (!lines[i].includes("@oagen-ignore-start")) continue;
9130
+ if (/^\S/.test(lines[i])) continue;
9131
+ const markerIndent = indentOf(lines[i]);
9132
+ for (let j = i - 1; j >= 0; j--) {
9133
+ if (indentOf(lines[j]) >= markerIndent) continue;
9134
+ const m = lines[j].match(/^\s*describe\((['"`])(.+?)\1\s*,/);
9135
+ if (m) {
9136
+ scopes.add(m[2]);
9137
+ break;
9138
+ }
9139
+ if (/^\s*(?:export\s+)?class\s+\w+/.test(lines[j])) break;
9140
+ }
9141
+ }
9142
+ return scopes;
9143
+ }
9144
+ /** Concatenated text of the existing test file's `@oagen-ignore` regions, so
9145
+ * helper generation can see which `expect<Model>()` helpers preserved
9146
+ * hand-written test blocks still reference. */
9147
+ function existingTestIgnoreText(ctx, relPath) {
9148
+ const root = ctx.outputDir ?? ctx.targetDir;
9149
+ if (!root) return "";
9150
+ let content;
9151
+ try {
9152
+ content = fs.readFileSync(path.join(root, relPath), "utf8");
9153
+ } catch {
9154
+ return "";
9155
+ }
9156
+ return [...content.matchAll(/@oagen-ignore-start[\s\S]*?@oagen-ignore-end/g)].map((m) => m[0]).join("\n");
9157
+ }
9158
+ function generateEntityHelpers(service, allPlans, renderedPlans, ignoreRegionText, modelMap, ctx) {
9159
+ const responseModelOf = (entry) => {
9160
+ const { op, plan } = entry;
8952
9161
  if (plan.isPaginated && op.pagination?.itemType.kind === "model") {
8953
- modelName = op.pagination.itemType.name;
9162
+ let modelName = op.pagination.itemType.name;
8954
9163
  const rawModel = modelMap.get(modelName);
8955
9164
  if (rawModel) {
8956
9165
  const unwrapped = unwrapListModel$4(rawModel, modelMap);
8957
9166
  if (unwrapped) modelName = unwrapped.name;
8958
9167
  }
8959
- } else if (plan.responseModelName) modelName = plan.responseModelName;
9168
+ return modelName;
9169
+ }
9170
+ return plan.responseModelName ?? null;
9171
+ };
9172
+ const modelUsage = /* @__PURE__ */ new Map();
9173
+ for (const entry of allPlans) {
9174
+ const modelName = responseModelOf(entry);
8960
9175
  if (modelName) modelUsage.set(modelName, (modelUsage.get(modelName) ?? 0) + 1);
8961
9176
  }
9177
+ const renderedModels = /* @__PURE__ */ new Set();
9178
+ for (const entry of renderedPlans) {
9179
+ const modelName = responseModelOf(entry);
9180
+ if (!modelName) continue;
9181
+ if (entry.plan.isPaginated && entry.op.pagination?.itemType.kind === "model") {
9182
+ const baselineMethod = optionsMethodFor(service, entry.method, entry.op, entry.plan, ctx);
9183
+ const baselineItemType = autoPaginatableItemType(baselineMethod?.returnType);
9184
+ const generatedItemType = resolveInterfaceName(modelName, ctx);
9185
+ if (Boolean(baselineMethod?.returnType && !baselineItemType) || Boolean(baselineItemType && generatedItemType && baselineItemType !== generatedItemType)) continue;
9186
+ }
9187
+ renderedModels.add(modelName);
9188
+ }
8962
9189
  const lines = [];
8963
9190
  const helpers = /* @__PURE__ */ new Set();
8964
9191
  for (const [modelName, count] of modelUsage) {
@@ -8969,6 +9196,7 @@ function generateEntityHelpers(plans, modelMap, ctx) {
8969
9196
  if (assertions.length === 0) continue;
8970
9197
  const helperName = `expect${resolveInterfaceName(modelName, ctx)}`;
8971
9198
  if (helpers.has(helperName)) continue;
9199
+ if (!(renderedModels.has(modelName) || ignoreRegionText.includes(`${helperName}(`))) continue;
8972
9200
  helpers.add(helperName);
8973
9201
  lines.push(`function ${helperName}(result: any) {`);
8974
9202
  for (const assertion of assertions) lines.push(` ${assertion}`);
@@ -8998,7 +9226,7 @@ function buildFieldAssertions(model, accessor, modelMap) {
8998
9226
  const assertions = [];
8999
9227
  for (const field of model.fields) {
9000
9228
  if (!field.required) continue;
9001
- const domainField = fieldName$6(field.name);
9229
+ const domainField = fieldName$6(field.domainName ?? field.name);
9002
9230
  const isDateTime = isDateTimeFieldType(field.type);
9003
9231
  const fieldAccessor = isDateTime ? `${accessor}.${domainField}.toISOString()` : `${accessor}.${domainField}`;
9004
9232
  if (field.example !== void 0) {
@@ -10064,6 +10292,21 @@ function withNodeOperationOverrides(ctx) {
10064
10292
  return next;
10065
10293
  }
10066
10294
  //#endregion
10295
+ //#region src/shared/file-header.ts
10296
+ /**
10297
+ * Canonical "do not edit" banner text shared by every emitter's
10298
+ * `fileHeader()`. The text is comment-syntax agnostic — each emitter prefixes
10299
+ * it with the appropriate comment marker for its language (`//`, `#`, etc.).
10300
+ *
10301
+ * Keeping this in one place prevents the wording from drifting between
10302
+ * languages (Rust previously emitted a different banner).
10303
+ *
10304
+ * Go intentionally does NOT use this constant: its header must match the
10305
+ * standard `^// Code generated .* DO NOT EDIT\.$` marker that Go tooling
10306
+ * relies on to recognize generated files. See `src/go/index.ts`.
10307
+ */
10308
+ const AUTOGEN_NOTICE = "This file is auto-generated by oagen. Do not edit.";
10309
+ //#endregion
10067
10310
  //#region src/node/index.ts
10068
10311
  /**
10069
10312
  * Cache live-surface per ctx — every emitter method receives the same ctx in
@@ -10609,7 +10852,7 @@ const nodeEmitter = {
10609
10852
  return {};
10610
10853
  },
10611
10854
  fileHeader() {
10612
- return "// This file is auto-generated by oagen. Do not edit.";
10855
+ return `// ${AUTOGEN_NOTICE}`;
10613
10856
  },
10614
10857
  formatCommand(targetDir) {
10615
10858
  const hasPrettier = fs$1.existsSync(path$1.join(targetDir, ".prettierrc"));
@@ -10677,6 +10920,15 @@ function fieldName$5(name) {
10677
10920
  return toSnakeCase(name);
10678
10921
  }
10679
10922
  /**
10923
+ * snake_case domain field name for a model field, honoring a `domainName`
10924
+ * override (set via the `fieldHints` config) so a wire field can surface under
10925
+ * a friendlier name. The wire name (still derived from `field.name`) is
10926
+ * unaffected, so the API contract is preserved.
10927
+ */
10928
+ function domainFieldName$5(field) {
10929
+ return toSnakeCase(field.domainName ?? field.name);
10930
+ }
10931
+ /**
10680
10932
  * Python builtins that should not be shadowed by parameter names.
10681
10933
  * When a path/query param name collides, suffix with underscore.
10682
10934
  */
@@ -11574,7 +11826,7 @@ function generateModels$6(models, ctx) {
11574
11826
  }
11575
11827
  const seenFieldNames = /* @__PURE__ */ new Set();
11576
11828
  const deduplicatedFields = model.fields.filter((f) => {
11577
- const pyName = fieldName$5(f.name);
11829
+ const pyName = domainFieldName$5(f);
11578
11830
  if (seenFieldNames.has(pyName)) return false;
11579
11831
  seenFieldNames.add(pyName);
11580
11832
  return true;
@@ -11636,7 +11888,7 @@ function generateModels$6(models, ctx) {
11636
11888
  return typeStr;
11637
11889
  };
11638
11890
  for (const field of requiredFields) {
11639
- const pyFieldName = fieldName$5(field.name);
11891
+ const pyFieldName = domainFieldName$5(field);
11640
11892
  const pyType = rewriteDiscriminatorType(resolveModelFieldType(field.type));
11641
11893
  if (field.description || field.deprecated) {
11642
11894
  const parts = [];
@@ -11647,7 +11899,7 @@ function generateModels$6(models, ctx) {
11647
11899
  } else lines.push(` ${pyFieldName}: ${pyType}`);
11648
11900
  }
11649
11901
  for (const field of optionalFields) {
11650
- const pyFieldName = fieldName$5(field.name);
11902
+ const pyFieldName = domainFieldName$5(field);
11651
11903
  const pyType = `Optional[${rewriteDiscriminatorType(field.type.kind === "nullable" ? resolveModelFieldType(field.type.inner) : resolveModelFieldType(field.type))}]`;
11652
11904
  if (field.description || field.deprecated) {
11653
11905
  const parts = [];
@@ -11665,7 +11917,7 @@ function generateModels$6(models, ctx) {
11665
11917
  const preludeLines = [];
11666
11918
  const fieldAssignmentLines = [];
11667
11919
  for (const field of [...requiredFields, ...optionalFields]) {
11668
- const pyFieldName = fieldName$5(field.name);
11920
+ const pyFieldName = domainFieldName$5(field);
11669
11921
  const wireKey = field.name;
11670
11922
  const isRequired = !isOptionalField(model.name, field, ctx);
11671
11923
  const discPrelude = renderDiscriminatedUnionPrelude(field, pyFieldName, wireKey, modelClassName, isRequired);
@@ -11693,7 +11945,7 @@ function generateModels$6(models, ctx) {
11693
11945
  lines.push(" \"\"\"Serialize to a dictionary.\"\"\"");
11694
11946
  lines.push(" result: Dict[str, Any] = {}");
11695
11947
  for (const field of [...requiredFields, ...optionalFields]) {
11696
- const pyFieldName = fieldName$5(field.name);
11948
+ const pyFieldName = domainFieldName$5(field);
11697
11949
  const wireKey = field.name;
11698
11950
  const isRequired = !isOptionalField(model.name, field, ctx);
11699
11951
  const isNullable = field.type.kind === "nullable";
@@ -13569,7 +13821,7 @@ function generateModelFixture$4(model, modelMap, enumMap) {
13569
13821
  const fixture = {};
13570
13822
  const seenFieldNames = /* @__PURE__ */ new Set();
13571
13823
  const deduplicatedFields = model.fields.filter((f) => {
13572
- const pyName = fieldName$5(f.name);
13824
+ const pyName = domainFieldName$5(f);
13573
13825
  if (seenFieldNames.has(pyName)) return false;
13574
13826
  seenFieldNames.add(pyName);
13575
13827
  return true;
@@ -14365,16 +14617,16 @@ function pickAssertableFields(modelName, spec, maxFields = 2) {
14365
14617
  if (typeof val === "string") {
14366
14618
  if (val.includes("\"") || val.includes("'") || val.includes("{") || val.includes("\\") || val.includes("\n")) continue;
14367
14619
  results.push({
14368
- field: fieldName$5(f.name),
14620
+ field: domainFieldName$5(f),
14369
14621
  value: `"${val}"`
14370
14622
  });
14371
14623
  } else if (typeof val === "boolean") results.push({
14372
- field: fieldName$5(f.name),
14624
+ field: domainFieldName$5(f),
14373
14625
  value: val ? "True" : "False",
14374
14626
  isBool: true
14375
14627
  });
14376
14628
  else if (typeof val === "number") results.push({
14377
- field: fieldName$5(f.name),
14629
+ field: domainFieldName$5(f),
14378
14630
  value: String(val)
14379
14631
  });
14380
14632
  }
@@ -14434,7 +14686,7 @@ function buildTestArgs$1(op, spec, hiddenParams) {
14434
14686
  ].includes(param.name)) continue;
14435
14687
  if (plan.hasBody && op.requestBody?.kind === "model") {
14436
14688
  const rbName = op.requestBody.name;
14437
- if (spec.models.find((m) => m.name === rbName)?.fields.some((f) => fieldName$5(f.name) === fieldName$5(param.name))) continue;
14689
+ if (spec.models.find((m) => m.name === rbName)?.fields.some((f) => domainFieldName$5(f) === fieldName$5(param.name))) continue;
14438
14690
  }
14439
14691
  if (param.required && !pathParamNames.has(fieldName$5(param.name))) args.push(`${fieldName$5(param.name)}=${generateTestValue$1(param.type, param.name)}`);
14440
14692
  }
@@ -14693,7 +14945,7 @@ function generateModelRoundTripTests(spec, ctx) {
14693
14945
  if (model.discriminator) continue;
14694
14946
  const seenFieldNames = /* @__PURE__ */ new Set();
14695
14947
  const dedupFields = model.fields.filter((f) => {
14696
- const pyName = fieldName$5(f.name);
14948
+ const pyName = domainFieldName$5(f);
14697
14949
  if (seenFieldNames.has(pyName)) return false;
14698
14950
  seenFieldNames.add(pyName);
14699
14951
  return true;
@@ -14885,7 +15137,7 @@ const pythonEmitter = {
14885
15137
  return buildOperationsMap$6(spec, ctx);
14886
15138
  },
14887
15139
  fileHeader() {
14888
- return "# This file is auto-generated by oagen. Do not edit.";
15140
+ return `# ${AUTOGEN_NOTICE}`;
14889
15141
  },
14890
15142
  formatCommand(_targetDir) {
14891
15143
  return {
@@ -14986,6 +15238,15 @@ function resolveMethodName$4(op, _service, ctx) {
14986
15238
  function fieldName$4(name) {
14987
15239
  return toCamelCase(name);
14988
15240
  }
15241
+ /**
15242
+ * camelCase DOMAIN property name for a model field, honoring a `domainName`
15243
+ * override (set via the `fieldHints` config) so a wire field can surface under
15244
+ * a friendlier PHP property name. The wire key (see {@link wireName}) still
15245
+ * derives from `field.name`. No-op when `domainName` is unset.
15246
+ */
15247
+ function domainFieldName$4(field) {
15248
+ return fieldName$4(field.domainName ?? field.name);
15249
+ }
14989
15250
  /** snake_case name for fixtures and other snake_case contexts. */
14990
15251
  function snakeName(name) {
14991
15252
  return toSnakeCase(stripUrnPrefix(name));
@@ -15120,14 +15381,14 @@ function generateModels$5(models, ctx) {
15120
15381
  const optionalFields = model.fields.filter((f) => !f.required);
15121
15382
  const seenNames = /* @__PURE__ */ new Set();
15122
15383
  const allFields = [...requiredFields, ...optionalFields].filter((f) => {
15123
- const phpName = fieldName$4(f.name);
15384
+ const phpName = domainFieldName$4(f);
15124
15385
  if (seenNames.has(phpName)) return false;
15125
15386
  seenNames.add(phpName);
15126
15387
  return true;
15127
15388
  });
15128
15389
  for (let i = 0; i < allFields.length; i++) {
15129
15390
  const field = allFields[i];
15130
- const phpName = fieldName$4(field.name);
15391
+ const phpName = domainFieldName$4(field);
15131
15392
  const phpType = mapTypeRef$5(field.type);
15132
15393
  const isOptional = !field.required;
15133
15394
  const comma = i < allFields.length - 1 ? "," : ",";
@@ -15154,7 +15415,7 @@ function generateModels$5(models, ctx) {
15154
15415
  lines.push(` return new self(`);
15155
15416
  for (let i = 0; i < allFields.length; i++) {
15156
15417
  const field = allFields[i];
15157
- const phpName = fieldName$4(field.name);
15418
+ const phpName = domainFieldName$4(field);
15158
15419
  const wireName = field.name;
15159
15420
  const comma = i < allFields.length - 1 ? "," : ",";
15160
15421
  const accessor = generateFromArrayAccessor(field.type, wireName, field.required);
@@ -15167,7 +15428,7 @@ function generateModels$5(models, ctx) {
15167
15428
  lines.push(" {");
15168
15429
  lines.push(" return [");
15169
15430
  for (const field of allFields) {
15170
- const phpName = fieldName$4(field.name);
15431
+ const phpName = domainFieldName$4(field);
15171
15432
  const wireName = field.name;
15172
15433
  const serialized = generateToArrayValue(field.type, `$this->${phpName}`, !field.required);
15173
15434
  lines.push(` '${wireName}' => ${serialized},`);
@@ -16591,7 +16852,7 @@ function emitFieldHydrationAssertions(lines, modelName, resultVar, fixtureVar, c
16591
16852
  const assertFields = [candidates.find((f) => f.name === "id"), candidates.filter((f) => f.name !== "id")[0]].filter(Boolean);
16592
16853
  for (const f of assertFields) {
16593
16854
  if (!f) continue;
16594
- const phpProp = toCamelCase(f.name);
16855
+ const phpProp = domainFieldName$4(f);
16595
16856
  lines.push(` $this->assertSame(${fixtureVar}['${f.name}'], ${resultVar}->${phpProp});`);
16596
16857
  }
16597
16858
  }
@@ -16771,7 +17032,7 @@ const phpEmitter = {
16771
17032
  return buildOperationsMap$5(spec, ctx);
16772
17033
  },
16773
17034
  fileHeader() {
16774
- return "<?php\n\ndeclare(strict_types=1);\n\n// This file is auto-generated by oagen. Do not edit.";
17035
+ return `<?php\n\ndeclare(strict_types=1);\n\n// ${AUTOGEN_NOTICE}`;
16775
17036
  },
16776
17037
  formatCommand(targetDir) {
16777
17038
  if (fs$1.existsSync(path$1.join(targetDir, ".php-cs-fixer.dist.php")) || fs$1.existsSync(path$1.join(targetDir, ".php-cs-fixer.php"))) return {
@@ -16834,6 +17095,15 @@ function methodName$3(name) {
16834
17095
  function fieldName$3(name) {
16835
17096
  return applyAcronyms(toPascalCase(name));
16836
17097
  }
17098
+ /**
17099
+ * PascalCase domain field name for a model field, honoring a `domainName`
17100
+ * override (set via the `fieldHints` config) so a wire field can surface under
17101
+ * a friendlier name. The wire name (the `json:"..."` struct tag) still derives
17102
+ * from `field.name`.
17103
+ */
17104
+ function domainFieldName$3(field) {
17105
+ return applyAcronyms(toPascalCase(field.domainName ?? field.name));
17106
+ }
16837
17107
  /** Lower-camel identifier with Go acronym conventions preserved. */
16838
17108
  function unexportedName(name) {
16839
17109
  const exported = className$3(name);
@@ -17082,7 +17352,7 @@ function generateModels$4(models, ctx) {
17082
17352
  lines.push(`type ${structName} struct {`);
17083
17353
  const seenFieldNames = /* @__PURE__ */ new Set();
17084
17354
  for (const field of model.fields) {
17085
- const goFieldName = fieldName$3(field.name);
17355
+ const goFieldName = domainFieldName$3(field);
17086
17356
  if (seenFieldNames.has(goFieldName)) continue;
17087
17357
  seenFieldNames.add(goFieldName);
17088
17358
  const goType = !field.required ? makeOptional$2(mapTypeRef$4(field.type)) : mapTypeRef$4(field.type);
@@ -17896,7 +18166,7 @@ function generateParamsStruct(mountName, method, op, plan, ctx, resolvedOp) {
17896
18166
  if (bodyModel) for (const field of bodyModel.fields) {
17897
18167
  if (hidden.has(field.name)) continue;
17898
18168
  if (groupedParams.has(field.name)) continue;
17899
- const goField = fieldName$3(field.name);
18169
+ const goField = domainFieldName$3(field);
17900
18170
  if (emittedFields.has(goField)) continue;
17901
18171
  emittedFields.add(goField);
17902
18172
  const goType = !field.required ? makeOptional$1(mapTypeRef$4(field.type)) : mapTypeRef$4(field.type);
@@ -18234,7 +18504,7 @@ function emitHiddenParamsBodyStruct(lines, method, op, ctx, resolvedOp) {
18234
18504
  if (hidden.has(field.name)) continue;
18235
18505
  if (groupedParamNames.has(field.name)) continue;
18236
18506
  if (!field.required) continue;
18237
- const goField = fieldName$3(field.name);
18507
+ const goField = domainFieldName$3(field);
18238
18508
  const goType = mapTypeRef$4(field.type);
18239
18509
  lines.push(`\t${goField} ${goType} \`json:"${field.name}"\``);
18240
18510
  }
@@ -18246,7 +18516,7 @@ function emitHiddenParamsBodyStruct(lines, method, op, ctx, resolvedOp) {
18246
18516
  if (hidden.has(field.name)) continue;
18247
18517
  if (groupedParamNames.has(field.name)) continue;
18248
18518
  if (field.required) continue;
18249
- const goField = fieldName$3(field.name);
18519
+ const goField = domainFieldName$3(field);
18250
18520
  const goType = makeOptional$1(mapTypeRef$4(field.type));
18251
18521
  lines.push(`\t${goField} ${goType} \`json:"${field.name},omitempty"\``);
18252
18522
  }
@@ -18267,7 +18537,7 @@ function emitBodyWithHiddenParams(lines, op, pathExpr, plan, ctx, resolvedOp, pa
18267
18537
  if (paramsType && bodyModel) for (const field of bodyModel.fields) {
18268
18538
  if (hidden.has(field.name)) continue;
18269
18539
  if (!field.required) continue;
18270
- const goField = fieldName$3(field.name);
18540
+ const goField = domainFieldName$3(field);
18271
18541
  lines.push(`\t\t${goField}: params.${goField},`);
18272
18542
  }
18273
18543
  lines.push(" }");
@@ -18278,7 +18548,7 @@ function emitBodyWithHiddenParams(lines, op, pathExpr, plan, ctx, resolvedOp, pa
18278
18548
  if (paramsType && bodyModel) for (const field of bodyModel.fields) {
18279
18549
  if (hidden.has(field.name)) continue;
18280
18550
  if (field.required) continue;
18281
- const goField = fieldName$3(field.name);
18551
+ const goField = domainFieldName$3(field);
18282
18552
  lines.push(`\tbody.${goField} = params.${goField}`);
18283
18553
  }
18284
18554
  const queryArg = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0 ? "params" : "nil";
@@ -18705,7 +18975,7 @@ function generateModelFixture$2(model, modelMap, enumMap) {
18705
18975
  const fixture = {};
18706
18976
  const seenFieldNames = /* @__PURE__ */ new Set();
18707
18977
  const deduplicatedFields = model.fields.filter((f) => {
18708
- const goName = fieldName$3(f.name);
18978
+ const goName = domainFieldName$3(f);
18709
18979
  if (seenFieldNames.has(goName)) return false;
18710
18980
  seenFieldNames.add(goName);
18711
18981
  return true;
@@ -19445,6 +19715,15 @@ function methodName$2(name) {
19445
19715
  function fieldName$2(name) {
19446
19716
  return toPascalCase(name);
19447
19717
  }
19718
+ /**
19719
+ * PascalCase domain property name for a model field, honoring a `domainName`
19720
+ * override (set via the `fieldHints` config) so a wire field can surface under
19721
+ * a friendlier C# property name. The wire/serialization key (the
19722
+ * `[JsonPropertyName("...")]` value) still derives from `field.name`.
19723
+ */
19724
+ function domainFieldName$2(field) {
19725
+ return toPascalCase(field.domainName ?? field.name);
19726
+ }
19448
19727
  function trimAsyncSuffix(name) {
19449
19728
  return name.endsWith("Async") ? name.slice(0, -5) : name;
19450
19729
  }
@@ -19811,7 +20090,7 @@ function generateModels$3(models, ctx, discCtx) {
19811
20090
  const baseClassName = modelClassName(model.name);
19812
20091
  const fieldMap = /* @__PURE__ */ new Map();
19813
20092
  for (const field of model.fields) {
19814
- let csName = fieldName$2(field.name);
20093
+ let csName = domainFieldName$2(field);
19815
20094
  if (csName === baseClassName) csName = `${csName}Value`;
19816
20095
  fieldMap.set(csName, mapTypeRef$3(field.type));
19817
20096
  }
@@ -19827,7 +20106,9 @@ function generateModels$3(models, ctx, discCtx) {
19827
20106
  const fieldTypes = model.fields.map((f) => mapTypeRef$3(f.type));
19828
20107
  const needsCollections = fieldTypes.some((t) => t.startsWith("List<") || t.startsWith("Dictionary<"));
19829
20108
  const needsSystem = fieldTypes.some((t) => t.includes("DateTimeOffset"));
19830
- const needsJsonAttrs = model.fields.some((f) => fieldName$2(f.name) === csClassName) || model.fields.some((f) => f.required && isEnumRef(f.type));
20109
+ const hasClassNameCollision = model.fields.some((f) => domainFieldName$2(f) === csClassName);
20110
+ const hasDomainRename = model.fields.some((f) => domainFieldName$2(f) !== fieldName$2(f.name));
20111
+ const needsJsonAttrs = hasClassNameCollision || hasDomainRename || model.fields.some((f) => f.required && isEnumRef(f.type));
19831
20112
  lines.push(`namespace ${ctx.namespacePascal}`);
19832
20113
  lines.push("{");
19833
20114
  if (needsSystem) lines.push(" using System;");
@@ -19853,9 +20134,10 @@ function generateModels$3(models, ctx, discCtx) {
19853
20134
  const dictObjectFields = [];
19854
20135
  const seenFieldNames = /* @__PURE__ */ new Set();
19855
20136
  for (const field of model.fields) {
19856
- let csFieldName = fieldName$2(field.name);
20137
+ let csFieldName = domainFieldName$2(field);
19857
20138
  const collidesWithClassName = csFieldName === csClassName;
19858
20139
  if (collidesWithClassName) csFieldName = `${csFieldName}Value`;
20140
+ const hasDomainOverride = domainFieldName$2(field) !== fieldName$2(field.name);
19859
20141
  if (seenFieldNames.has(csFieldName)) continue;
19860
20142
  seenFieldNames.add(csFieldName);
19861
20143
  let useNewModifier = false;
@@ -19902,7 +20184,7 @@ function generateModels$3(models, ctx, discCtx) {
19902
20184
  const isRequiredEnum = field.required && isEnumRef(field.type) && constInit === null;
19903
20185
  lines.push(...emitJsonPropertyAttributes(field.name, {
19904
20186
  isRequiredEnum,
19905
- explicitWireName: collidesWithClassName
20187
+ explicitWireName: collidesWithClassName || hasDomainOverride
19906
20188
  }));
19907
20189
  const discriminatedUnionConverter = discriminatedUnionConverterName(field.type);
19908
20190
  if (discriminatedUnionConverter) lines.push(` [Newtonsoft.Json.JsonConverter(typeof(${discriminatedUnionConverter}))]`);
@@ -21136,9 +21418,10 @@ function generateFixtures$1(spec) {
21136
21418
  const modelMap = new Map(spec.models.map((m) => [m.name, m]));
21137
21419
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
21138
21420
  const files = [];
21421
+ const nonPaginatedWrapperRefs = collectNonPaginatedResponseModelNames(spec.services);
21139
21422
  for (const model of spec.models) {
21140
21423
  if (isListMetadataModel(model)) continue;
21141
- if (isListWrapperModel(model)) continue;
21424
+ if (isListWrapperModel(model) && !nonPaginatedWrapperRefs.has(model.name)) continue;
21142
21425
  const fixture = model.fields.length === 0 ? {} : generateModelFixture$1(model, modelMap, enumMap);
21143
21426
  files.push({
21144
21427
  path: `testdata/${fixtureFileName(model.name)}.json`,
@@ -21207,7 +21490,7 @@ function generateModelFixture$1(model, modelMap, enumMap) {
21207
21490
  const fixture = {};
21208
21491
  const seenFieldNames = /* @__PURE__ */ new Set();
21209
21492
  const deduplicatedFields = model.fields.filter((f) => {
21210
- const csName = fieldName$2(f.name);
21493
+ const csName = domainFieldName$2(f);
21211
21494
  if (seenFieldNames.has(csName)) return false;
21212
21495
  seenFieldNames.add(csName);
21213
21496
  return true;
@@ -21728,7 +22011,7 @@ function buildFixtureAssertions(model, spec) {
21728
22011
  if (field.type.kind !== "primitive" || field.type.type !== "string") continue;
21729
22012
  if (field.type.format === "date-time" || field.type.format === "date") continue;
21730
22013
  if (field.type.format === "binary") continue;
21731
- const csField = fieldName$2(field.name);
22014
+ const csField = domainFieldName$2(field);
21732
22015
  const val = fixture[field.name];
21733
22016
  if (typeof val === "string" && val.length > 0) assertions.push(`Assert.Equal(${csStringLiteral(val)}, result.${csField});`);
21734
22017
  else assertions.push(`Assert.NotEmpty(result.${csField});`);
@@ -21987,7 +22270,7 @@ const dotnetEmitter = {
21987
22270
  return buildOperationsMap$3(spec, fixNamespace(ctx));
21988
22271
  },
21989
22272
  fileHeader() {
21990
- return "// This file is auto-generated by oagen. Do not edit.";
22273
+ return `// ${AUTOGEN_NOTICE}`;
21991
22274
  },
21992
22275
  formatCommand(targetDir) {
21993
22276
  const workspace = findDotnetWorkspace(targetDir);
@@ -22050,6 +22333,16 @@ function propertyName(name) {
22050
22333
  if (camel === "object") return "objectType";
22051
22334
  return escapeReserved(camel);
22052
22335
  }
22336
+ /**
22337
+ * camelCase domain property name for a MODEL field, honoring a `domainName`
22338
+ * override (set via the `fieldHints` config) so a wire field can surface under
22339
+ * a friendlier name. The wire key (the `@JsonProperty("...")` argument) still
22340
+ * derives from `field.name`. No-op when `domainName` is unset, so it is also
22341
+ * safe on params. Only apply to model fields.
22342
+ */
22343
+ function domainPropertyName(field) {
22344
+ return propertyName(field.domainName ?? field.name);
22345
+ }
22053
22346
  /** Lower-case Kotlin package segment for a service / mount group. */
22054
22347
  function packageSegment(name) {
22055
22348
  return toPascalCase(name).toLowerCase();
@@ -22752,7 +23045,7 @@ function renderFields(fields, overrideFields = /* @__PURE__ */ new Set()) {
22752
23045
  const lines = [];
22753
23046
  for (const rawField of fields) {
22754
23047
  const field = promoteFieldType$1(rawField);
22755
- const kotlinName = propertyName(field.name);
23048
+ const kotlinName = domainPropertyName(field);
22756
23049
  if (seen.has(kotlinName)) continue;
22757
23050
  seen.add(kotlinName);
22758
23051
  const baseType = mapTypeRef$2(field.type);
@@ -24606,7 +24899,7 @@ function buildResponseAssertions(responseModelName, ctx) {
24606
24899
  for (const field of model.fields) {
24607
24900
  if (!field.required) continue;
24608
24901
  if (assertions.length >= MAX_RESPONSE_ASSERTIONS) break;
24609
- const ktProp = propertyName(field.name);
24902
+ const ktProp = domainPropertyName(field);
24610
24903
  const type = field.type;
24611
24904
  if (type.kind === "primitive") {
24612
24905
  if (type.format === "date-time") continue;
@@ -25050,7 +25343,7 @@ const kotlinEmitter = {
25050
25343
  return buildOperationsMap$2(spec, ctx);
25051
25344
  },
25052
25345
  fileHeader() {
25053
- return "// This file is auto-generated by oagen. Do not edit.";
25346
+ return `// ${AUTOGEN_NOTICE}`;
25054
25347
  },
25055
25348
  formatCommand(targetDir) {
25056
25349
  if (!fs.existsSync(path.join(targetDir, "gradlew"))) return null;
@@ -25102,6 +25395,15 @@ function fieldName$1(name) {
25102
25395
  return toSnakeCase(name);
25103
25396
  }
25104
25397
  /**
25398
+ * snake_case domain field name for a model field, honoring a `domainName`
25399
+ * override (set via the `fieldHints` config) so a wire field can surface under
25400
+ * a friendlier name. The wire key (still derived from `field.name`) is what
25401
+ * gets sent/received over the wire — only the domain attr/accessor name changes.
25402
+ */
25403
+ function domainFieldName$1(field) {
25404
+ return toSnakeCase(field.domainName ?? field.name);
25405
+ }
25406
+ /**
25105
25407
  * Ruby reserved words that cannot be used as parameter names.
25106
25408
  * When a path/query param name collides, suffix with underscore.
25107
25409
  */
@@ -25332,7 +25634,7 @@ function generateModels$1(models, ctx) {
25332
25634
  }
25333
25635
  const seenFieldNames = /* @__PURE__ */ new Set();
25334
25636
  const fields = model.fields.filter((f) => {
25335
- const n = fieldName$1(f.name);
25637
+ const n = domainFieldName$1(f);
25336
25638
  if (seenFieldNames.has(n)) return false;
25337
25639
  seenFieldNames.add(n);
25338
25640
  return true;
@@ -25346,7 +25648,7 @@ function generateModels$1(models, ctx) {
25346
25648
  lines.push(" HASH_ATTRS = {");
25347
25649
  for (let i = 0; i < fields.length; i++) {
25348
25650
  const field = fields[i];
25349
- const fname = fieldName$1(field.name);
25651
+ const fname = domainFieldName$1(field);
25350
25652
  const sep = i === fields.length - 1 ? "" : ",";
25351
25653
  lines.push(` ${rubyHashLiteralKey(field.name)} :${fname}${sep}`);
25352
25654
  }
@@ -25355,13 +25657,13 @@ function generateModels$1(models, ctx) {
25355
25657
  if (deprecatedFields.length > 0) {
25356
25658
  for (const f of deprecatedFields) {
25357
25659
  const desc = f.description ? ` ${f.description.split("\n")[0].trim()}` : "";
25358
- lines.push(` # @!attribute ${fieldName$1(f.name)}`);
25660
+ lines.push(` # @!attribute ${domainFieldName$1(f)}`);
25359
25661
  lines.push(` # @deprecated${desc}`);
25360
25662
  }
25361
25663
  lines.push("");
25362
25664
  }
25363
25665
  if (accessorFields.length > 0) {
25364
- const attrs = accessorFields.map((f) => `:${fieldName$1(f.name)}`);
25666
+ const attrs = accessorFields.map((f) => `:${domainFieldName$1(f)}`);
25365
25667
  if (attrs.length === 1) lines.push(` attr_accessor ${attrs[0]}`);
25366
25668
  else {
25367
25669
  lines.push(` attr_accessor \\`);
@@ -25373,7 +25675,7 @@ function generateModels$1(models, ctx) {
25373
25675
  lines.push("");
25374
25676
  }
25375
25677
  for (const f of deprecatedFields) {
25376
- const fname = fieldName$1(f.name);
25678
+ const fname = domainFieldName$1(f);
25377
25679
  lines.push(` def ${fname}`);
25378
25680
  lines.push(` warn "[DEPRECATION] \\\`${fname}\\\` is deprecated and will be removed in a future version.", uplevel: 1`);
25379
25681
  lines.push(` @${fname}`);
@@ -25387,7 +25689,7 @@ function generateModels$1(models, ctx) {
25387
25689
  lines.push(" def initialize(json)");
25388
25690
  lines.push(" hash = self.class.normalize(json)");
25389
25691
  for (const field of fields) {
25390
- const fname = fieldName$1(field.name);
25692
+ const fname = domainFieldName$1(field);
25391
25693
  const rawKey = field.name;
25392
25694
  lines.push(` ${deserializeAssignment(fname, rawKey, field.type, field.required, enumNames, modelNames)}`);
25393
25695
  }
@@ -26883,7 +27185,7 @@ function generateModelRoundTripTest(spec) {
26883
27185
  const dedupFields = /* @__PURE__ */ new Set();
26884
27186
  for (const f of model.fields) {
26885
27187
  const wireName = f.name;
26886
- const rubyFieldName = fieldName$1(f.name);
27188
+ const rubyFieldName = domainFieldName$1(f);
26887
27189
  if (dedupFields.has(rubyFieldName)) continue;
26888
27190
  dedupFields.add(rubyFieldName);
26889
27191
  const stub = roundTripStub(f.type, enumNames);
@@ -27212,7 +27514,7 @@ function generateRbiFiles(spec, ctx) {
27212
27514
  lines.push("");
27213
27515
  const seenFieldNames = /* @__PURE__ */ new Set();
27214
27516
  for (const f of model.fields) {
27215
- const fname = fieldName$1(f.name);
27517
+ const fname = domainFieldName$1(f);
27216
27518
  if (seenFieldNames.has(fname)) continue;
27217
27519
  seenFieldNames.add(fname);
27218
27520
  const sorbetType = f.required ? mapSorbetType(f.type) : wrapNilable(mapSorbetType(f.type));
@@ -27489,7 +27791,7 @@ const rubyEmitter = {
27489
27791
  return buildOperationsMap$1(spec, ctx);
27490
27792
  },
27491
27793
  fileHeader() {
27492
- return `# frozen_string_literal: true\n\n# This file is auto-generated by oagen. Do not edit.`;
27794
+ return `# frozen_string_literal: true\n\n# ${AUTOGEN_NOTICE}`;
27493
27795
  },
27494
27796
  formatCommand(targetDir) {
27495
27797
  return {
@@ -27575,6 +27877,15 @@ function methodName(name) {
27575
27877
  function fieldName(name) {
27576
27878
  return escapeKeyword(toSnakeCase(name));
27577
27879
  }
27880
+ /**
27881
+ * snake_case domain field name for a model field, honoring a `domainName`
27882
+ * override (set via the `fieldHints` config) so a wire field can surface under
27883
+ * a friendlier identifier. The wire name (and thus the `#[serde(rename = ...)]`
27884
+ * key) still derives from `field.name`.
27885
+ */
27886
+ function domainFieldName(field) {
27887
+ return escapeKeyword(toSnakeCase(field.domainName ?? field.name));
27888
+ }
27578
27889
  /** PascalCase enum variant. */
27579
27890
  function variantName(value) {
27580
27891
  const pascal = toPascalCase(String(value));
@@ -27846,6 +28157,7 @@ function generateModels(models, ctx, registry) {
27846
28157
  const files = [];
27847
28158
  const moduleNames = [];
27848
28159
  const seen = /* @__PURE__ */ new Set();
28160
+ const taggedVariantFields = collectTaggedVariantFields(models);
27849
28161
  for (const model of models) {
27850
28162
  const mod = moduleName(model.name);
27851
28163
  if (seen.has(mod)) continue;
@@ -27854,7 +28166,7 @@ function generateModels(models, ctx, registry) {
27854
28166
  const path = ctx.overlayLookup?.fileBySymbol?.get(model.name) ?? `src/models/${mod}.rs`;
27855
28167
  files.push({
27856
28168
  path,
27857
- content: renderModel(model, registry),
28169
+ content: renderModel(model, registry, taggedVariantFields.get(model.name)),
27858
28170
  overwriteExisting: true
27859
28171
  });
27860
28172
  }
@@ -27866,7 +28178,43 @@ function generateModels(models, ctx, registry) {
27866
28178
  });
27867
28179
  return files;
27868
28180
  }
27869
- function renderModel(model, registry) {
28181
+ /**
28182
+ * Walk every model field and record which models are arms of an
28183
+ * internally-tagged union, mapped to that union's discriminator property.
28184
+ */
28185
+ function collectTaggedVariantFields(models) {
28186
+ const out = /* @__PURE__ */ new Map();
28187
+ const visit = (ref) => {
28188
+ switch (ref.kind) {
28189
+ case "union":
28190
+ if (ref.discriminator?.property) for (const variant of ref.variants) {
28191
+ const name = variantModelName(variant);
28192
+ if (name) out.set(name, ref.discriminator.property);
28193
+ }
28194
+ ref.variants.forEach(visit);
28195
+ break;
28196
+ case "array":
28197
+ visit(ref.items);
28198
+ break;
28199
+ case "nullable":
28200
+ visit(ref.inner);
28201
+ break;
28202
+ case "map":
28203
+ visit(ref.valueType);
28204
+ break;
28205
+ default: break;
28206
+ }
28207
+ };
28208
+ for (const model of models) for (const field of model.fields) visit(field.type);
28209
+ return out;
28210
+ }
28211
+ /** Resolve the underlying model name of a union arm, unwrapping a nullable. */
28212
+ function variantModelName(ref) {
28213
+ if (ref.kind === "model") return ref.name;
28214
+ if (ref.kind === "nullable") return variantModelName(ref.inner);
28215
+ return null;
28216
+ }
28217
+ function renderModel(model, registry, tagField) {
27870
28218
  const lines = [];
27871
28219
  lines.push(HEADER_PLACEHOLDER);
27872
28220
  lines.push("#[allow(unused_imports)]");
@@ -27878,7 +28226,7 @@ function renderModel(model, registry) {
27878
28226
  if (model.description) lines.push(...docComment$1(model.description));
27879
28227
  lines.push("#[derive(Debug, Clone, Serialize, Deserialize)]");
27880
28228
  const resolvedNames = resolveFieldNames(model.fields);
27881
- const fieldLines = model.fields.map((f, i) => renderField(f, resolvedNames[i], model.name, registry));
28229
+ const fieldLines = model.fields.map((f, i) => renderField(f, resolvedNames[i], model.name, registry, tagField));
27882
28230
  if (fieldLines.length === 0) lines.push(`pub struct ${typeName(model.name)} {}`);
27883
28231
  else {
27884
28232
  lines.push(`pub struct ${typeName(model.name)} {`);
@@ -27888,17 +28236,20 @@ function renderModel(model, registry) {
27888
28236
  return lines.filter((l) => l !== HEADER_PLACEHOLDER).join("\n") + "\n";
27889
28237
  }
27890
28238
  /**
27891
- * Resolve unique Rust identifiers for struct fields. Multiple wire names can
27892
- * collide after `fieldName()` snake-cases them (e.g. `integration_type` and
27893
- * `integrationType` both become `integration_type`). Subsequent collisions get
27894
- * a numeric suffix so the struct compiles; serde `rename` preserves the
27895
- * original wire name in every case.
28239
+ * Resolve unique Rust identifiers for struct fields. The domain identifier
28240
+ * honors a `fieldHints` override (`domainName`, e.g. wire `connection_type`
28241
+ * domain `type`); the wire name (and the `#[serde(rename = ...)]` key emitted
28242
+ * in `renderField`) still derives from `f.name`. Multiple names can collide
28243
+ * after snake-casing (e.g. `integration_type` and `integrationType` both
28244
+ * become `integration_type`). Subsequent collisions get a numeric suffix so
28245
+ * the struct compiles; serde `rename` preserves the original wire name in every
28246
+ * case.
27896
28247
  */
27897
28248
  function resolveFieldNames(fields) {
27898
28249
  const used = /* @__PURE__ */ new Set();
27899
28250
  const out = [];
27900
28251
  for (const f of fields) {
27901
- const base = fieldName(f.name);
28252
+ const base = domainFieldName(f);
27902
28253
  let candidate = base;
27903
28254
  let suffix = 2;
27904
28255
  while (used.has(candidate)) {
@@ -27910,7 +28261,7 @@ function resolveFieldNames(fields) {
27910
28261
  }
27911
28262
  return out;
27912
28263
  }
27913
- function renderField(field, rustField, modelName, registry) {
28264
+ function renderField(field, rustField, modelName, registry, tagField) {
27914
28265
  const lines = [];
27915
28266
  const hasDescription = !!field.description;
27916
28267
  if (hasDescription) for (const c of docComment$1(field.description)) lines.push(` ${c}`);
@@ -27925,8 +28276,13 @@ function renderField(field, rustField, modelName, registry) {
27925
28276
  });
27926
28277
  if ((!field.required || field.type.kind === "nullable") && !baseType.startsWith("Option<")) baseType = makeOptional(baseType);
27927
28278
  baseType = applySecretRedaction(baseType, field.name);
27928
- if (rename) lines.push(` #[serde(rename = "${rename}")]`);
27929
- if (baseType.startsWith("Option<")) lines.push(" #[serde(skip_serializing_if = \"Option::is_none\", default)]");
28279
+ if (tagField === field.name) {
28280
+ const args = rename ? `rename = "${rename}", default, skip_serializing` : "default, skip_serializing";
28281
+ lines.push(` #[serde(${args})]`);
28282
+ } else {
28283
+ if (rename) lines.push(` #[serde(rename = "${rename}")]`);
28284
+ if (baseType.startsWith("Option<")) lines.push(" #[serde(skip_serializing_if = \"Option::is_none\", default)]");
28285
+ }
27930
28286
  if (field.deprecated) lines.push(" #[deprecated]");
27931
28287
  lines.push(` pub ${rustField}: ${baseType},`);
27932
28288
  return lines.join("\n");
@@ -28508,7 +28864,7 @@ function registerSyntheticBody(op, paramsName, bodyGroupParamNames, bodyGroupFie
28508
28864
  if (!f.required && !rust.startsWith("Option<")) rust = makeOptional(rust);
28509
28865
  rust = applySecretRedaction(rust, f.name);
28510
28866
  return {
28511
- rustName: fieldName(f.name),
28867
+ rustName: domainFieldName(f),
28512
28868
  wireName: f.name,
28513
28869
  rustType: rust,
28514
28870
  required: !!f.required && !rust.startsWith("Option<"),
@@ -29863,7 +30219,7 @@ const rustEmitter = {
29863
30219
  return buildOperationsMap(spec, ctx);
29864
30220
  },
29865
30221
  fileHeader() {
29866
- return "// Code generated by oagen. DO NOT EDIT.";
30222
+ return `// ${AUTOGEN_NOTICE}`;
29867
30223
  },
29868
30224
  formatCommand(_targetDir) {
29869
30225
  return {
@@ -29918,4 +30274,4 @@ const workosEmittersPlugin = {
29918
30274
  //#endregion
29919
30275
  export { fieldName$2 as A, servicePropertyName$2 as B, apiClassName as C, dotnetEmitter as D, propertyName as E, fieldName$3 as F, fieldName$5 as H, methodName$3 as I, trimMountedResourceFromMethod$2 as L, trimMountedResourceFromMethod$1 as M, goEmitter as N, appendAsyncSuffix as O, className$3 as P, phpEmitter as R, kotlinEmitter as S, packageSegment as T, safeParamName$1 as U, pythonEmitter as V, nodeEmitter as W, rubyEmitter as _, rustExtractor as a, resolveServiceTarget as b, pythonExtractor as c, rustEmitter as d, fieldName as f, typeName as g, resourceAccessorName as h, kotlinExtractor as i, methodName$2 as j, className$2 as k, rubyExtractor as l, moduleName as m, elixirExtractor as n, goExtractor as o, methodName as p, dotnetExtractor as r, phpExtractor as s, workosEmittersPlugin as t, nodeExtractor as u, buildExportedClassNameSet as v, methodName$1 as w, safeParamName as x, fieldName$1 as y, fieldName$4 as z };
29920
30276
 
29921
- //# sourceMappingURL=plugin-bqfwowQ3.mjs.map
30277
+ //# sourceMappingURL=plugin-Cciic50q.mjs.map