@workos/oagen-emitters 0.13.0 → 0.14.1

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.
@@ -140,7 +140,11 @@ function renderField(field: Field, rustField: string, modelName: string, registr
140
140
  function renderModelsBarrel(modules: string[]): string {
141
141
  const sorted = [...new Set(modules)].sort();
142
142
  const lines: string[] = [];
143
- for (const m of sorted) lines.push(`pub mod ${m};`);
143
+ // Declare the modules privately so `pub use crate::models::*` in lib.rs only
144
+ // re-exports the struct names, not the module names themselves. Otherwise a
145
+ // module like `models::organization_membership` collides with the same-named
146
+ // `resources::organization_membership` when both barrels are glob-re-exported.
147
+ for (const m of sorted) lines.push(`mod ${m};`);
144
148
  lines.push('');
145
149
  for (const m of sorted) lines.push(`pub use ${m}::*;`);
146
150
  return lines.join('\n') + '\n';
@@ -1317,7 +1317,10 @@ function renderResourcesBarrel(exports: { module: string; struct: string }[]): s
1317
1317
  unique.sort((a, b) => a.module.localeCompare(b.module));
1318
1318
 
1319
1319
  const lines: string[] = [];
1320
- for (const { module } of unique) lines.push(`pub mod ${module};`);
1320
+ // Declare modules privately see the matching comment in `models.ts`.
1321
+ // `pub mod resources::organization_membership` would collide with the
1322
+ // same-named module re-exported via `pub use models::*` in lib.rs.
1323
+ for (const { module } of unique) lines.push(`mod ${module};`);
1321
1324
  lines.push('');
1322
1325
  for (const { module, struct } of unique) lines.push(`pub use ${module}::${struct};`);
1323
1326
  return lines.join('\n') + '\n';
@@ -1,10 +1,31 @@
1
- import type { Model, Field, TypeRef, Enum } from '@workos/oagen';
2
- import { toSnakeCase } from '@workos/oagen';
1
+ import type { Model, Field, TypeRef, Enum, Service } from '@workos/oagen';
2
+ import { toSnakeCase, toUpperSnakeCase, walkTypeRef } from '@workos/oagen';
3
3
  import { readFileSync, existsSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
5
  // @ts-ignore -- js-yaml has no type declarations in this project
6
6
  import { load as yamlLoad } from 'js-yaml';
7
7
 
8
+ /**
9
+ * Collect model names referenced as the return type of any non-paginated
10
+ * operation. The list-wrapper skip rule below assumes a wrapper is always
11
+ * replaced by the SDK's pagination machinery — but a few endpoints
12
+ * (e.g. `GET /vault/v1/kv/{id}/versions`) have a list-envelope response
13
+ * shape with no pagination params, so the parser leaves them as a plain
14
+ * model reference. We must still emit those wrappers as regular models;
15
+ * otherwise the generated resource code references an undefined name.
16
+ */
17
+ export function collectNonPaginatedResponseModelNames(services: Service[]): Set<string> {
18
+ const names = new Set<string>();
19
+ for (const service of services) {
20
+ for (const op of service.operations) {
21
+ if (op.pagination) continue;
22
+ walkTypeRef(op.response, { model: (r) => names.add(r.name) });
23
+ for (const sr of op.successResponses ?? []) walkTypeRef(sr.type, { model: (r) => names.add(r.name) });
24
+ }
25
+ }
26
+ return names;
27
+ }
28
+
8
29
  /**
9
30
  * Detect whether a model is a list wrapper -- the standard paginated
10
31
  * list envelope with `data` (array), `list_metadata`, and optionally `object: 'list'`.
@@ -83,7 +104,7 @@ function discoverSpecPath(): string | null {
83
104
  let _rawSpecCache: Record<string, any> | null = null;
84
105
  let _rawSpecLoaded = false;
85
106
 
86
- function loadRawSpec(): Record<string, any> | null {
107
+ export function loadRawSpec(): Record<string, any> | null {
87
108
  if (_rawSpecLoaded) return _rawSpecCache;
88
109
  _rawSpecLoaded = true;
89
110
  const specPath = discoverSpecPath();
@@ -114,7 +135,10 @@ function lookupRawSchema(name: string): Record<string, any> | null {
114
135
  */
115
136
  interface SyntheticCollector {
116
137
  models: Model[];
117
- enums: Array<{ name: string; values: Array<{ value: string; description?: string }> }>;
138
+ enums: Array<{
139
+ name: string;
140
+ values: Array<{ value: string; description?: string }>;
141
+ }>;
118
142
  /** Track names already used to avoid duplicates. */
119
143
  usedNames: Set<string>;
120
144
  }
@@ -500,7 +524,7 @@ export function detectDiscriminators(models: Model[]): Model[] {
500
524
  * Returns a new array of enriched models (original models are not mutated).
501
525
  * Synthetic enums are stored internally; retrieve them via `getSyntheticEnums()`.
502
526
  */
503
- export function enrichModelsFromSpec(models: Model[]): Model[] {
527
+ export function enrichModelsFromSpec(models: Model[], enums: Enum[] = []): Model[] {
504
528
  const spec = loadRawSpec();
505
529
  if (!spec) {
506
530
  _lastSyntheticEnums = [];
@@ -515,6 +539,17 @@ export function enrichModelsFromSpec(models: Model[]): Model[] {
515
539
  collector.usedNames.add(m.name);
516
540
  collector.usedNames.add(toSnakeCase(m.name));
517
541
  }
542
+ // Seed existing IR enum names too. The parser already emits inline enums
543
+ // like `DataIntegrationAccessTokenResponseError` (PascalCase); without this
544
+ // seed, the synthetic path would emit a sibling enum named
545
+ // `DataIntegrationAccessTokenResponse_error`, and language emitters that
546
+ // PascalCase-normalize identifiers (Ruby, Go, PHP, Python) would collapse
547
+ // both onto the same class/file path — the second overwrites the first
548
+ // with a `X = X` self-alias.
549
+ for (const e of enums) {
550
+ collector.usedNames.add(e.name);
551
+ collector.usedNames.add(toSnakeCase(e.name));
552
+ }
518
553
 
519
554
  const enriched = models.map((model) => {
520
555
  const rawSchema = lookupRawSchema(model.name);
@@ -582,10 +617,17 @@ export function enrichModelsFromSpec(models: Model[]): Model[] {
582
617
  return modified ? { ...model, fields: newFields } : model;
583
618
  });
584
619
 
585
- // Convert synthetic enum collector entries to proper Enum objects
620
+ // Convert synthetic enum collector entries to proper Enum objects. PHP's
621
+ // emitter (and others built on top of `EnumValue.name`) crash when this
622
+ // field is missing, so derive it from the value via the same upper-snake
623
+ // transform the parser uses for declared enums.
586
624
  _lastSyntheticEnums = collector.enums.map((e) => ({
587
625
  name: e.name,
588
- values: e.values.map((v) => ({ value: v.value, description: v.description })),
626
+ values: e.values.map((v) => ({
627
+ name: toUpperSnakeCase(String(v.value)),
628
+ value: v.value,
629
+ description: v.description,
630
+ })),
589
631
  })) as Enum[];
590
632
 
591
633
  // Append synthetic models, skipping those whose snake_case name collides
@@ -110,7 +110,7 @@ describe('rust/models', () => {
110
110
  const event = files.find((f) => f.path === 'src/models/event.rs')!;
111
111
  expect(event.content).toContain('pub payload: EventPayloadOneOf,');
112
112
  const barrel = files.find((f) => f.path === 'src/models/mod.rs')!;
113
- expect(barrel.content).toContain('pub mod _unions;');
113
+ expect(barrel.content).toContain('mod _unions;');
114
114
  // The _unions.rs file is rendered by generateClient (the final structural
115
115
  // pass) so resource-side body unions can join the same registry.
116
116
  const clientFiles = generateClient(emptySpec, ctx, registry);
@@ -170,8 +170,8 @@ describe('rust/models', () => {
170
170
  ];
171
171
  const files = generateModels(models, ctx, new UnionRegistry());
172
172
  const barrel = files.find((f) => f.path === 'src/models/mod.rs')!;
173
- expect(barrel.content).toContain('pub mod alpha;');
174
- expect(barrel.content).toContain('pub mod beta;');
173
+ expect(barrel.content).toContain('mod alpha;');
174
+ expect(barrel.content).toContain('mod beta;');
175
175
  expect(barrel.content).toContain('pub use alpha::*;');
176
176
  });
177
177
  });