@workos/oagen-emitters 0.18.3 → 0.19.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.
Files changed (67) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/{plugin-1ckLMpgo.mjs → plugin-BXDPA9pJ.mjs} +581 -172
  6. package/dist/plugin-BXDPA9pJ.mjs.map +1 -0
  7. package/dist/plugin.mjs +1 -1
  8. package/docs/sdk-architecture/rust.md +2 -2
  9. package/package.json +5 -5
  10. package/src/dotnet/enums.ts +11 -5
  11. package/src/dotnet/fixtures.ts +5 -2
  12. package/src/dotnet/index.ts +2 -1
  13. package/src/dotnet/models.ts +41 -10
  14. package/src/dotnet/naming.ts +10 -0
  15. package/src/dotnet/resources.ts +3 -3
  16. package/src/dotnet/tests.ts +8 -4
  17. package/src/go/fixtures.ts +4 -2
  18. package/src/go/index.ts +4 -0
  19. package/src/go/models.ts +4 -2
  20. package/src/go/naming.ts +10 -0
  21. package/src/go/resources.ts +22 -9
  22. package/src/go/tests.ts +3 -3
  23. package/src/kotlin/enums.ts +21 -11
  24. package/src/kotlin/index.ts +2 -1
  25. package/src/kotlin/models.ts +24 -9
  26. package/src/kotlin/naming.ts +11 -0
  27. package/src/kotlin/resources.ts +2 -2
  28. package/src/kotlin/tests.ts +7 -3
  29. package/src/node/enums.ts +8 -5
  30. package/src/node/field-plan.ts +3 -3
  31. package/src/node/index.ts +2 -1
  32. package/src/node/models.ts +69 -22
  33. package/src/node/naming.ts +10 -0
  34. package/src/node/options.ts +45 -1
  35. package/src/node/resources.ts +67 -18
  36. package/src/node/tests.ts +302 -31
  37. package/src/php/enums.ts +18 -5
  38. package/src/php/index.ts +13 -4
  39. package/src/php/models.ts +22 -10
  40. package/src/php/naming.ts +10 -0
  41. package/src/php/resources.ts +6 -4
  42. package/src/php/tests.ts +17 -5
  43. package/src/python/enums.ts +39 -28
  44. package/src/python/fixtures.ts +4 -3
  45. package/src/python/index.ts +2 -1
  46. package/src/python/models.ts +39 -24
  47. package/src/python/naming.ts +10 -0
  48. package/src/python/resources.ts +3 -3
  49. package/src/python/tests.ts +14 -9
  50. package/src/ruby/enums.ts +28 -19
  51. package/src/ruby/index.ts +2 -1
  52. package/src/ruby/models.ts +33 -19
  53. package/src/ruby/naming.ts +10 -0
  54. package/src/ruby/rbi.ts +20 -7
  55. package/src/ruby/resources.ts +2 -2
  56. package/src/ruby/tests.ts +6 -3
  57. package/src/rust/enums.ts +9 -1
  58. package/src/rust/index.ts +2 -1
  59. package/src/rust/models.ts +100 -15
  60. package/src/rust/naming.ts +10 -0
  61. package/src/rust/resources.ts +14 -3
  62. package/src/rust/tests.ts +2 -2
  63. package/src/shared/file-header.ts +13 -0
  64. package/src/shared/resolved-ops.ts +47 -0
  65. package/test/rust/models.test.ts +49 -0
  66. package/test/shared/synthetic-enum-seed.test.ts +79 -0
  67. package/dist/plugin-1ckLMpgo.mjs.map +0 -1
@@ -119,6 +119,55 @@ describe('rust/models', () => {
119
119
  expect(unions.content).toContain('pub enum EventPayloadOneOf {');
120
120
  });
121
121
 
122
+ it('relaxes the discriminator field on internally-tagged union variants', () => {
123
+ // Mirrors the ApiKey `owner` union: serde's `#[serde(tag = "type")]`
124
+ // consumes `type` as the enum tag and strips it from the variant body, so
125
+ // a *required* `type` field on the variant struct can never be satisfied
126
+ // ("missing field `type`"). The emitter must mark it default+skip_serializing.
127
+ const owner = (name: string, extra: Model['fields'] = []): Model => ({
128
+ name,
129
+ fields: [
130
+ { name: 'type', type: { kind: 'primitive', type: 'string' }, required: true },
131
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
132
+ ...extra,
133
+ ],
134
+ });
135
+ const models: Model[] = [
136
+ {
137
+ name: 'ApiKey',
138
+ fields: [
139
+ {
140
+ name: 'owner',
141
+ type: {
142
+ kind: 'union',
143
+ variants: [
144
+ { kind: 'model', name: 'ApiKeyOwner' },
145
+ { kind: 'model', name: 'UserApiKeyOwner' },
146
+ ],
147
+ discriminator: {
148
+ property: 'type',
149
+ mapping: { organization: 'ApiKeyOwner', user: 'UserApiKeyOwner' },
150
+ },
151
+ },
152
+ required: true,
153
+ },
154
+ ],
155
+ },
156
+ owner('ApiKeyOwner'),
157
+ owner('UserApiKeyOwner', [
158
+ { name: 'organization_id', type: { kind: 'primitive', type: 'string' }, required: true },
159
+ ]),
160
+ ];
161
+ const files = generateModels(models, ctx, new UnionRegistry());
162
+ for (const mod of ['api_key_owner', 'user_api_key_owner']) {
163
+ const f = files.find((x) => x.path === `src/models/${mod}.rs`)!;
164
+ expect(f.content).toContain('#[serde(rename = "type", default, skip_serializing)]');
165
+ expect(f.content).toContain('pub type_: String,');
166
+ // The non-discriminator field is untouched.
167
+ expect(f.content).toContain('pub id: String,');
168
+ }
169
+ });
170
+
122
171
  it('documents Field.default as a "Defaults to" doc comment', () => {
123
172
  const models: Model[] = [
124
173
  {
@@ -0,0 +1,79 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import type { Enum, Model } from '@workos/oagen';
6
+ import { enrichModelsFromSpec, getSyntheticEnums } from '../../src/shared/model-utils.js';
7
+
8
+ // Regression: an inline oneOf enum whose synthetic name (`Parent_field`)
9
+ // snake-collapses onto an existing IR enum must NOT spawn a duplicate
10
+ // synthetic. `DataIntegrationAccessTokenResponse.error` (-> synthetic
11
+ // `DataIntegrationAccessTokenResponse_error`) and the parser-emitted IR enum
12
+ // `DataIntegrationAccessTokenResponseError` both snake to
13
+ // `data_integration_access_token_response_error`. When both exist, every
14
+ // PascalCase-normalizing emitter (PHP, Ruby, Go, ...) collapses them onto the
15
+ // SAME file path, and which one wins is decided by array order — which differs
16
+ // between a full and a scoped (`--services`) generation, yielding a
17
+ // non-deterministic enum-case order. Seeding `enrichModelsFromSpec` with the
18
+ // IR enum names suppresses the duplicate so the real enum always wins.
19
+ const SPEC = {
20
+ openapi: '3.0.0',
21
+ info: { title: 'fixture', version: '1.0.0' },
22
+ paths: {},
23
+ components: {
24
+ schemas: {
25
+ DataIntegrationAccessTokenResponse: {
26
+ oneOf: [
27
+ {
28
+ type: 'object',
29
+ properties: {
30
+ error: {
31
+ type: 'string',
32
+ enum: ['not_installed', 'needs_reauthorization'],
33
+ },
34
+ },
35
+ },
36
+ ],
37
+ },
38
+ },
39
+ },
40
+ };
41
+
42
+ const SYNTHETIC_NAME = 'DataIntegrationAccessTokenResponse_error';
43
+ const models: Model[] = [{ name: 'DataIntegrationAccessTokenResponse', fields: [] }];
44
+ const collidingEnum: Enum = {
45
+ name: 'DataIntegrationAccessTokenResponseError',
46
+ values: [
47
+ { name: 'NOT_INSTALLED', value: 'not_installed' },
48
+ { name: 'NEEDS_REAUTHORIZATION', value: 'needs_reauthorization' },
49
+ ],
50
+ };
51
+
52
+ let dir: string;
53
+
54
+ beforeAll(() => {
55
+ dir = mkdtempSync(join(tmpdir(), 'synthetic-enum-seed-'));
56
+ const specPath = join(dir, 'spec.yaml');
57
+ // `loadRawSpec` accepts JSON too (js-yaml parses JSON), so write JSON.
58
+ writeFileSync(specPath, JSON.stringify(SPEC));
59
+ process.env.OPENAPI_SPEC_PATH = specPath;
60
+ });
61
+
62
+ afterAll(() => {
63
+ delete process.env.OPENAPI_SPEC_PATH;
64
+ if (dir) rmSync(dir, { recursive: true, force: true });
65
+ });
66
+
67
+ describe('enrichModelsFromSpec — synthetic enum seed', () => {
68
+ it('emits the inline synthetic enum when no IR enum names are seeded', () => {
69
+ enrichModelsFromSpec(models);
70
+ const names = getSyntheticEnums().map((e) => e.name);
71
+ expect(names).toContain(SYNTHETIC_NAME);
72
+ });
73
+
74
+ it('suppresses the duplicate synthetic when the colliding IR enum is seeded', () => {
75
+ enrichModelsFromSpec(models, [collidingEnum]);
76
+ const names = getSyntheticEnums().map((e) => e.name);
77
+ expect(names).not.toContain(SYNTHETIC_NAME);
78
+ });
79
+ });