@workos/oagen-emitters 0.2.1 → 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.
- package/.husky/pre-commit +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11893 -3226
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +298 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +3 -0
- package/src/node/client.ts +78 -115
- package/src/node/enums.ts +9 -0
- package/src/node/errors.ts +37 -232
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +9 -1
- package/src/node/index.ts +2 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +374 -364
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +32 -12
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +13 -71
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +171 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +209 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/node/client.test.ts +18 -12
- package/test/node/enums.test.ts +2 -0
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +2 -0
- package/test/node/naming.test.ts +23 -0
- package/test/node/resources.test.ts +99 -69
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +644 -0
- package/test/php/tests.test.ts +118 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -746
package/test/node/errors.test.ts
CHANGED
|
@@ -2,47 +2,8 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { generateErrors } from '../../src/node/errors.js';
|
|
3
3
|
|
|
4
4
|
describe('generateErrors', () => {
|
|
5
|
-
it('
|
|
5
|
+
it('returns empty array without context (static exceptions now hand-maintained)', () => {
|
|
6
6
|
const files = generateErrors();
|
|
7
|
-
|
|
8
|
-
const names = files.map((f) => f.path);
|
|
9
|
-
expect(names).toContain('src/common/exceptions/bad-request.exception.ts');
|
|
10
|
-
expect(names).toContain('src/common/exceptions/unauthorized.exception.ts');
|
|
11
|
-
expect(names).toContain('src/common/exceptions/not-found.exception.ts');
|
|
12
|
-
expect(names).toContain('src/common/exceptions/conflict.exception.ts');
|
|
13
|
-
expect(names).toContain('src/common/exceptions/unprocessable-entity.exception.ts');
|
|
14
|
-
expect(names).toContain('src/common/exceptions/rate-limit-exceeded.exception.ts');
|
|
15
|
-
expect(names).toContain('src/common/exceptions/generic-server.exception.ts');
|
|
16
|
-
expect(names).toContain('src/common/exceptions/no-api-key-provided.exception.ts');
|
|
17
|
-
expect(names).toContain('src/common/exceptions/index.ts');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('generates NotFoundException with correct status', () => {
|
|
21
|
-
const files = generateErrors();
|
|
22
|
-
const notFoundFile = files.find((f) => f.path.includes('not-found.exception.ts'))!;
|
|
23
|
-
|
|
24
|
-
expect(notFoundFile.content).toContain('export class NotFoundException extends Error');
|
|
25
|
-
expect(notFoundFile.content).toContain('readonly status = 404;');
|
|
26
|
-
expect(notFoundFile.content).toContain('requestID: string');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('generates RateLimitExceededException with retryAfter', () => {
|
|
30
|
-
const files = generateErrors();
|
|
31
|
-
const rateLimitFile = files.find((f) => f.path.includes('rate-limit-exceeded.exception.ts'))!;
|
|
32
|
-
|
|
33
|
-
expect(rateLimitFile.content).toContain('export class RateLimitExceededException extends Error');
|
|
34
|
-
expect(rateLimitFile.content).toContain('readonly status = 429;');
|
|
35
|
-
expect(rateLimitFile.content).toContain('retryAfter?: number');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('generates exception barrel with all exports', () => {
|
|
39
|
-
const files = generateErrors();
|
|
40
|
-
const barrel = files.find((f) => f.path === 'src/common/exceptions/index.ts')!;
|
|
41
|
-
|
|
42
|
-
expect(barrel.content).toContain('export { BadRequestException }');
|
|
43
|
-
expect(barrel.content).toContain('export { UnauthorizedException }');
|
|
44
|
-
expect(barrel.content).toContain('export { NotFoundException }');
|
|
45
|
-
expect(barrel.content).toContain('export { RateLimitExceededException }');
|
|
46
|
-
expect(barrel.content).toContain('export { NoApiKeyProvidedException }');
|
|
7
|
+
expect(files).toEqual([]);
|
|
47
8
|
});
|
|
48
9
|
});
|
package/test/node/models.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateModels } 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 = {
|
package/test/node/naming.test.ts
CHANGED
|
@@ -10,10 +10,31 @@ import {
|
|
|
10
10
|
servicePropertyName,
|
|
11
11
|
resolveServiceName,
|
|
12
12
|
buildServiceNameMap,
|
|
13
|
+
stripNoiseSuffixes,
|
|
13
14
|
} from '../../src/node/naming.js';
|
|
14
15
|
import type { EmitterContext, ApiSpec, Service } from '@workos/oagen';
|
|
16
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
15
17
|
|
|
16
18
|
describe('naming', () => {
|
|
19
|
+
describe('stripNoiseSuffixes', () => {
|
|
20
|
+
it('strips trailing Dto', () => {
|
|
21
|
+
expect(stripNoiseSuffixes('OrganizationDto')).toBe('Organization');
|
|
22
|
+
expect(stripNoiseSuffixes('UpdateOrganizationDto')).toBe('UpdateOrganization');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('is case-insensitive for the Dto suffix', () => {
|
|
26
|
+
expect(stripNoiseSuffixes('OrganizationDTO')).toBe('Organization');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does not strip Dto from the middle of a name', () => {
|
|
30
|
+
expect(stripNoiseSuffixes('DtoFactory')).toBe('DtoFactory');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('leaves names without Dto unchanged', () => {
|
|
34
|
+
expect(stripNoiseSuffixes('Organization')).toBe('Organization');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
17
38
|
describe('className', () => {
|
|
18
39
|
it('converts to PascalCase', () => {
|
|
19
40
|
expect(className('organizations')).toBe('Organizations');
|
|
@@ -88,6 +109,7 @@ describe('naming', () => {
|
|
|
88
109
|
services: [],
|
|
89
110
|
models: [],
|
|
90
111
|
enums: [],
|
|
112
|
+
sdk: defaultSdkBehavior(),
|
|
91
113
|
};
|
|
92
114
|
|
|
93
115
|
it('returns overlay class name when available', () => {
|
|
@@ -160,6 +182,7 @@ describe('naming', () => {
|
|
|
160
182
|
services: [],
|
|
161
183
|
models: [],
|
|
162
184
|
enums: [],
|
|
185
|
+
sdk: defaultSdkBehavior(),
|
|
163
186
|
};
|
|
164
187
|
|
|
165
188
|
it('maps IR names to resolved names', () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateResources, resolveResourceClassName, hasCompatibleConstructor } from '../../src/node/resources.js';
|
|
3
3
|
import type { EmitterContext, ApiSpec, 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 = {
|
|
@@ -309,7 +311,7 @@ describe('generateResources', () => {
|
|
|
309
311
|
expect(content).toContain(' *');
|
|
310
312
|
expect(content).toContain(' * You may optionally inform Radar that an attempt was successful.');
|
|
311
313
|
expect(content).toContain(' * @param id - The unique identifier of the attempt.');
|
|
312
|
-
expect(content).toContain(' * @returns {RadarAttempt}');
|
|
314
|
+
expect(content).toContain(' * @returns {Promise<RadarAttempt>}');
|
|
313
315
|
expect(content).toContain(' * @deprecated');
|
|
314
316
|
expect(content).toContain(' */');
|
|
315
317
|
});
|
|
@@ -342,7 +344,61 @@ describe('generateResources', () => {
|
|
|
342
344
|
|
|
343
345
|
const files = generateResources(services, ctx);
|
|
344
346
|
const content = files[0].content;
|
|
345
|
-
expect(content).toContain('@returns {Organization}');
|
|
347
|
+
expect(content).toContain('@returns {Promise<Organization>}');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('renders @returns from overlay return type when available', () => {
|
|
351
|
+
const services: Service[] = [
|
|
352
|
+
{
|
|
353
|
+
name: 'Authorization',
|
|
354
|
+
operations: [
|
|
355
|
+
{
|
|
356
|
+
name: 'createEnvironmentRole',
|
|
357
|
+
httpMethod: 'post',
|
|
358
|
+
path: '/authorization/roles',
|
|
359
|
+
pathParams: [],
|
|
360
|
+
queryParams: [],
|
|
361
|
+
headerParams: [],
|
|
362
|
+
requestBody: { kind: 'model', name: 'CreateRoleInput' },
|
|
363
|
+
response: { kind: 'model', name: 'Role' },
|
|
364
|
+
errors: [],
|
|
365
|
+
injectIdempotencyKey: false,
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
const overlayCtx: EmitterContext = {
|
|
372
|
+
namespace: 'workos',
|
|
373
|
+
namespacePascal: 'WorkOS',
|
|
374
|
+
spec: { ...emptySpec, services, models: [] },
|
|
375
|
+
overlayLookup: {
|
|
376
|
+
methodByOperation: new Map([
|
|
377
|
+
[
|
|
378
|
+
'POST /authorization/roles',
|
|
379
|
+
{
|
|
380
|
+
className: 'Authorization',
|
|
381
|
+
methodName: 'createEnvironmentRole',
|
|
382
|
+
params: [{ name: 'payload', type: 'CreateRoleInput', optional: false }],
|
|
383
|
+
returnType: 'Promise<EnvironmentRole>',
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
]),
|
|
387
|
+
httpKeyByMethod: new Map(),
|
|
388
|
+
interfaceByName: new Map(),
|
|
389
|
+
typeAliasByName: new Map(),
|
|
390
|
+
requiredExports: new Map(),
|
|
391
|
+
modelNameByIR: new Map(),
|
|
392
|
+
fileBySymbol: new Map(),
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const files = generateResources(services, overlayCtx);
|
|
397
|
+
const content = files[0].content;
|
|
398
|
+
// JSDoc should use the overlay return type, not the spec schema name
|
|
399
|
+
expect(content).toContain('@returns {Promise<EnvironmentRole>}');
|
|
400
|
+
expect(content).not.toContain('@returns {Role}');
|
|
401
|
+
expect(content).not.toContain('@returns {Promise<Role>}');
|
|
346
402
|
});
|
|
347
403
|
|
|
348
404
|
it('renders query param docs for non-paginated operations', () => {
|
|
@@ -466,9 +522,9 @@ describe('generateResources', () => {
|
|
|
466
522
|
const files = generateResources(services, ctx);
|
|
467
523
|
const content = files[0].content;
|
|
468
524
|
// Only emit a single @returns for the primary response model (no status-code variants)
|
|
469
|
-
expect(content).toContain('@returns {Organization}');
|
|
470
|
-
expect(content).not.toContain('@returns {Organization} 200');
|
|
471
|
-
expect(content).not.toContain('@returns {Organization} 201');
|
|
525
|
+
expect(content).toContain('@returns {Promise<Organization>}');
|
|
526
|
+
expect(content).not.toContain('@returns {Promise<Organization>} 200');
|
|
527
|
+
expect(content).not.toContain('@returns {Promise<Organization>} 201');
|
|
472
528
|
});
|
|
473
529
|
|
|
474
530
|
it('generates DELETE-with-body method using deleteWithBody', () => {
|
|
@@ -1665,91 +1721,65 @@ describe('partial service coverage', () => {
|
|
|
1665
1721
|
const files = generateResources(services, ctxCovered);
|
|
1666
1722
|
expect(files.length).toBe(1);
|
|
1667
1723
|
const content = files[0].content;
|
|
1668
|
-
//
|
|
1724
|
+
// All methods should have generated JSDoc — the merger matches by name
|
|
1725
|
+
// and handles @deprecated preservation, so the emitter always provides
|
|
1726
|
+
// docstrings for the merger to work with.
|
|
1669
1727
|
expect(content).toContain('List all permissions.');
|
|
1670
1728
|
// skipIfExists should remain true for covered services
|
|
1671
1729
|
expect(files[0].skipIfExists).toBe(true);
|
|
1672
1730
|
});
|
|
1673
1731
|
|
|
1674
|
-
it('
|
|
1732
|
+
it('uses resolved operation method names when provided', () => {
|
|
1733
|
+
const op = {
|
|
1734
|
+
name: 'listRolesOrganizations',
|
|
1735
|
+
httpMethod: 'get' as const,
|
|
1736
|
+
path: '/authorization/organizations/{organizationId}/roles',
|
|
1737
|
+
pathParams: [
|
|
1738
|
+
{
|
|
1739
|
+
name: 'organizationId',
|
|
1740
|
+
type: { kind: 'primitive' as const, type: 'string' as const },
|
|
1741
|
+
required: true,
|
|
1742
|
+
},
|
|
1743
|
+
],
|
|
1744
|
+
queryParams: [],
|
|
1745
|
+
headerParams: [],
|
|
1746
|
+
response: { kind: 'model' as const, name: 'RoleList' },
|
|
1747
|
+
errors: [],
|
|
1748
|
+
pagination: {
|
|
1749
|
+
strategy: 'cursor' as const,
|
|
1750
|
+
param: 'after',
|
|
1751
|
+
itemType: { kind: 'model' as const, name: 'RoleList' },
|
|
1752
|
+
},
|
|
1753
|
+
injectIdempotencyKey: false,
|
|
1754
|
+
};
|
|
1675
1755
|
const services: Service[] = [
|
|
1676
1756
|
{
|
|
1677
1757
|
name: 'Authorization',
|
|
1678
|
-
operations: [
|
|
1679
|
-
{
|
|
1680
|
-
name: 'listRolesOrganizations',
|
|
1681
|
-
httpMethod: 'get',
|
|
1682
|
-
path: '/authorization/organizations/{organizationId}/roles',
|
|
1683
|
-
pathParams: [
|
|
1684
|
-
{
|
|
1685
|
-
name: 'organizationId',
|
|
1686
|
-
type: { kind: 'primitive', type: 'string' },
|
|
1687
|
-
required: true,
|
|
1688
|
-
},
|
|
1689
|
-
],
|
|
1690
|
-
queryParams: [],
|
|
1691
|
-
headerParams: [],
|
|
1692
|
-
response: { kind: 'model', name: 'RoleList' },
|
|
1693
|
-
errors: [],
|
|
1694
|
-
pagination: {
|
|
1695
|
-
strategy: 'cursor' as const,
|
|
1696
|
-
param: 'after',
|
|
1697
|
-
itemType: { kind: 'model' as const, name: 'RoleList' },
|
|
1698
|
-
},
|
|
1699
|
-
injectIdempotencyKey: false,
|
|
1700
|
-
},
|
|
1701
|
-
],
|
|
1758
|
+
operations: [op],
|
|
1702
1759
|
},
|
|
1703
1760
|
];
|
|
1704
1761
|
|
|
1705
|
-
const
|
|
1762
|
+
const ctxResolved: EmitterContext = {
|
|
1706
1763
|
...ctx,
|
|
1707
1764
|
spec: {
|
|
1708
1765
|
...emptySpec,
|
|
1709
1766
|
services,
|
|
1710
1767
|
models: [{ name: 'RoleList', fields: [] }],
|
|
1711
1768
|
},
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
},
|
|
1721
|
-
apiSurface: {
|
|
1722
|
-
language: 'node',
|
|
1723
|
-
extractedFrom: 'test',
|
|
1724
|
-
extractedAt: '2024-01-01',
|
|
1725
|
-
classes: {
|
|
1726
|
-
Authorization: {
|
|
1727
|
-
name: 'Authorization',
|
|
1728
|
-
methods: {
|
|
1729
|
-
listOrganizationRoles: [
|
|
1730
|
-
{
|
|
1731
|
-
name: 'listOrganizationRoles',
|
|
1732
|
-
params: [],
|
|
1733
|
-
returnType: 'void',
|
|
1734
|
-
async: true,
|
|
1735
|
-
},
|
|
1736
|
-
],
|
|
1737
|
-
},
|
|
1738
|
-
properties: {},
|
|
1739
|
-
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
1740
|
-
},
|
|
1741
|
-
},
|
|
1742
|
-
interfaces: {},
|
|
1743
|
-
typeAliases: {},
|
|
1744
|
-
enums: {},
|
|
1745
|
-
exports: {},
|
|
1746
|
-
},
|
|
1769
|
+
resolvedOperations: [
|
|
1770
|
+
{
|
|
1771
|
+
operation: op,
|
|
1772
|
+
service: services[0],
|
|
1773
|
+
methodName: 'list_organization_roles',
|
|
1774
|
+
mountOn: 'Authorization',
|
|
1775
|
+
} as any,
|
|
1776
|
+
],
|
|
1747
1777
|
};
|
|
1748
1778
|
|
|
1749
|
-
const files = generateResources(services,
|
|
1779
|
+
const files = generateResources(services, ctxResolved);
|
|
1750
1780
|
expect(files.length).toBe(1);
|
|
1751
1781
|
const content = files[0].content;
|
|
1752
|
-
// Should use
|
|
1782
|
+
// Should use the resolved operation name (converted to camelCase)
|
|
1753
1783
|
expect(content).toContain('async listOrganizationRoles');
|
|
1754
1784
|
expect(content).not.toContain('async listRolesOrganizations');
|
|
1755
1785
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateSerializers } from '../../src/node/
|
|
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 = {
|
|
@@ -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
|
+
});
|