@workos/oagen-emitters 0.9.1 → 0.11.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.
@@ -3245,7 +3245,7 @@ function computeNonEventReachable(services, models) {
3245
3245
  //#region src/node/enums.ts
3246
3246
  function generateEnums$6(enums, ctx) {
3247
3247
  if (enums.length === 0) return [];
3248
- const enumToService = assignEnumsToServices$1(enums, ctx.spec.services);
3248
+ const enumToService = assignEnumsToServices(enums, ctx.spec.services);
3249
3249
  const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
3250
3250
  const resolveDir = (irService) => irService ? resolveServiceDir$1(serviceNameMap.get(irService) ?? irService) : "common";
3251
3251
  const files = [];
@@ -3315,7 +3315,7 @@ function extractLiteralUnionValues(aliasValue) {
3315
3315
  while ((match = regex.exec(aliasValue)) !== null) values.add(match[1]);
3316
3316
  return values;
3317
3317
  }
3318
- function assignEnumsToServices$1(enums, services) {
3318
+ function assignEnumsToServices(enums, services) {
3319
3319
  const enumToService = /* @__PURE__ */ new Map();
3320
3320
  const enumNames = new Set(enums.map((e) => e.name));
3321
3321
  for (const service of services) for (const op of service.operations) {
@@ -3855,7 +3855,7 @@ function generateModels$6(models, ctx, shared) {
3855
3855
  const typeDecls = /* @__PURE__ */ new Map();
3856
3856
  const crossServiceImports = /* @__PURE__ */ new Map();
3857
3857
  const unresolvableNames = /* @__PURE__ */ new Set();
3858
- const enumToService = assignEnumsToServices$1(ctx.spec.enums, ctx.spec.services);
3858
+ const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
3859
3859
  const resolvedEnumNames = /* @__PURE__ */ new Map();
3860
3860
  for (const e of ctx.spec.enums) resolvedEnumNames.set(resolveInterfaceName(e.name, ctx), e.name);
3861
3861
  for (const field of model.fields) {
@@ -4960,7 +4960,7 @@ function generateResourceClass(service, ctx) {
4960
4960
  for (const [relPath, specifiers] of serializerImportsByPath) lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
4961
4961
  const specEnumNames = new Set(ctx.spec.enums.map((e) => e.name));
4962
4962
  if (paramEnums.size > 0) {
4963
- const enumToService = assignEnumsToServices$1(ctx.spec.enums, ctx.spec.services);
4963
+ const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
4964
4964
  for (const name of paramEnums) {
4965
4965
  if (allModels.has(name)) continue;
4966
4966
  if (!specEnumNames.has(name)) continue;
@@ -6924,6 +6924,330 @@ function joinUnionVariants$4(ref, variants) {
6924
6924
  return `Union[${unique.join(", ")}]`;
6925
6925
  }
6926
6926
  //#endregion
6927
+ //#region src/python/shared-schemas.ts
6928
+ /**
6929
+ * Walk every operation across all services and tally, per schema, the set of
6930
+ * services that transitively reference it. Schemas referenced by more than one
6931
+ * service are "shared" — they should be emitted under common/ rather than
6932
+ * the first alphabetical service that happens to use them.
6933
+ *
6934
+ * Transitive walk for models follows model->model field references AND
6935
+ * discriminator variant mappings to a fixed point; enums are leaves.
6936
+ */
6937
+ function findSharedSchemas(spec) {
6938
+ const modelsByName = new Map(spec.models.map((m) => [m.name, m]));
6939
+ const modelToServices = /* @__PURE__ */ new Map();
6940
+ const enumToServices = /* @__PURE__ */ new Map();
6941
+ const note = (map, name, service) => {
6942
+ let bucket = map.get(name);
6943
+ if (!bucket) {
6944
+ bucket = /* @__PURE__ */ new Set();
6945
+ map.set(name, bucket);
6946
+ }
6947
+ bucket.add(service);
6948
+ };
6949
+ for (const service of spec.services) {
6950
+ const directModels = /* @__PURE__ */ new Set();
6951
+ const directEnums = /* @__PURE__ */ new Set();
6952
+ const collect = (ref) => {
6953
+ walkTypeRef(ref, {
6954
+ model: (r) => directModels.add(r.name),
6955
+ enum: (r) => directEnums.add(r.name)
6956
+ });
6957
+ };
6958
+ for (const op of service.operations) {
6959
+ if (op.requestBody) collect(op.requestBody);
6960
+ collect(op.response);
6961
+ for (const p of [
6962
+ ...op.pathParams,
6963
+ ...op.queryParams,
6964
+ ...op.headerParams,
6965
+ ...op.cookieParams ?? []
6966
+ ]) collect(p.type);
6967
+ if (op.pagination) collect(op.pagination.itemType);
6968
+ for (const err of op.errors) if (err.type) collect(err.type);
6969
+ for (const sr of op.successResponses ?? []) collect(sr.type);
6970
+ }
6971
+ const queue = [...directModels];
6972
+ while (queue.length > 0) {
6973
+ const name = queue.pop();
6974
+ const model = modelsByName.get(name);
6975
+ if (!model) continue;
6976
+ for (const field of model.fields) walkTypeRef(field.type, {
6977
+ model: (r) => {
6978
+ if (!directModels.has(r.name)) {
6979
+ directModels.add(r.name);
6980
+ queue.push(r.name);
6981
+ }
6982
+ },
6983
+ enum: (r) => directEnums.add(r.name)
6984
+ });
6985
+ const disc = model.discriminator;
6986
+ if (disc?.mapping) {
6987
+ for (const variantName of Object.values(disc.mapping)) if (!directModels.has(variantName)) {
6988
+ directModels.add(variantName);
6989
+ queue.push(variantName);
6990
+ }
6991
+ }
6992
+ }
6993
+ for (const name of directModels) note(modelToServices, name, service.name);
6994
+ for (const name of directEnums) note(enumToServices, name, service.name);
6995
+ }
6996
+ const sharedModels = /* @__PURE__ */ new Set();
6997
+ for (const [name, services] of modelToServices) if (services.size >= 2) sharedModels.add(name);
6998
+ const sharedEnums = /* @__PURE__ */ new Set();
6999
+ for (const [name, services] of enumToServices) if (services.size >= 2) sharedEnums.add(name);
7000
+ return {
7001
+ models: sharedModels,
7002
+ enums: sharedEnums
7003
+ };
7004
+ }
7005
+ function computeSchemaPlacement(spec, ctx) {
7006
+ const annotatedModels = detectDiscriminators(spec.models);
7007
+ if (annotatedModels !== spec.models) spec = {
7008
+ ...spec,
7009
+ models: annotatedModels
7010
+ };
7011
+ const modelsByName = new Map(spec.models.map((m) => [m.name, m]));
7012
+ const hintedModels = new Set(Object.keys(ctx.modelHints ?? {}));
7013
+ const originalModelToService = assignModelsToServices(spec.models, spec.services, ctx.modelHints);
7014
+ const originalEnumToService = assignEnumsToServicesNatural(spec.enums, spec.services);
7015
+ const modelAliases = computeModelAliases(spec);
7016
+ const enumAliases = computeEnumAliases(spec.enums);
7017
+ const initial = findSharedSchemas(spec);
7018
+ for (const [aliasName, canonicalName] of modelAliases) if (initial.models.has(aliasName)) initial.models.add(canonicalName);
7019
+ for (const [aliasName, canonicalName] of enumAliases) if (initial.enums.has(aliasName)) initial.enums.add(canonicalName);
7020
+ const sharedModels = /* @__PURE__ */ new Set();
7021
+ for (const name of initial.models) if (!hintedModels.has(name)) sharedModels.add(name);
7022
+ for (const model of spec.models) if (!originalModelToService.has(model.name) && !hintedModels.has(model.name)) sharedModels.add(model.name);
7023
+ const sharedEnums = new Set(initial.enums);
7024
+ for (const enumDef of spec.enums) if (!originalEnumToService.has(enumDef.name)) sharedEnums.add(enumDef.name);
7025
+ let changed = true;
7026
+ while (changed) {
7027
+ changed = false;
7028
+ for (const name of sharedModels) {
7029
+ const model = modelsByName.get(name);
7030
+ if (!model) continue;
7031
+ const deps = collectEmittedDependencies(model, modelAliases);
7032
+ for (const dep of deps.models) {
7033
+ if (sharedModels.has(dep)) continue;
7034
+ if (!modelsByName.has(dep)) continue;
7035
+ sharedModels.add(dep);
7036
+ changed = true;
7037
+ }
7038
+ for (const dep of deps.enums) {
7039
+ if (sharedEnums.has(dep)) continue;
7040
+ sharedEnums.add(dep);
7041
+ changed = true;
7042
+ }
7043
+ }
7044
+ }
7045
+ const modelToService = new Map(originalModelToService);
7046
+ for (const name of sharedModels) modelToService.delete(name);
7047
+ const enumToService = new Map(originalEnumToService);
7048
+ for (const name of sharedEnums) enumToService.delete(name);
7049
+ const relocatedModels = /* @__PURE__ */ new Set();
7050
+ for (const name of sharedModels) {
7051
+ if (!originalModelToService.has(name)) continue;
7052
+ relocatedModels.add(name);
7053
+ }
7054
+ const relocatedEnums = /* @__PURE__ */ new Set();
7055
+ for (const name of sharedEnums) {
7056
+ if (!originalEnumToService.has(name)) continue;
7057
+ relocatedEnums.add(name);
7058
+ }
7059
+ return {
7060
+ modelToService,
7061
+ enumToService,
7062
+ originalModelToService,
7063
+ originalEnumToService,
7064
+ relocatedModels,
7065
+ relocatedEnums,
7066
+ modelAliases,
7067
+ enumAliases
7068
+ };
7069
+ }
7070
+ /**
7071
+ * Dependencies the emitter will materialize for a model's generated file.
7072
+ * Captures alias canonicals, discriminator variants, and field-level model+enum
7073
+ * references so the placement closure can decide whether the dependency must
7074
+ * also live in `common/`.
7075
+ */
7076
+ function collectEmittedDependencies(model, modelAliases) {
7077
+ const models = /* @__PURE__ */ new Set();
7078
+ const enums = /* @__PURE__ */ new Set();
7079
+ const canonical = modelAliases.get(model.name);
7080
+ if (canonical) {
7081
+ models.add(canonical);
7082
+ return {
7083
+ models,
7084
+ enums
7085
+ };
7086
+ }
7087
+ const disc = model.discriminator;
7088
+ if (disc?.mapping) for (const variant of Object.values(disc.mapping)) models.add(variant);
7089
+ const fieldDeps = collectFieldDependencies(model);
7090
+ for (const m of fieldDeps.models) models.add(m);
7091
+ for (const e of fieldDeps.enums) enums.add(e);
7092
+ return {
7093
+ models,
7094
+ enums
7095
+ };
7096
+ }
7097
+ function collectModelUsage(spec) {
7098
+ const request = /* @__PURE__ */ new Set();
7099
+ const response = /* @__PURE__ */ new Set();
7100
+ for (const service of spec.services) for (const op of service.operations) {
7101
+ const plan = planOperation(op);
7102
+ if (plan.responseModelName) response.add(plan.responseModelName);
7103
+ if (op.pagination?.itemType.kind === "model") response.add(op.pagination.itemType.name);
7104
+ if (op.requestBody?.kind === "model") request.add(op.requestBody.name);
7105
+ if (op.requestBody?.kind === "union") {
7106
+ for (const variant of op.requestBody.variants ?? []) if (variant.kind === "model") request.add(variant.name);
7107
+ }
7108
+ }
7109
+ const mixed = /* @__PURE__ */ new Set();
7110
+ for (const name of request) if (response.has(name)) mixed.add(name);
7111
+ return {
7112
+ requestOnly: new Set([...request].filter((name) => !mixed.has(name))),
7113
+ response: new Set([...response].filter((name) => !mixed.has(name))),
7114
+ mixed
7115
+ };
7116
+ }
7117
+ function compareAliasPriority(left, right, usage) {
7118
+ const score = (name) => {
7119
+ if (usage.response.has(name)) return 0;
7120
+ if (usage.mixed.has(name)) return 1;
7121
+ if (usage.requestOnly.has(name)) return 2;
7122
+ return 3;
7123
+ };
7124
+ const diff = score(left) - score(right);
7125
+ if (diff !== 0) return diff;
7126
+ return left.localeCompare(right);
7127
+ }
7128
+ function canAliasModels(canonical, alias, usage) {
7129
+ if (fileName$2(canonical) === fileName$2(alias)) return false;
7130
+ if (usage.response.has(canonical) && usage.requestOnly.has(alias) || usage.response.has(alias) && usage.requestOnly.has(canonical)) return false;
7131
+ return true;
7132
+ }
7133
+ /**
7134
+ * Compute the Python emitter's structural model dedup map: alias -> canonical.
7135
+ * Mirrors the logic in models.ts so the placement closure can promote
7136
+ * canonicals when their aliases are shared.
7137
+ */
7138
+ function computeModelAliases(spec) {
7139
+ const recursiveHashes = buildRecursiveHashMap$1(spec.models, spec.enums);
7140
+ const usage = collectModelUsage(spec);
7141
+ const hashGroups = /* @__PURE__ */ new Map();
7142
+ for (const model of spec.models) {
7143
+ if (model.fields.length === 0 && !model.discriminator) {}
7144
+ const hash = recursiveHashes.get(model.name) ?? "";
7145
+ if (!hashGroups.has(hash)) hashGroups.set(hash, []);
7146
+ hashGroups.get(hash).push(model.name);
7147
+ }
7148
+ const aliasOf = /* @__PURE__ */ new Map();
7149
+ for (const [, names] of hashGroups) {
7150
+ if (names.length <= 1) continue;
7151
+ const sorted = [...names].sort((a, b) => compareAliasPriority(a, b, usage));
7152
+ const canonical = sorted[0];
7153
+ for (let i = 1; i < sorted.length; i++) if (canAliasModels(canonical, sorted[i], usage)) aliasOf.set(sorted[i], canonical);
7154
+ }
7155
+ return aliasOf;
7156
+ }
7157
+ /**
7158
+ * Compute the Python emitter's structural enum dedup map: alias -> canonical.
7159
+ * Mirrors the logic in enums.ts.
7160
+ */
7161
+ function computeEnumAliases(enums) {
7162
+ const hashGroups = /* @__PURE__ */ new Map();
7163
+ for (const enumDef of enums) {
7164
+ const hash = [...enumDef.values].map((v) => String(v.value)).sort().join("|");
7165
+ if (!hashGroups.has(hash)) hashGroups.set(hash, []);
7166
+ hashGroups.get(hash).push(enumDef.name);
7167
+ }
7168
+ const aliasOf = /* @__PURE__ */ new Map();
7169
+ for (const [, names] of hashGroups) {
7170
+ if (names.length <= 1) continue;
7171
+ const sorted = [...names].sort();
7172
+ const canonical = sorted[0];
7173
+ for (let i = 1; i < sorted.length; i++) aliasOf.set(sorted[i], canonical);
7174
+ }
7175
+ return aliasOf;
7176
+ }
7177
+ /**
7178
+ * Recursive structural hashing for models so dedup runs against deeply-equal
7179
+ * shapes, not just same-named ones. Cycles fall back to the model name.
7180
+ */
7181
+ function buildRecursiveHashMap$1(models, enums) {
7182
+ const modelByName = new Map(models.map((m) => [m.name, m]));
7183
+ const hashCache = /* @__PURE__ */ new Map();
7184
+ const visiting = /* @__PURE__ */ new Set();
7185
+ const enumVH = /* @__PURE__ */ new Map();
7186
+ for (const e of enums) enumVH.set(e.name, [...e.values].map((v) => String(v.value)).sort().join("|"));
7187
+ function modelHash(name) {
7188
+ const cached = hashCache.get(name);
7189
+ if (cached != null) return cached;
7190
+ if (visiting.has(name)) return `m:${name}`;
7191
+ visiting.add(name);
7192
+ const model = modelByName.get(name);
7193
+ if (!model) {
7194
+ visiting.delete(name);
7195
+ return `m:${name}`;
7196
+ }
7197
+ const hash = [...model.fields].sort((a, b) => a.name.localeCompare(b.name)).map((f) => `${f.name}:${deepTypeHash(f.type)}:${f.required}`).join("|");
7198
+ visiting.delete(name);
7199
+ hashCache.set(name, hash);
7200
+ return hash;
7201
+ }
7202
+ function deepTypeHash(ref) {
7203
+ switch (ref.kind) {
7204
+ case "primitive": return `p:${ref.type}${ref.format ? `:${ref.format}` : ""}`;
7205
+ case "model": return `m:{${modelHash(ref.name)}}`;
7206
+ case "enum": {
7207
+ const vh = enumVH.get(ref.name);
7208
+ return vh != null ? `e:{${vh}}` : `e:${ref.name}`;
7209
+ }
7210
+ case "array": return `a:${deepTypeHash(ref.items)}`;
7211
+ case "nullable": return `n:${deepTypeHash(ref.inner)}`;
7212
+ case "union": return `u:${(ref.variants ?? []).map((v) => deepTypeHash(v)).sort().join(",")}`;
7213
+ case "map": return `d:${deepTypeHash(ref.valueType)}`;
7214
+ case "literal": return `l:${String(ref.value)}`;
7215
+ default: return "unknown";
7216
+ }
7217
+ }
7218
+ for (const model of models) modelHash(model.name);
7219
+ return hashCache;
7220
+ }
7221
+ /**
7222
+ * Natural enum-to-service assignment without sharing logic — the first service
7223
+ * (alphabetically by spec order) to reference an enum wins.
7224
+ */
7225
+ function assignEnumsToServicesNatural(enums, services) {
7226
+ const enumNames = new Set(enums.map((e) => e.name));
7227
+ const enumToService = /* @__PURE__ */ new Map();
7228
+ for (const service of services) {
7229
+ const refs = /* @__PURE__ */ new Set();
7230
+ const collect = (ref) => {
7231
+ walkTypeRef(ref, { enum: (r) => refs.add(r.name) });
7232
+ };
7233
+ for (const op of service.operations) {
7234
+ if (op.requestBody) collect(op.requestBody);
7235
+ collect(op.response);
7236
+ for (const p of [
7237
+ ...op.pathParams,
7238
+ ...op.queryParams,
7239
+ ...op.headerParams,
7240
+ ...op.cookieParams ?? []
7241
+ ]) collect(p.type);
7242
+ }
7243
+ for (const name of refs) {
7244
+ if (!enumNames.has(name)) continue;
7245
+ if (!enumToService.has(name)) enumToService.set(name, service.name);
7246
+ }
7247
+ }
7248
+ return enumToService;
7249
+ }
7250
+ //#endregion
6927
7251
  //#region src/python/enums.ts
6928
7252
  /**
6929
7253
  * Convert a PascalCase class name to a human-readable lowercase string,
@@ -6940,12 +7264,16 @@ function humanizeClassName(name) {
6940
7264
  */
6941
7265
  function generateEnums$5(enums, ctx) {
6942
7266
  if (enums.length === 0) return [];
6943
- const enumToService = assignEnumsToServices(enums, ctx.spec.services);
7267
+ const placement = computeSchemaPlacement(enums === ctx.spec.enums ? ctx.spec : {
7268
+ ...ctx.spec,
7269
+ enums
7270
+ }, ctx);
7271
+ const enumToService = placement.enumToService;
6944
7272
  const mountDirMap = buildMountDirMap$1(ctx);
6945
7273
  const resolveDir = (irService) => irService ? mountDirMap.get(irService) ?? "common" : "common";
6946
7274
  const files = [];
6947
7275
  const compatAliases = collectCompatEnumAliases(enums, ctx);
6948
- const aliasOf = collectEnumAliasOf$3(enums);
7276
+ const aliasOf = placement.enumAliases;
6949
7277
  for (const enumDef of enums) {
6950
7278
  const dirName = resolveDir(enumToService.get(enumDef.name));
6951
7279
  const canonicalName = aliasOf.get(enumDef.name);
@@ -7110,24 +7438,11 @@ function collectCompatEnumAliases(enums, ctx) {
7110
7438
  }
7111
7439
  return aliases;
7112
7440
  }
7113
- function collectEnumAliasOf$3(enums) {
7114
- const hashGroups = /* @__PURE__ */ new Map();
7115
- for (const enumDef of enums) {
7116
- const hash = [...enumDef.values].map((v) => String(v.value)).sort().join("|");
7117
- if (!hashGroups.has(hash)) hashGroups.set(hash, []);
7118
- hashGroups.get(hash).push(enumDef.name);
7119
- }
7120
- const aliasOf = /* @__PURE__ */ new Map();
7121
- for (const [, names] of hashGroups) {
7122
- if (names.length <= 1) continue;
7123
- const sorted = [...names].sort();
7124
- const canonical = sorted[0];
7125
- for (let i = 1; i < sorted.length; i++) aliasOf.set(sorted[i], canonical);
7126
- }
7127
- return aliasOf;
7128
- }
7129
7441
  function collectGeneratedEnumSymbolsByDir(enums, ctx) {
7130
- const enumToService = assignEnumsToServices(enums, ctx.spec.services);
7442
+ const enumToService = computeSchemaPlacement(enums === ctx.spec.enums ? ctx.spec : {
7443
+ ...ctx.spec,
7444
+ enums
7445
+ }, ctx).enumToService;
7131
7446
  const mountDirMap = buildMountDirMap$1(ctx);
7132
7447
  const resolveDir = (irService) => irService ? mountDirMap.get(irService) ?? "common" : "common";
7133
7448
  const compatAliases = collectCompatEnumAliases(enums, ctx);
@@ -7143,26 +7458,6 @@ function collectGeneratedEnumSymbolsByDir(enums, ctx) {
7143
7458
  function enumValueHash(enumDef) {
7144
7459
  return [...enumDef.values].map((value) => String(value.value)).sort().join("|");
7145
7460
  }
7146
- function assignEnumsToServices(enums, services) {
7147
- const enumToService = /* @__PURE__ */ new Map();
7148
- const enumNames = new Set(enums.map((e) => e.name));
7149
- for (const service of services) for (const op of service.operations) {
7150
- const refs = /* @__PURE__ */ new Set();
7151
- const collect = (ref) => {
7152
- walkTypeRef(ref, { enum: (r) => refs.add(r.name) });
7153
- };
7154
- if (op.requestBody) collect(op.requestBody);
7155
- collect(op.response);
7156
- for (const p of [
7157
- ...op.pathParams,
7158
- ...op.queryParams,
7159
- ...op.headerParams,
7160
- ...op.cookieParams ?? []
7161
- ]) collect(p.type);
7162
- for (const name of refs) if (enumNames.has(name) && !enumToService.has(name)) enumToService.set(name, service.name);
7163
- }
7164
- return enumToService;
7165
- }
7166
7461
  //#endregion
7167
7462
  //#region src/python/models.ts
7168
7463
  /**
@@ -7171,29 +7466,16 @@ function assignEnumsToServices(enums, services) {
7171
7466
  */
7172
7467
  function generateModels$5(models, ctx) {
7173
7468
  if (models.length === 0) return [];
7174
- const modelToService = assignModelsToServices(models, ctx.spec.services, ctx.modelHints);
7175
- const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
7469
+ const { modelToService, enumToService, originalModelToService, originalEnumToService, relocatedModels, relocatedEnums, modelAliases: aliasOf } = computeSchemaPlacement(models === ctx.spec.models ? ctx.spec : {
7470
+ ...ctx.spec,
7471
+ models
7472
+ }, ctx);
7176
7473
  const mountDirMap = buildMountDirMap$1(ctx);
7177
7474
  const resolveDir = (irService) => irService ? mountDirMap.get(irService) ?? "common" : "common";
7178
7475
  const files = [];
7179
7476
  const emittedModelSymbolsByDir = /* @__PURE__ */ new Map();
7180
7477
  const symbolToFile = /* @__PURE__ */ new Map();
7181
- const modelUsage = collectModelUsage(ctx.spec);
7182
- const recursiveHashes = buildRecursiveHashMap$1(models, ctx.spec.enums);
7183
- const hashGroups = /* @__PURE__ */ new Map();
7184
- for (const model of models) {
7185
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
7186
- const hash = recursiveHashes.get(model.name) ?? "";
7187
- if (!hashGroups.has(hash)) hashGroups.set(hash, []);
7188
- hashGroups.get(hash).push(model.name);
7189
- }
7190
- const aliasOf = /* @__PURE__ */ new Map();
7191
- for (const [, names] of hashGroups) {
7192
- if (names.length <= 1) continue;
7193
- const sorted = [...names].sort((a, b) => compareAliasPriority(a, b, modelUsage));
7194
- const canonical = sorted[0];
7195
- for (let i = 1; i < sorted.length; i++) if (canAliasModels(canonical, sorted[i], modelUsage)) aliasOf.set(sorted[i], canonical);
7196
- }
7478
+ const symbolToOriginalService = /* @__PURE__ */ new Map();
7197
7479
  const emittedFilePaths = /* @__PURE__ */ new Set();
7198
7480
  for (const model of models) {
7199
7481
  if (isListWrapperModel(model)) continue;
@@ -7282,6 +7564,12 @@ function generateModels$5(models, ctx) {
7282
7564
  symbolToFile.set(variantTypeName, fileName$2(model.name));
7283
7565
  emittedModelSymbolsByDir.get(dirName).push(unknownClassName);
7284
7566
  symbolToFile.set(unknownClassName, fileName$2(model.name));
7567
+ const dispatcherNatural = originalModelToService.get(model.name);
7568
+ if (dispatcherNatural) {
7569
+ symbolToOriginalService.set(model.name, dispatcherNatural);
7570
+ symbolToOriginalService.set(variantTypeName, dispatcherNatural);
7571
+ symbolToOriginalService.set(unknownClassName, dispatcherNatural);
7572
+ }
7285
7573
  continue;
7286
7574
  }
7287
7575
  const canonicalName = aliasOf.get(model.name);
@@ -7303,6 +7591,8 @@ function generateModels$5(models, ctx) {
7303
7591
  });
7304
7592
  if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
7305
7593
  emittedModelSymbolsByDir.get(dirName).push(model.name);
7594
+ const aliasNatural = originalModelToService.get(model.name);
7595
+ if (aliasNatural) symbolToOriginalService.set(model.name, aliasNatural);
7306
7596
  continue;
7307
7597
  }
7308
7598
  const seenFieldNames = /* @__PURE__ */ new Set();
@@ -7387,19 +7677,29 @@ function generateModels$5(models, ctx) {
7387
7677
  lines.push(` def from_dict(cls, data: Dict[str, Any]) -> "${modelClassName}":`);
7388
7678
  lines.push(` """Deserialize from a dictionary."""`);
7389
7679
  lines.push(" try:");
7390
- lines.push(" return cls(");
7680
+ const preludeLines = [];
7681
+ const fieldAssignmentLines = [];
7391
7682
  for (const field of [...requiredFields, ...optionalFields]) {
7392
7683
  const pyFieldName = fieldName$4(field.name);
7393
7684
  const wireKey = field.name;
7394
7685
  const isRequired = !isOptionalField(model.name, field, ctx);
7686
+ const discPrelude = renderDiscriminatedUnionPrelude(field, pyFieldName, wireKey, modelClassName, isRequired);
7687
+ if (discPrelude) {
7688
+ preludeLines.push(...discPrelude.prelude);
7689
+ fieldAssignmentLines.push(` ${pyFieldName}=${discPrelude.expr},`);
7690
+ continue;
7691
+ }
7395
7692
  let accessor;
7396
7693
  if (field.type.kind === "literal" && isRequired) accessor = `data.get("${wireKey}", ${pythonLiteralDefault(field.type.value)})`;
7397
7694
  else accessor = isRequired ? `data["${wireKey}"]` : `data.get("${wireKey}")`;
7398
7695
  const deserRequired = isRequired && field.type.kind !== "nullable";
7399
7696
  const walrusVar = `_v_${pyFieldName}`;
7400
7697
  const deserExpr = deserializeField(field.type, accessor, deserRequired, walrusVar);
7401
- lines.push(` ${pyFieldName}=${deserExpr},`);
7698
+ fieldAssignmentLines.push(` ${pyFieldName}=${deserExpr},`);
7402
7699
  }
7700
+ for (const preludeLine of preludeLines) lines.push(preludeLine);
7701
+ lines.push(" return cls(");
7702
+ for (const assignment of fieldAssignmentLines) lines.push(assignment);
7403
7703
  lines.push(" )");
7404
7704
  lines.push(" except (KeyError, ValueError) as e:");
7405
7705
  lines.push(` _raise_deserialize_error("${modelClassName}", e)`);
@@ -7437,20 +7737,45 @@ function generateModels$5(models, ctx) {
7437
7737
  });
7438
7738
  if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
7439
7739
  emittedModelSymbolsByDir.get(dirName).push(model.name);
7740
+ const regularNatural = originalModelToService.get(model.name);
7741
+ if (regularNatural) symbolToOriginalService.set(model.name, regularNatural);
7440
7742
  }
7441
7743
  const symbolsByDir = /* @__PURE__ */ new Map();
7442
7744
  for (const [dirName, names] of emittedModelSymbolsByDir) {
7443
7745
  const key = `src/${ctx.namespace}/${dirName}/models`;
7444
7746
  if (!symbolsByDir.has(key)) symbolsByDir.set(key, []);
7445
- symbolsByDir.get(key).push(...names);
7747
+ for (const name of names) symbolsByDir.get(key).push({ name });
7446
7748
  }
7447
7749
  const reachableEnumNames = collectReachableEnumNames(ctx);
7448
7750
  const enumSymbolsByDir = collectGeneratedEnumSymbolsByDir(ctx.spec.enums.filter((enumDef) => reachableEnumNames.has(enumDef.name)), ctx);
7449
7751
  for (const [dirName, names] of enumSymbolsByDir) {
7450
7752
  const key = `src/${ctx.namespace}/${dirName}/models`;
7451
7753
  if (!symbolsByDir.has(key)) symbolsByDir.set(key, []);
7452
- symbolsByDir.get(key).push(...names);
7453
- }
7754
+ for (const name of names) symbolsByDir.get(key).push({ name });
7755
+ }
7756
+ const commonDirName = "common";
7757
+ const addReExport = (naturalService, name, sourceFile) => {
7758
+ if (!naturalService) return;
7759
+ const naturalDir = mountDirMap.get(naturalService) ?? naturalService;
7760
+ if (naturalDir === commonDirName) return;
7761
+ const key = `src/${ctx.namespace}/${naturalDir}/models`;
7762
+ if (!symbolsByDir.has(key)) symbolsByDir.set(key, []);
7763
+ symbolsByDir.get(key).push({
7764
+ name,
7765
+ reExport: {
7766
+ fromDir: commonDirName,
7767
+ file: sourceFile
7768
+ }
7769
+ });
7770
+ };
7771
+ for (const symbol of symbolToOriginalService.keys()) {
7772
+ const naturalService = symbolToOriginalService.get(symbol);
7773
+ const file = symbolToFile.get(symbol) ?? fileName$2(symbol);
7774
+ const primaryName = file === fileName$2(symbol) ? symbol : reverseLookupModelByFile(file, ctx);
7775
+ if (primaryName && !relocatedModels.has(primaryName)) continue;
7776
+ addReExport(naturalService, symbol, file);
7777
+ }
7778
+ for (const enumName of relocatedEnums) addReExport(originalEnumToService.get(enumName), enumName, fileName$2(enumName));
7454
7779
  const serviceDirModelPaths = /* @__PURE__ */ new Set();
7455
7780
  for (const service of ctx.spec.services) {
7456
7781
  const dirName = mountDirMap.get(service.name) ?? resolveDir(service.name);
@@ -7462,12 +7787,21 @@ function generateModels$5(models, ctx) {
7462
7787
  integrateTarget: true,
7463
7788
  overwriteExisting: true
7464
7789
  });
7465
- for (const [dirPath, names] of symbolsByDir) {
7466
- const uniqueNames = [...new Set(names)].sort();
7790
+ for (const [dirPath, symbols] of symbolsByDir) {
7791
+ const seen = /* @__PURE__ */ new Map();
7792
+ for (const sym of symbols) {
7793
+ const existing = seen.get(sym.name);
7794
+ if (!existing || existing.reExport && !sym.reExport) seen.set(sym.name, sym);
7795
+ }
7796
+ const uniqueSymbols = [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
7467
7797
  const importLines = [];
7468
- for (const name of uniqueNames) {
7469
- const fileNameForSymbol = symbolToFile.get(name) ?? fileName$2(name);
7470
- importLines.push(`from .${fileNameForSymbol} import ${className$5(name)} as ${className$5(name)}`);
7798
+ for (const sym of uniqueSymbols) {
7799
+ const cls = className$5(sym.name);
7800
+ if (sym.reExport) importLines.push(`from ${ctx.namespace}.${dirToModule(sym.reExport.fromDir)}.models.${sym.reExport.file} import ${cls} as ${cls}`);
7801
+ else {
7802
+ const fileNameForSymbol = symbolToFile.get(sym.name) ?? fileName$2(sym.name);
7803
+ importLines.push(`from .${fileNameForSymbol} import ${cls} as ${cls}`);
7804
+ }
7471
7805
  }
7472
7806
  const imports = importLines.join("\n");
7473
7807
  files.push({
@@ -7478,7 +7812,7 @@ function generateModels$5(models, ctx) {
7478
7812
  });
7479
7813
  if (!serviceDirModelPaths.has(dirPath)) {
7480
7814
  const parentDir = dirPath.replace(/\/models$/, "");
7481
- const reExports = [...new Set(names)].sort().map((name) => `from .models import ${className$5(name)} as ${className$5(name)}`).join("\n");
7815
+ const reExports = uniqueSymbols.map((sym) => `from .models import ${className$5(sym.name)} as ${className$5(sym.name)}`).join("\n");
7482
7816
  files.push({
7483
7817
  path: `${parentDir}/__init__.py`,
7484
7818
  content: reExports,
@@ -7489,6 +7823,14 @@ function generateModels$5(models, ctx) {
7489
7823
  }
7490
7824
  return files;
7491
7825
  }
7826
+ /**
7827
+ * Given a snake_case file name, return the IR model name that owns it.
7828
+ * Used to attribute dispatcher children (FooVariant, FooUnknown) to their
7829
+ * parent model when computing relocation re-exports.
7830
+ */
7831
+ function reverseLookupModelByFile(file, ctx) {
7832
+ for (const m of ctx.spec.models) if (fileName$2(m.name) === file) return m.name;
7833
+ }
7492
7834
  function collectTypingImports(ref, imports) {
7493
7835
  switch (ref.kind) {
7494
7836
  case "array":
@@ -7553,42 +7895,6 @@ function collectReachableEnumNames(ctx) {
7553
7895
  }
7554
7896
  return referencedEnums;
7555
7897
  }
7556
- function collectModelUsage(spec) {
7557
- const request = /* @__PURE__ */ new Set();
7558
- const response = /* @__PURE__ */ new Set();
7559
- for (const service of spec.services) for (const op of service.operations) {
7560
- const plan = planOperation(op);
7561
- if (plan.responseModelName) response.add(plan.responseModelName);
7562
- if (op.pagination?.itemType.kind === "model") response.add(op.pagination.itemType.name);
7563
- if (op.requestBody?.kind === "model") request.add(op.requestBody.name);
7564
- if (op.requestBody?.kind === "union") {
7565
- for (const variant of op.requestBody.variants ?? []) if (variant.kind === "model") request.add(variant.name);
7566
- }
7567
- }
7568
- const mixed = /* @__PURE__ */ new Set();
7569
- for (const name of request) if (response.has(name)) mixed.add(name);
7570
- return {
7571
- requestOnly: new Set([...request].filter((name) => !mixed.has(name))),
7572
- response: new Set([...response].filter((name) => !mixed.has(name))),
7573
- mixed
7574
- };
7575
- }
7576
- function compareAliasPriority(left, right, usage) {
7577
- const score = (name) => {
7578
- if (usage.response.has(name)) return 0;
7579
- if (usage.mixed.has(name)) return 1;
7580
- if (usage.requestOnly.has(name)) return 2;
7581
- return 3;
7582
- };
7583
- const diff = score(left) - score(right);
7584
- if (diff !== 0) return diff;
7585
- return left.localeCompare(right);
7586
- }
7587
- function canAliasModels(canonical, alias, usage) {
7588
- if (fileName$2(canonical) === fileName$2(alias)) return false;
7589
- if (usage.response.has(canonical) && usage.requestOnly.has(alias) || usage.response.has(alias) && usage.requestOnly.has(canonical)) return false;
7590
- return true;
7591
- }
7592
7898
  function isOptionalField(modelName, field, ctx) {
7593
7899
  if (!field.required || field.deprecated) return true;
7594
7900
  return false;
@@ -7609,6 +7915,71 @@ function isDateTimeType(ref) {
7609
7915
  if (ref.kind === "nullable") return isDateTimeType(ref.inner);
7610
7916
  return ref.kind === "primitive" && ref.type === "string" && ref.format === "date-time";
7611
7917
  }
7918
+ /**
7919
+ * If `field` is a discriminated-union (or nullable-wrapped discriminated-union)
7920
+ * field, return prelude statements that perform strict dispatch and an
7921
+ * expression for the `cls(...)` call. Returns null otherwise.
7922
+ *
7923
+ * Strict dispatch means: an unknown discriminator value raises ValueError
7924
+ * naming the parent class, field, observed value, and valid options. The
7925
+ * caller's `try/except` block converts that into `_raise_deserialize_error`.
7926
+ */
7927
+ function renderDiscriminatedUnionPrelude(field, pyFieldName, wireKey, parentClassName, isRequired) {
7928
+ let unionRef = null;
7929
+ let nullable = false;
7930
+ if (field.type.kind === "union" && field.type.discriminator?.mapping) unionRef = field.type;
7931
+ else if (field.type.kind === "nullable" && field.type.inner.kind === "union" && field.type.inner.discriminator?.mapping) {
7932
+ unionRef = field.type.inner;
7933
+ nullable = true;
7934
+ }
7935
+ if (!unionRef) return null;
7936
+ const mapping = unionRef.discriminator.mapping;
7937
+ const entries = Object.entries(mapping);
7938
+ if (entries.length === 0) return null;
7939
+ const discProp = unionRef.discriminator.property;
7940
+ const rawVar = `_${pyFieldName}_raw`;
7941
+ const dataVar = `_${pyFieldName}_data`;
7942
+ const typeVar = `_${pyFieldName}_disc`;
7943
+ const mapVar = `_${pyFieldName}_disc_map`;
7944
+ const clsVar = `_${pyFieldName}_cls`;
7945
+ const valueVar = `_${pyFieldName}_value`;
7946
+ const indent = " ";
7947
+ const dispatchBlock = (innerIndent) => {
7948
+ const lines = [];
7949
+ lines.push(`${innerIndent}${dataVar} = cast(Dict[str, Any], ${rawVar})`);
7950
+ lines.push(`${innerIndent}${typeVar} = cast(str, ${dataVar}.get("${discProp}"))`);
7951
+ lines.push(`${innerIndent}${mapVar}: Dict[str, Any] = {`);
7952
+ for (const [value, variantModelName] of entries) lines.push(`${innerIndent} "${value}": ${className$5(variantModelName)},`);
7953
+ lines.push(`${innerIndent}}`);
7954
+ lines.push(`${innerIndent}${clsVar} = ${mapVar}.get(${typeVar})`);
7955
+ lines.push(`${innerIndent}if ${clsVar} is None:`);
7956
+ lines.push(`${innerIndent} raise ValueError(`);
7957
+ lines.push(`${innerIndent} f"Unknown discriminator '${discProp}' for ${parentClassName}.${pyFieldName}: {${typeVar}!r}. "`);
7958
+ lines.push(`${innerIndent} f"Expected one of {sorted(${mapVar})}."`);
7959
+ lines.push(`${innerIndent} )`);
7960
+ return lines;
7961
+ };
7962
+ const prelude = [];
7963
+ if (isRequired && !nullable) {
7964
+ prelude.push(`${indent}${rawVar} = data["${wireKey}"]`);
7965
+ prelude.push(...dispatchBlock(indent));
7966
+ return {
7967
+ prelude,
7968
+ expr: `${clsVar}.from_dict(${dataVar})`
7969
+ };
7970
+ }
7971
+ const accessor = isRequired ? `data["${wireKey}"]` : `data.get("${wireKey}")`;
7972
+ prelude.push(`${indent}${rawVar} = ${accessor}`);
7973
+ prelude.push(`${indent}if ${rawVar} is None:`);
7974
+ prelude.push(`${indent} ${valueVar} = None`);
7975
+ prelude.push(`${indent}else:`);
7976
+ prelude.push(...dispatchBlock(indent + " "));
7977
+ prelude.push(`${indent} ${valueVar} = ${clsVar}.from_dict(${dataVar})`);
7978
+ return {
7979
+ prelude,
7980
+ expr: valueVar
7981
+ };
7982
+ }
7612
7983
  function deserializeField(ref, accessor, isRequired, walrusVar = "_v") {
7613
7984
  if (isDateTimeType(ref)) {
7614
7985
  if (isRequired) return `_parse_datetime(${accessor})`;
@@ -7639,17 +8010,6 @@ function deserializeField(ref, accessor, isRequired, walrusVar = "_v") {
7639
8010
  case "union": {
7640
8011
  const modelVariants = (ref.variants ?? []).filter((v) => v.kind === "model");
7641
8012
  const uniqueModels = [...new Set(modelVariants.map((v) => v.name))];
7642
- if (ref.discriminator && ref.discriminator.mapping) {
7643
- const entries = Object.entries(ref.discriminator.mapping);
7644
- if (entries.length > 0) {
7645
- const dispatchMap = entries.map(([value, modelName]) => `"${value}": ${className$5(modelName)}`).join(", ");
7646
- const dataExpr = isRequired ? accessor : walrusVar;
7647
- const dataCast = `cast(Dict[str, Any], ${dataExpr})`;
7648
- const branch = `(_disc.from_dict(${dataCast}) if (_disc := ${`{${dispatchMap}}.get(cast(str, ${dataCast}.get("${ref.discriminator.property}")))`}) is not None else ${dataExpr})`;
7649
- if (isRequired) return branch;
7650
- return `(${branch}) if (${walrusVar} := ${accessor}) is not None else None`;
7651
- }
7652
- }
7653
8013
  if (uniqueModels.length === 1) return deserializeField({
7654
8014
  kind: "model",
7655
8015
  name: uniqueModels[0]
@@ -7671,61 +8031,12 @@ function serializeField(ref, accessor) {
7671
8031
  case "union": {
7672
8032
  const modelVariants = (ref.variants ?? []).filter((v) => v.kind === "model");
7673
8033
  if ([...new Set(modelVariants.map((v) => v.name))].length === 1) return `${accessor}.to_dict()`;
7674
- if (ref.discriminator && ref.discriminator.mapping && modelVariants.length > 0) return `${accessor}.to_dict() if hasattr(${accessor}, "to_dict") else ${accessor}`;
8034
+ if (ref.discriminator && ref.discriminator.mapping && modelVariants.length > 0) return `${accessor}.to_dict()`;
7675
8035
  return accessor;
7676
8036
  }
7677
8037
  default: return accessor;
7678
8038
  }
7679
8039
  }
7680
- /**
7681
- * Build recursive structural hashes for all models.
7682
- *
7683
- * Model references are resolved to their own structural hash (bottom-up) and
7684
- * enum references are resolved to their value-set hash. This means
7685
- * structurally-identical model *trees* — like the dozens of per-event Context /
7686
- * ContextActor / ContextGoogleAnalyticsSession sub-models in the spec — get
7687
- * the same hash even though their IR names differ.
7688
- */
7689
- function buildRecursiveHashMap$1(models, enums) {
7690
- const modelByName = new Map(models.map((m) => [m.name, m]));
7691
- const hashCache = /* @__PURE__ */ new Map();
7692
- const visiting = /* @__PURE__ */ new Set();
7693
- const enumVH = /* @__PURE__ */ new Map();
7694
- for (const e of enums) enumVH.set(e.name, [...e.values].map((v) => String(v.value)).sort().join("|"));
7695
- function modelHash(name) {
7696
- const cached = hashCache.get(name);
7697
- if (cached != null) return cached;
7698
- if (visiting.has(name)) return `m:${name}`;
7699
- visiting.add(name);
7700
- const model = modelByName.get(name);
7701
- if (!model) {
7702
- visiting.delete(name);
7703
- return `m:${name}`;
7704
- }
7705
- const hash = [...model.fields].sort((a, b) => a.name.localeCompare(b.name)).map((f) => `${f.name}:${deepTypeHash(f.type)}:${f.required}`).join("|");
7706
- visiting.delete(name);
7707
- hashCache.set(name, hash);
7708
- return hash;
7709
- }
7710
- function deepTypeHash(ref) {
7711
- switch (ref.kind) {
7712
- case "primitive": return `p:${ref.type}${ref.format ? `:${ref.format}` : ""}`;
7713
- case "model": return `m:{${modelHash(ref.name)}}`;
7714
- case "enum": {
7715
- const vh = enumVH.get(ref.name);
7716
- return vh != null ? `e:{${vh}}` : `e:${ref.name}`;
7717
- }
7718
- case "array": return `a:${deepTypeHash(ref.items)}`;
7719
- case "nullable": return `n:${deepTypeHash(ref.inner)}`;
7720
- case "union": return `u:${(ref.variants ?? []).map((v) => deepTypeHash(v)).sort().join(",")}`;
7721
- case "map": return `d:${deepTypeHash(ref.valueType)}`;
7722
- case "literal": return `l:${String(ref.value)}`;
7723
- default: return "unknown";
7724
- }
7725
- }
7726
- for (const model of models) modelHash(model.name);
7727
- return hashCache;
7728
- }
7729
8040
  //#endregion
7730
8041
  //#region src/python/path-expression.ts
7731
8042
  /**
@@ -8031,7 +8342,9 @@ function emitMethodSignature(lines, op, plan, method, isAsync, specEnumNames, mo
8031
8342
  lines.push(" after: Optional[str] = None,");
8032
8343
  const orderParam = op.queryParams.find((p) => p.name === "order");
8033
8344
  const orderType = orderParam && orderParam.type.kind === "enum" ? mapTypeRefUnquoted(orderParam.type, specEnumNames, true) : "str";
8034
- lines.push(` order: Optional[${orderType}] = "desc",`);
8345
+ const orderDefaultRaw = orderParam?.default;
8346
+ const orderDefault = typeof orderDefaultRaw === "string" || typeof orderDefaultRaw === "number" || typeof orderDefaultRaw === "boolean" ? pythonLiteral(orderDefaultRaw) : "None";
8347
+ lines.push(` order: Optional[${orderType}] = ${orderDefault},`);
8035
8348
  for (const param of op.queryParams) {
8036
8349
  if ([
8037
8350
  "limit",
@@ -8578,7 +8891,8 @@ function generateResources$5(services, ctx) {
8578
8891
  if (enumImports.size > 0) lines.push(`from ${importPrefix}_types import RequestOptions, enum_value`);
8579
8892
  else lines.push(`from ${importPrefix}_types import RequestOptions`);
8580
8893
  const actualModelImports = [...modelImports];
8581
- const modelToServiceMap = assignModelsToServices(ctx.spec.models, ctx.spec.services, ctx.modelHints);
8894
+ const placement = computeSchemaPlacement(ctx.spec, ctx);
8895
+ const modelToServiceMap = new Map(placement.modelToService);
8582
8896
  for (const model of ctx.spec.models) if (model.discriminator) {
8583
8897
  const svc = modelToServiceMap.get(model.name);
8584
8898
  if (svc) modelToServiceMap.set(model.name + "Variant", svc);
@@ -8603,19 +8917,7 @@ function generateResources$5(services, ctx) {
8603
8917
  const unique = names.filter((n) => !localSet.has(n));
8604
8918
  for (const n of unique) lines.push(`from ${ctx.namespace}.${dirToModule(csDir)}.models.${fileName$2(n)} import ${className$5(n)}`);
8605
8919
  }
8606
- const enumToServiceMap = /* @__PURE__ */ new Map();
8607
- for (const e of ctx.spec.enums) for (const svc of ctx.spec.services) for (const op of svc.operations) {
8608
- const refs = /* @__PURE__ */ new Set();
8609
- const allTypeRefs = [
8610
- op.response,
8611
- ...op.requestBody ? [op.requestBody] : [],
8612
- ...op.pathParams.map((p) => p.type),
8613
- ...op.queryParams.map((p) => p.type),
8614
- ...op.headerParams.map((p) => p.type)
8615
- ];
8616
- for (const typeRef of allTypeRefs) for (const ref of collectEnumRefs(typeRef)) refs.add(ref);
8617
- if (refs.has(e.name) && !enumToServiceMap.has(e.name)) enumToServiceMap.set(e.name, svc.name);
8618
- }
8920
+ const enumToServiceMap = placement.enumToService;
8619
8921
  const localEnums = [];
8620
8922
  const crossServiceEnums = /* @__PURE__ */ new Map();
8621
8923
  for (const name of [...enumImports].sort()) {
@@ -9387,13 +9689,14 @@ function generateServiceTest$2(service, spec, ctx, accessPaths, resolvedOps) {
9387
9689
  if (!model) return true;
9388
9690
  return !isListWrapperModel(model);
9389
9691
  });
9390
- const modelToServiceMap = assignModelsToServices(spec.models, spec.services, ctx.modelHints);
9692
+ const placement = computeSchemaPlacement(spec, ctx);
9693
+ const modelToServiceMap = placement.modelToService;
9694
+ const enumToServiceMap = placement.enumToService;
9391
9695
  const mountDirMap = buildMountDirMap$1(ctx);
9392
9696
  const resolveModelDir = (modelName) => {
9393
9697
  const svc = modelToServiceMap.get(modelName);
9394
9698
  return svc ? mountDirMap.get(svc) ?? "common" : "common";
9395
9699
  };
9396
- const enumToServiceMap = assignEnumsToServices(spec.enums, spec.services);
9397
9700
  const resolveEnumDir = (enumName) => {
9398
9701
  const svc = enumToServiceMap.get(enumName);
9399
9702
  return svc ? mountDirMap.get(svc) ?? "common" : "common";
@@ -10237,7 +10540,7 @@ function generateModelRoundTripTests(spec, ctx) {
10237
10540
  for (const name of responseModelNames) requestOnlyModelNames.delete(name);
10238
10541
  const models = spec.models.filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m) && !requestOnlyModelNames.has(m.name));
10239
10542
  if (models.length === 0) return null;
10240
- const modelToService = assignModelsToServices(spec.models, spec.services, ctx.modelHints);
10543
+ const modelToService = computeSchemaPlacement(spec, ctx).originalModelToService;
10241
10544
  const roundTripDirMap = buildMountDirMap$1(ctx);
10242
10545
  const resolveDir = (irService) => irService ? roundTripDirMap.get(irService) ?? "common" : "common";
10243
10546
  const lines = [];
@@ -10798,7 +11101,8 @@ function generateFromArrayValue(ref, accessor) {
10798
11101
  const entries = Object.entries(ref.discriminator.mapping);
10799
11102
  if (entries.length > 0) {
10800
11103
  const arms = entries.map(([value, modelName]) => `'${value}' => ${className$4(modelName)}::fromArray(${accessor})`).join(", ");
10801
- return `match (${accessor}['${ref.discriminator.property}'] ?? null) { ${arms}, default => ${accessor} }`;
11104
+ const discProp = ref.discriminator.property;
11105
+ return `match (${accessor}['${discProp}'] ?? null) { ${arms}, ${`default => throw new \\UnexpectedValueException(sprintf('Unknown ${discProp}: %s', json_encode(${accessor}['${discProp}'] ?? null)))`} }`;
10802
11106
  }
10803
11107
  }
10804
11108
  const resolved = resolveDegenerateUnion(ref);
@@ -10854,6 +11158,11 @@ function generateToArrayValue(ref, accessor, nullable = false) {
10854
11158
  case "union": {
10855
11159
  const resolved = resolveDegenerateUnion(ref);
10856
11160
  if (resolved) return generateToArrayValue(resolved, accessor, nullable);
11161
+ if (ref.variants.every((v) => v.kind === "model")) return `${accessor}${ns}->toArray()`;
11162
+ if (ref.variants.some((v) => v.kind === "model" || v.kind === "enum")) {
11163
+ const summary = ref.variants.map((v) => v.kind === "model" ? `model:${v.name}` : v.kind === "enum" ? `enum:${v.name}` : v.kind).join(" | ");
11164
+ throw new Error(`[php emitter] Cannot generate toArray for heterogeneous union: ${summary}. Unions must be all-model or all-scalar; mixed and all-enum unions are not yet supported.`);
11165
+ }
10857
11166
  return accessor;
10858
11167
  }
10859
11168
  case "literal": return accessor;
@@ -11235,8 +11544,8 @@ function generateMethod$2(lines, op, service, ctx, modelMap, resolvedOp) {
11235
11544
  const phpName = fieldName$3(q.name);
11236
11545
  if (seenDocParams.has(phpName)) continue;
11237
11546
  seenDocParams.add(phpName);
11238
- const isNonNullableOrder = q.name === "order" && q.type.kind === "enum";
11239
- const nullSuffix = !q.required && !isNonNullableOrder && !docType.endsWith("|null") ? "|null" : "";
11547
+ const hasEnumDefault = q.default != null && q.type.kind === "enum";
11548
+ const nullSuffix = !q.required && !hasEnumDefault && !docType.endsWith("|null") ? "|null" : "";
11240
11549
  const prefix = q.deprecated ? "(deprecated) " : "";
11241
11550
  let desc = q.description ? ` ${prefix}${q.description}` : q.deprecated ? " (deprecated)" : "";
11242
11551
  if (q.default != null) desc += ` Defaults to ${JSON.stringify(q.default)}.`;
@@ -11458,15 +11767,11 @@ function buildMethodParams(op, plan, modelMap, ctx, hiddenParams) {
11458
11767
  if (usedNames.has(phpName)) continue;
11459
11768
  usedNames.add(phpName);
11460
11769
  if (q.required) required.push(`${phpType} $${phpName}`);
11461
- else if (q.name === "order") if (q.type.kind === "enum") {
11770
+ else if (q.default != null && q.type.kind === "enum") {
11462
11771
  const enumType = mapTypeRef$4(q.type, { qualified: true });
11463
- const caseName = toPascalCase("desc");
11772
+ const caseName = toPascalCase(String(q.default));
11464
11773
  optional.push(`${enumType} $${phpName} = ${enumType}::${caseName}`);
11465
11774
  } else {
11466
- const nullableType = phpType.startsWith("?") ? phpType : `?${phpType}`;
11467
- optional.push(`${nullableType} $${phpName} = 'desc'`);
11468
- }
11469
- else {
11470
11775
  const nullableType = phpType.startsWith("?") ? phpType : `?${phpType}`;
11471
11776
  optional.push(`${nullableType} $${phpName} = null`);
11472
11777
  }
@@ -11514,8 +11819,8 @@ function buildQueryArray(op, hiddenParams) {
11514
11819
  return op.queryParams.filter((q) => !hidden.has(q.name) && !groupedParams.has(q.name)).map((q) => {
11515
11820
  const phpName = fieldName$3(q.name);
11516
11821
  if (isEnumType(q.type)) {
11517
- const isNonNullableOrder = q.name === "order" && q.type.kind === "enum";
11518
- const nullsafe = q.required || isNonNullableOrder ? "" : "?";
11822
+ const hasEnumDefault = q.default != null && q.type.kind === "enum";
11823
+ const nullsafe = q.required || hasEnumDefault ? "" : "?";
11519
11824
  return `'${q.name}' => $${phpName}${nullsafe}->value,`;
11520
11825
  }
11521
11826
  return `'${q.name}' => $${phpName},`;
@@ -12611,6 +12916,8 @@ function generateModels$3(models, ctx) {
12611
12916
  lines.push("}");
12612
12917
  lines.push("");
12613
12918
  }
12919
+ const orderEnumType = detectSharedOrderEnum(ctx.spec.services);
12920
+ const orderGoType = orderEnumType ? `*${className$3(orderEnumType)}` : "*string";
12614
12921
  lines.push("// PaginationParams contains common pagination parameters for list operations.");
12615
12922
  lines.push("type PaginationParams struct {");
12616
12923
  lines.push(" // Before is a cursor for reverse pagination.");
@@ -12619,8 +12926,8 @@ function generateModels$3(models, ctx) {
12619
12926
  lines.push(" After *string `url:\"after,omitempty\" json:\"-\"`");
12620
12927
  lines.push(" // Limit is the maximum number of items to return per page.");
12621
12928
  lines.push(" Limit *int `url:\"limit,omitempty\" json:\"-\"`");
12622
- lines.push(" // Order is the sort order for results (asc or desc).");
12623
- lines.push(" Order *string `url:\"order,omitempty\" json:\"-\"`");
12929
+ lines.push(" // Order is the sort order for results.");
12930
+ lines.push(`\tOrder ${orderGoType} \`url:"order,omitempty" json:"-"\``);
12624
12931
  lines.push("}");
12625
12932
  lines.push("");
12626
12933
  files.push({
@@ -12700,6 +13007,38 @@ function lowerFirst$1(s) {
12700
13007
  return lowerFirstForDoc(s);
12701
13008
  }
12702
13009
  /**
13010
+ * If every paginated list operation's `order` query parameter $refs the same
13011
+ * top-level enum, return that enum's IR name. Otherwise return null. When
13012
+ * the spec is consistent this lifts `PaginationParams.Order` from `*string`
13013
+ * to `*PaginationOrder` (or whatever the spec calls it), giving callers
13014
+ * compile-time validation.
13015
+ *
13016
+ * We require strict consistency: if any operation uses a primitive string for
13017
+ * `order`, or two operations reference different enums, we conservatively
13018
+ * stay on `*string` so the shared struct doesn't lie about its accepted
13019
+ * values.
13020
+ */
13021
+ function detectSharedOrderEnum(services) {
13022
+ let candidate = null;
13023
+ let sawAny = false;
13024
+ for (const service of services) for (const op of service.operations) {
13025
+ if (!op.pagination) continue;
13026
+ const orderParam = op.queryParams.find((p) => p.name === "order");
13027
+ if (!orderParam) continue;
13028
+ sawAny = true;
13029
+ const enumName = unwrapEnumName(orderParam.type);
13030
+ if (!enumName) return null;
13031
+ if (candidate === null) candidate = enumName;
13032
+ else if (candidate !== enumName) return null;
13033
+ }
13034
+ return sawAny ? candidate : null;
13035
+ }
13036
+ function unwrapEnumName(ref) {
13037
+ if (ref.kind === "enum") return ref.name;
13038
+ if (ref.kind === "nullable") return unwrapEnumName(ref.inner);
13039
+ return null;
13040
+ }
13041
+ /**
12703
13042
  * Extract a deprecation reason from a field description.
12704
13043
  * Looks for patterns like "Use X instead", "Replaced by Y", etc.
12705
13044
  * Falls back to a generic message if no migration guidance is found.
@@ -15558,15 +15897,22 @@ function generateEnums$2(enums, ctx) {
15558
15897
  lines.push(" [STJS.JsonConverter(typeof(WorkOSStringEnumConverterFactory))]");
15559
15898
  lines.push(` public enum ${typeName}`);
15560
15899
  lines.push(" {");
15561
- lines.push(` [EnumMember(Value = "unknown")]`);
15562
- lines.push(` Unknown,`);
15563
- lines.push("");
15900
+ const defaultMatch = enumDef.default !== void 0 ? uniqueValues.find((v) => v.value === enumDef.default) : void 0;
15901
+ const orderedValues = defaultMatch ? [defaultMatch, ...uniqueValues.filter((v) => v !== defaultMatch)] : uniqueValues;
15564
15902
  const usedNames = /* @__PURE__ */ new Set();
15565
- usedNames.add("Unknown");
15566
15903
  const usedWireValues = /* @__PURE__ */ new Set();
15567
- usedWireValues.add("unknown");
15568
- for (let i = 0; i < uniqueValues.length; i++) {
15569
- const v = uniqueValues[i];
15904
+ if (defaultMatch) {
15905
+ usedNames.add("Unknown");
15906
+ usedWireValues.add("unknown");
15907
+ } else {
15908
+ lines.push(` [EnumMember(Value = "unknown")]`);
15909
+ lines.push(` Unknown,`);
15910
+ lines.push("");
15911
+ usedNames.add("Unknown");
15912
+ usedWireValues.add("unknown");
15913
+ }
15914
+ for (let i = 0; i < orderedValues.length; i++) {
15915
+ const v = orderedValues[i];
15570
15916
  if (usedWireValues.has(String(v.value))) continue;
15571
15917
  usedWireValues.add(String(v.value));
15572
15918
  let memberName = className$2(String(v.value));
@@ -15582,8 +15928,13 @@ function generateEnums$2(enums, ctx) {
15582
15928
  lines.push(` [System.Obsolete("${msg}")]`);
15583
15929
  }
15584
15930
  lines.push(` [EnumMember(Value = "${v.value}")]`);
15585
- const comma = i < uniqueValues.length - 1 ? "," : ",";
15586
- lines.push(` ${memberName}${comma}`);
15931
+ const ordinal = defaultMatch ? ` = ${i}` : "";
15932
+ lines.push(` ${memberName}${ordinal},`);
15933
+ }
15934
+ if (defaultMatch) {
15935
+ lines.push("");
15936
+ lines.push(` [EnumMember(Value = "unknown")]`);
15937
+ lines.push(` Unknown = 99,`);
15587
15938
  }
15588
15939
  lines.push(" }");
15589
15940
  lines.push("}");
@@ -21415,7 +21766,7 @@ function emitMethod(args) {
21415
21766
  const n = safeParamName(q.name);
21416
21767
  if (seenParamNames.has(n)) continue;
21417
21768
  seenParamNames.add(n);
21418
- const defaultVal = q.name === "order" ? rubyStringLit("desc") : "nil";
21769
+ const defaultVal = q.default != null ? rubyDefaultLiteral(q.default) : "nil";
21419
21770
  sigParts.push(`${n}: ${defaultVal}`);
21420
21771
  }
21421
21772
  for (const group of op.parameterGroups ?? []) {
@@ -21795,6 +22146,14 @@ function buildYardDoc(op, pathParams, queryParams, bodyFields, hiddenParams, bod
21795
22146
  function rubyStringLit(s) {
21796
22147
  return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
21797
22148
  }
22149
+ /** Render an arbitrary spec-default value as a Ruby literal for a method-arg default. */
22150
+ function rubyDefaultLiteral(value) {
22151
+ if (typeof value === "string") return rubyStringLit(value);
22152
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "nil";
22153
+ if (typeof value === "boolean") return value ? "true" : "false";
22154
+ if (value === null) return "nil";
22155
+ return "nil";
22156
+ }
21798
22157
  /**
21799
22158
  * Build a Ruby double-quoted string expression for the `else raise ArgumentError`
21800
22159
  * arm of a parameter-group dispatcher. Lists the expected variant classes and
@@ -22800,4 +23159,4 @@ const workosEmittersPlugin = {
22800
23159
  //#endregion
22801
23160
  export { nodeEmitter as _, rustExtractor as a, pythonExtractor as c, rubyEmitter as d, kotlinEmitter as f, pythonEmitter as g, phpEmitter as h, kotlinExtractor as i, rubyExtractor as l, goEmitter as m, elixirExtractor as n, goExtractor as o, dotnetEmitter as p, dotnetExtractor as r, phpExtractor as s, workosEmittersPlugin as t, nodeExtractor as u };
22802
23161
 
22803
- //# sourceMappingURL=plugin-Dh9JSScr.mjs.map
23162
+ //# sourceMappingURL=plugin-DW3cnedr.mjs.map