@workos/oagen-emitters 0.2.0 → 0.3.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 (110) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.oxfmtrc.json +8 -1
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +129 -0
  6. package/dist/index.d.mts +10 -1
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +11943 -2728
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +298 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +17 -3
  18. package/smoke/sdk-elixir.ts +17 -3
  19. package/smoke/sdk-go.ts +137 -46
  20. package/smoke/sdk-kotlin.ts +23 -4
  21. package/smoke/sdk-node.ts +15 -3
  22. package/smoke/sdk-php.ts +28 -26
  23. package/smoke/sdk-python.ts +5 -2
  24. package/smoke/sdk-ruby.ts +17 -3
  25. package/smoke/sdk-rust.ts +16 -3
  26. package/src/go/client.ts +141 -0
  27. package/src/go/enums.ts +196 -0
  28. package/src/go/fixtures.ts +212 -0
  29. package/src/go/index.ts +81 -0
  30. package/src/go/manifest.ts +36 -0
  31. package/src/go/models.ts +254 -0
  32. package/src/go/naming.ts +191 -0
  33. package/src/go/resources.ts +827 -0
  34. package/src/go/tests.ts +751 -0
  35. package/src/go/type-map.ts +82 -0
  36. package/src/go/wrappers.ts +261 -0
  37. package/src/index.ts +3 -0
  38. package/src/node/client.ts +167 -122
  39. package/src/node/enums.ts +13 -4
  40. package/src/node/errors.ts +42 -233
  41. package/src/node/field-plan.ts +726 -0
  42. package/src/node/fixtures.ts +15 -5
  43. package/src/node/index.ts +65 -16
  44. package/src/node/models.ts +264 -96
  45. package/src/node/naming.ts +52 -25
  46. package/src/node/resources.ts +621 -172
  47. package/src/node/sdk-errors.ts +41 -0
  48. package/src/node/tests.ts +71 -27
  49. package/src/node/type-map.ts +4 -2
  50. package/src/node/utils.ts +56 -64
  51. package/src/node/wrappers.ts +151 -0
  52. package/src/php/client.ts +171 -0
  53. package/src/php/enums.ts +67 -0
  54. package/src/php/errors.ts +9 -0
  55. package/src/php/fixtures.ts +181 -0
  56. package/src/php/index.ts +96 -0
  57. package/src/php/manifest.ts +36 -0
  58. package/src/php/models.ts +310 -0
  59. package/src/php/naming.ts +298 -0
  60. package/src/php/resources.ts +561 -0
  61. package/src/php/tests.ts +533 -0
  62. package/src/php/type-map.ts +90 -0
  63. package/src/php/utils.ts +18 -0
  64. package/src/php/wrappers.ts +151 -0
  65. package/src/python/client.ts +337 -0
  66. package/src/python/enums.ts +313 -0
  67. package/src/python/fixtures.ts +196 -0
  68. package/src/python/index.ts +95 -0
  69. package/src/python/manifest.ts +38 -0
  70. package/src/python/models.ts +688 -0
  71. package/src/python/naming.ts +209 -0
  72. package/src/python/resources.ts +1322 -0
  73. package/src/python/tests.ts +1335 -0
  74. package/src/python/type-map.ts +93 -0
  75. package/src/python/wrappers.ts +191 -0
  76. package/src/shared/model-utils.ts +255 -0
  77. package/src/shared/naming-utils.ts +107 -0
  78. package/src/shared/non-spec-services.ts +54 -0
  79. package/src/shared/resolved-ops.ts +109 -0
  80. package/src/shared/wrapper-utils.ts +59 -0
  81. package/test/go/client.test.ts +92 -0
  82. package/test/go/enums.test.ts +132 -0
  83. package/test/go/errors.test.ts +9 -0
  84. package/test/go/models.test.ts +265 -0
  85. package/test/go/resources.test.ts +408 -0
  86. package/test/go/tests.test.ts +143 -0
  87. package/test/node/client.test.ts +199 -94
  88. package/test/node/enums.test.ts +75 -3
  89. package/test/node/errors.test.ts +2 -41
  90. package/test/node/models.test.ts +109 -20
  91. package/test/node/naming.test.ts +37 -4
  92. package/test/node/resources.test.ts +662 -30
  93. package/test/node/serializers.test.ts +36 -7
  94. package/test/node/type-map.test.ts +11 -0
  95. package/test/php/client.test.ts +94 -0
  96. package/test/php/enums.test.ts +173 -0
  97. package/test/php/errors.test.ts +9 -0
  98. package/test/php/models.test.ts +497 -0
  99. package/test/php/resources.test.ts +644 -0
  100. package/test/php/tests.test.ts +118 -0
  101. package/test/python/client.test.ts +200 -0
  102. package/test/python/enums.test.ts +228 -0
  103. package/test/python/errors.test.ts +16 -0
  104. package/test/python/manifest.test.ts +74 -0
  105. package/test/python/models.test.ts +716 -0
  106. package/test/python/resources.test.ts +617 -0
  107. package/test/python/tests.test.ts +202 -0
  108. package/src/node/common.ts +0 -273
  109. package/src/node/config.ts +0 -71
  110. package/src/node/serializers.ts +0 -744
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { generateSerializers } from '../../src/node/serializers.js';
2
+ import { generateSerializers } from '../../src/node/models.js';
3
3
  import type { EmitterContext, ApiSpec, Model, Service } from '@workos/oagen';
4
+ import { defaultSdkBehavior } from '@workos/oagen';
4
5
 
5
6
  const emptySpec: ApiSpec = {
6
7
  name: 'Test',
@@ -9,6 +10,7 @@ const emptySpec: ApiSpec = {
9
10
  services: [],
10
11
  models: [],
11
12
  enums: [],
13
+ sdk: defaultSdkBehavior(),
12
14
  };
13
15
 
14
16
  const ctx: EmitterContext = {
@@ -30,7 +32,13 @@ describe('generateSerializers', () => {
30
32
  name: 'getOrganization',
31
33
  httpMethod: 'get',
32
34
  path: '/organizations/{id}',
33
- pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
35
+ pathParams: [
36
+ {
37
+ name: 'id',
38
+ type: { kind: 'primitive', type: 'string' },
39
+ required: true,
40
+ },
41
+ ],
34
42
  queryParams: [],
35
43
  headerParams: [],
36
44
  response: { kind: 'model', name: 'Organization' },
@@ -90,7 +98,13 @@ describe('generateSerializers', () => {
90
98
  name: 'getOrganization',
91
99
  httpMethod: 'get',
92
100
  path: '/organizations/{id}',
93
- pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
101
+ pathParams: [
102
+ {
103
+ name: 'id',
104
+ type: { kind: 'primitive', type: 'string' },
105
+ required: true,
106
+ },
107
+ ],
94
108
  queryParams: [],
95
109
  headerParams: [],
96
110
  response: { kind: 'model', name: 'Organization' },
@@ -151,7 +165,13 @@ describe('generateSerializers', () => {
151
165
  name: 'getOrganization',
152
166
  httpMethod: 'get',
153
167
  path: '/organizations/{id}',
154
- pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
168
+ pathParams: [
169
+ {
170
+ name: 'id',
171
+ type: { kind: 'primitive', type: 'string' },
172
+ required: true,
173
+ },
174
+ ],
155
175
  queryParams: [],
156
176
  headerParams: [],
157
177
  response: { kind: 'model', name: 'Organization' },
@@ -303,12 +323,18 @@ describe('generateSerializers', () => {
303
323
  fields: [
304
324
  {
305
325
  name: 'before',
306
- type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
326
+ type: {
327
+ kind: 'nullable',
328
+ inner: { kind: 'primitive', type: 'string' },
329
+ },
307
330
  required: false,
308
331
  },
309
332
  {
310
333
  name: 'after',
311
- type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
334
+ type: {
335
+ kind: 'nullable',
336
+ inner: { kind: 'primitive', type: 'string' },
337
+ },
312
338
  required: false,
313
339
  },
314
340
  ],
@@ -375,7 +401,10 @@ describe('generateSerializers', () => {
375
401
  },
376
402
  {
377
403
  name: 'data',
378
- type: { kind: 'array', items: { kind: 'model', name: 'Connection' } },
404
+ type: {
405
+ kind: 'array',
406
+ items: { kind: 'model', name: 'Connection' },
407
+ },
379
408
  required: true,
380
409
  },
381
410
  {
@@ -60,6 +60,17 @@ describe('mapTypeRef', () => {
60
60
  expect(mapTypeRef(ref)).toBe('string | number');
61
61
  });
62
62
 
63
+ it('deduplicates union variants', () => {
64
+ const ref: TypeRef = {
65
+ kind: 'union',
66
+ variants: [
67
+ { kind: 'model', name: 'AuthenticationFactorTotp' },
68
+ { kind: 'model', name: 'AuthenticationFactorTotp' },
69
+ ],
70
+ };
71
+ expect(mapTypeRef(ref)).toBe('AuthenticationFactorTotp');
72
+ });
73
+
63
74
  it('maps map type', () => {
64
75
  const ref: TypeRef = {
65
76
  kind: 'map',
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { EmitterContext, ApiSpec, Service, Model } from '@workos/oagen';
3
+ import { defaultSdkBehavior } from '@workos/oagen';
4
+ import { generateClient } from '../../src/php/client.js';
5
+
6
+ const models: Model[] = [
7
+ {
8
+ name: 'Organization',
9
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
10
+ },
11
+ ];
12
+
13
+ const services: Service[] = [
14
+ {
15
+ name: 'Organizations',
16
+ operations: [
17
+ {
18
+ name: 'listOrganizations',
19
+ httpMethod: 'get',
20
+ path: '/organizations',
21
+ pathParams: [],
22
+ queryParams: [],
23
+ headerParams: [],
24
+ response: { kind: 'model', name: 'Organization' },
25
+ errors: [],
26
+ injectIdempotencyKey: false,
27
+ },
28
+ ],
29
+ },
30
+ ];
31
+
32
+ const emptySpec: ApiSpec = {
33
+ name: 'Test',
34
+ version: '1.0.0',
35
+ baseUrl: 'https://api.example.com',
36
+ services,
37
+ models,
38
+ enums: [],
39
+ sdk: defaultSdkBehavior(),
40
+ };
41
+
42
+ const ctx: EmitterContext = {
43
+ namespace: 'workos',
44
+ namespacePascal: 'WorkOS',
45
+ spec: emptySpec,
46
+ };
47
+
48
+ describe('generateClient', () => {
49
+ it('only generates the main client file', () => {
50
+ const result = generateClient(emptySpec, ctx);
51
+
52
+ expect(result).toHaveLength(1);
53
+ expect(result[0].path).toBe('lib/WorkOS.php');
54
+ });
55
+
56
+ it('generates main client class with namespace', () => {
57
+ const result = generateClient(emptySpec, ctx);
58
+
59
+ expect(result[0].content).toContain('class WorkOS');
60
+ expect(result[0].content).toContain('namespace WorkOS;');
61
+ });
62
+
63
+ it('generates resource accessor methods', () => {
64
+ const result = generateClient(emptySpec, ctx);
65
+
66
+ expect(result[0].content).toContain('public function organizations(): Organizations');
67
+ });
68
+
69
+ it('includes constructor with config options', () => {
70
+ const result = generateClient(emptySpec, ctx);
71
+
72
+ expect(result[0].content).toContain('?string $apiKey = null');
73
+ expect(result[0].content).toContain('?string $clientId = null');
74
+ expect(result[0].content).toContain("string $baseUrl = 'https://api.example.com'");
75
+ expect(result[0].content).toContain('int $timeout = 60');
76
+ expect(result[0].content).toContain('int $maxRetries = 3');
77
+ expect(result[0].content).toContain(
78
+ 'new HttpClient($apiKey, $clientId, $baseUrl, $timeout, $maxRetries, $handler)',
79
+ );
80
+ expect(result[0].content).not.toContain('self::$apiKey = $apiKey;');
81
+ expect(result[0].content).not.toContain('self::$clientId = $clientId;');
82
+ });
83
+
84
+ it('includes non-spec service accessors', () => {
85
+ const result = generateClient(emptySpec, ctx);
86
+
87
+ expect(result[0].content).toContain('public function passwordless(): Passwordless');
88
+ expect(result[0].content).toContain('public function vault(): Vault');
89
+ expect(result[0].content).toContain('public function webhookVerification(): WebhookVerification');
90
+ expect(result[0].content).toContain('public function actions(): Actions');
91
+ expect(result[0].content).toContain('public function sessionManager(): SessionManager');
92
+ expect(result[0].content).toContain('public function pkce(): PKCEHelper');
93
+ });
94
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { EmitterContext, ApiSpec, Enum } from '@workos/oagen';
3
+ import { defaultSdkBehavior } from '@workos/oagen';
4
+ import { generateEnums } from '../../src/php/enums.js';
5
+ import { initializeEnumDedup } from '../../src/php/naming.js';
6
+
7
+ const emptySpec: ApiSpec = {
8
+ name: 'Test',
9
+ version: '1.0.0',
10
+ baseUrl: '',
11
+ services: [],
12
+ models: [],
13
+ enums: [],
14
+ sdk: defaultSdkBehavior(),
15
+ };
16
+
17
+ const ctx: EmitterContext = {
18
+ namespace: 'workos',
19
+ namespacePascal: 'WorkOS',
20
+ spec: emptySpec,
21
+ };
22
+
23
+ describe('generateEnums', () => {
24
+ it('returns empty array for no enums', () => {
25
+ expect(generateEnums([], ctx)).toEqual([]);
26
+ });
27
+
28
+ it('generates a string-backed enum', () => {
29
+ const enums: Enum[] = [
30
+ {
31
+ name: 'OrganizationStatus',
32
+ values: [
33
+ { name: 'ACTIVE', value: 'active' },
34
+ { name: 'INACTIVE', value: 'inactive' },
35
+ ],
36
+ },
37
+ ];
38
+
39
+ const result = generateEnums(enums, ctx);
40
+
41
+ expect(result).toHaveLength(1);
42
+ expect(result[0].path).toBe('lib/Resource/OrganizationStatus.php');
43
+ expect(result[0].content).toContain('enum OrganizationStatus: string');
44
+ expect(result[0].content).toContain("case Active = 'active';");
45
+ expect(result[0].content).toContain("case Inactive = 'inactive';");
46
+ });
47
+
48
+ it('generates an int-backed enum', () => {
49
+ const enums: Enum[] = [
50
+ {
51
+ name: 'Priority',
52
+ values: [
53
+ { name: 'LOW', value: 1 },
54
+ { name: 'MEDIUM', value: 2 },
55
+ { name: 'HIGH', value: 3 },
56
+ ],
57
+ },
58
+ ];
59
+
60
+ const result = generateEnums(enums, ctx);
61
+
62
+ expect(result[0].content).toContain('enum Priority: int');
63
+ expect(result[0].content).toContain('case Low = 1;');
64
+ expect(result[0].content).toContain('case Medium = 2;');
65
+ expect(result[0].content).toContain('case High = 3;');
66
+ });
67
+
68
+ it('generates correct namespace', () => {
69
+ const enums: Enum[] = [
70
+ {
71
+ name: 'Status',
72
+ values: [{ name: 'ACTIVE', value: 'active' }],
73
+ },
74
+ ];
75
+
76
+ const result = generateEnums(enums, ctx);
77
+
78
+ expect(result[0].content).toContain('namespace WorkOS\\Resource;');
79
+ });
80
+
81
+ it('collapses duplicate enums with identical values into one file', () => {
82
+ const enums: Enum[] = [
83
+ {
84
+ name: 'Order',
85
+ values: [
86
+ { name: 'ASC', value: 'asc' },
87
+ { name: 'DESC', value: 'desc' },
88
+ ],
89
+ },
90
+ {
91
+ name: 'ConnectionOrder',
92
+ values: [
93
+ { name: 'ASC', value: 'asc' },
94
+ { name: 'DESC', value: 'desc' },
95
+ ],
96
+ },
97
+ {
98
+ name: 'ApiKeyOrder',
99
+ values: [
100
+ { name: 'ASC', value: 'asc' },
101
+ { name: 'DESC', value: 'desc' },
102
+ ],
103
+ },
104
+ ];
105
+
106
+ // Initialize dedup before generating
107
+ initializeEnumDedup(enums);
108
+ const result = generateEnums(enums, ctx);
109
+
110
+ // Should produce only one file (the shortest name: Order)
111
+ expect(result).toHaveLength(1);
112
+ expect(result[0].path).toBe('lib/Resource/Order.php');
113
+ });
114
+
115
+ it('adds PHPDoc @deprecated for deprecated enum values', () => {
116
+ const enums: Enum[] = [
117
+ {
118
+ name: 'ConnectionType',
119
+ values: [
120
+ { name: 'SAML', value: 'saml' },
121
+ { name: 'OAUTH', value: 'oauth', deprecated: true },
122
+ ],
123
+ },
124
+ ];
125
+
126
+ const result = generateEnums(enums, ctx);
127
+
128
+ expect(result).toHaveLength(1);
129
+ // The non-deprecated value should not have a PHPDoc
130
+ expect(result[0].content).not.toContain('/** @deprecated */\n case Saml');
131
+ // The deprecated value should have a PHPDoc
132
+ expect(result[0].content).toContain('/** @deprecated */');
133
+ // Verify the deprecated case follows the PHPDoc
134
+ const lines = result[0].content.split('\n');
135
+ const deprecatedIdx = lines.findIndex((l: string) => l.includes('@deprecated'));
136
+ expect(deprecatedIdx).toBeGreaterThan(-1);
137
+ expect(lines[deprecatedIdx + 1]).toContain("= 'oauth';");
138
+ });
139
+
140
+ it('adds PHPDoc with description and @deprecated for enum values', () => {
141
+ const enums: Enum[] = [
142
+ {
143
+ name: 'ConnectionType',
144
+ values: [
145
+ { name: 'SAML', value: 'saml' },
146
+ { name: 'OAUTH', value: 'oauth', description: 'Use OIDC instead', deprecated: true },
147
+ ],
148
+ },
149
+ ];
150
+
151
+ const result = generateEnums(enums, ctx);
152
+
153
+ expect(result[0].content).toContain('Use OIDC instead');
154
+ expect(result[0].content).toContain('@deprecated');
155
+ });
156
+
157
+ it('deduplicates case names', () => {
158
+ const enums: Enum[] = [
159
+ {
160
+ name: 'DupEnum',
161
+ values: [
162
+ { name: 'FOO_BAR', value: 'foo_bar' },
163
+ { name: 'FOO__BAR', value: 'foo__bar' },
164
+ ],
165
+ },
166
+ ];
167
+
168
+ const result = generateEnums(enums, ctx);
169
+
170
+ expect(result[0].content).toContain('case FooBar =');
171
+ expect(result[0].content).toContain('case FooBar2 =');
172
+ });
173
+ });
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateErrors } from '../../src/php/errors.js';
3
+
4
+ describe('generateErrors', () => {
5
+ it('returns empty array (errors are now hand-maintained)', () => {
6
+ const result = generateErrors();
7
+ expect(result).toEqual([]);
8
+ });
9
+ });