@workos/oagen-emitters 0.12.5 → 0.13.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.mjs +1 -1
- package/dist/{plugin-Ca9LUkWW.mjs → plugin-B9F2jmwy.mjs} +1468 -1285
- package/dist/plugin-B9F2jmwy.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -7
- package/src/kotlin/client.ts +3 -2
- package/src/kotlin/naming.ts +34 -0
- package/src/kotlin/resources.ts +5 -3
- package/src/kotlin/tests.ts +12 -3
- package/src/node/client.ts +11 -2
- package/src/node/manifest.ts +5 -3
- package/src/node/naming.ts +24 -0
- package/src/node/resources.ts +21 -9
- package/src/node/tests.ts +4 -1
- package/src/php/client.ts +3 -2
- package/src/php/naming.ts +22 -0
- package/src/php/resources.ts +3 -2
- package/src/python/client.ts +16 -5
- package/src/python/naming.ts +25 -0
- package/src/python/resources.ts +4 -1
- package/src/ruby/client.ts +18 -5
- package/src/ruby/manifest.ts +5 -0
- package/src/ruby/naming.ts +30 -0
- package/src/ruby/rbi.ts +15 -8
- package/src/ruby/resources.ts +10 -3
- package/src/ruby/tests.ts +22 -5
- package/src/shared/service-name-collision.ts +56 -0
- package/dist/plugin-Ca9LUkWW.mjs.map +0 -1
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as workosEmittersPlugin } from "./plugin-
|
|
1
|
+
import { t as workosEmittersPlugin } from "./plugin-B9F2jmwy.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos/oagen-emitters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -40,20 +40,20 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@commitlint/cli": "^21.0.1",
|
|
42
42
|
"@commitlint/config-conventional": "^21.0.1",
|
|
43
|
-
"@types/node": "^25.9.
|
|
43
|
+
"@types/node": "^25.9.1",
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
45
|
+
"oxfmt": "^0.51.0",
|
|
46
|
+
"oxlint": "^1.66.0",
|
|
47
47
|
"prettier": "^3.8.3",
|
|
48
48
|
"tsdown": "^0.22.0",
|
|
49
|
-
"tsx": "^4.22.
|
|
49
|
+
"tsx": "^4.22.3",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
|
-
"vitest": "^4.1.
|
|
51
|
+
"vitest": "^4.1.7"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.19.
|
|
57
|
+
"@workos/oagen": "^0.19.5"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/kotlin/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveApiClassName, packageSegment, servicePropertyName, buildExportedClassNameSet } from './naming.js';
|
|
3
3
|
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
4
4
|
|
|
5
5
|
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
@@ -23,8 +23,9 @@ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
|
|
|
23
23
|
|
|
24
24
|
const imports = new Set<string>();
|
|
25
25
|
const accessorLines: string[] = [];
|
|
26
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
26
27
|
for (const mount of targets) {
|
|
27
|
-
const apiCls =
|
|
28
|
+
const apiCls = resolveApiClassName(mount, exportedClasses);
|
|
28
29
|
const fqn = `com.workos.${packageSegment(mount)}.${apiCls}`;
|
|
29
30
|
imports.add(fqn);
|
|
30
31
|
const prop = servicePropertyName(mount);
|
package/src/kotlin/naming.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Operation, Service, EmitterContext, TypeRef } from '@workos/oagen';
|
|
2
|
+
import {
|
|
3
|
+
buildExportedClassNameSet as buildExportedClassNameSetShared,
|
|
4
|
+
resolveServiceTarget as resolveServiceTargetShared,
|
|
5
|
+
} from '../shared/service-name-collision.js';
|
|
2
6
|
import { toPascalCase, toCamelCase, toSnakeCase } from '@workos/oagen';
|
|
3
7
|
import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
|
|
4
8
|
import { stripUrnPrefix } from '../shared/naming-utils.js';
|
|
@@ -73,6 +77,36 @@ export function apiClassName(name: string): string {
|
|
|
73
77
|
return className(name);
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the Kotlin service class name with the collision suffix applied
|
|
82
|
+
* when needed. Wraps `apiClassName` so callers don't need to thread the
|
|
83
|
+
* exported-classes set through unrelated emission logic.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveApiClassName(name: string, exportedClasses: Set<string>): string {
|
|
86
|
+
return apiClassName(resolveServiceTarget(name, exportedClasses));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the set of model + enum class names exported by the SDK. Used to
|
|
91
|
+
* detect collisions with operation-client class names — a colliding service
|
|
92
|
+
* gets a `Service` suffix appended.
|
|
93
|
+
*/
|
|
94
|
+
export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
|
|
95
|
+
return buildExportedClassNameSetShared(ctx, className);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a service's mount-target identifier, appending `Service` on
|
|
100
|
+
* collision with an exported model/enum class. Kotlin sees the collision
|
|
101
|
+
* when a service class shares a simple name with an imported model class
|
|
102
|
+
* (e.g. `com.workos.models.OrganizationMembership` vs
|
|
103
|
+
* `com.workos.organizationmembership.OrganizationMembership`) — the file's
|
|
104
|
+
* local declaration shadows the import for unqualified references.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
|
|
107
|
+
return resolveServiceTargetShared(target, exportedClasses, className);
|
|
108
|
+
}
|
|
109
|
+
|
|
76
110
|
/** Accessor property exposed on the WorkOS client (camelCase). */
|
|
77
111
|
export function servicePropertyName(name: string): string {
|
|
78
112
|
return toCamelCase(name);
|
package/src/kotlin/resources.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { enumCanonicalMap } from './enums.js';
|
|
|
16
16
|
import {
|
|
17
17
|
className,
|
|
18
18
|
propertyName,
|
|
19
|
-
|
|
19
|
+
resolveApiClassName,
|
|
20
20
|
packageSegment,
|
|
21
21
|
resolveMethodName,
|
|
22
22
|
ktLiteral,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
escapeReserved,
|
|
25
25
|
humanize,
|
|
26
26
|
maybeShortenEnumParamDescription,
|
|
27
|
+
buildExportedClassNameSet,
|
|
27
28
|
} from './naming.js';
|
|
28
29
|
import {
|
|
29
30
|
buildResolvedLookup,
|
|
@@ -96,13 +97,14 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
96
97
|
|
|
97
98
|
const files: GeneratedFile[] = [];
|
|
98
99
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
100
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
99
101
|
|
|
100
102
|
for (const [mountName, group] of mountGroups) {
|
|
101
103
|
const classCode = generateApiClass(mountName, group.operations, ctx, resolvedLookup);
|
|
102
104
|
if (!classCode) continue;
|
|
103
105
|
const pkg = packageSegment(mountName);
|
|
104
106
|
files.push({
|
|
105
|
-
path: `${KOTLIN_SRC_PREFIX}com/workos/${pkg}/${
|
|
107
|
+
path: `${KOTLIN_SRC_PREFIX}com/workos/${pkg}/${resolveApiClassName(mountName, exportedClasses)}.kt`,
|
|
106
108
|
content: classCode,
|
|
107
109
|
overwriteExisting: true,
|
|
108
110
|
});
|
|
@@ -118,7 +120,7 @@ function generateApiClass(
|
|
|
118
120
|
resolvedLookup: Map<string, ResolvedOperation>,
|
|
119
121
|
): string | null {
|
|
120
122
|
if (operations.length === 0) return null;
|
|
121
|
-
const apiClass =
|
|
123
|
+
const apiClass = resolveApiClassName(mountName, buildExportedClassNameSet(ctx));
|
|
122
124
|
const pkg = `com.workos.${packageSegment(mountName)}`;
|
|
123
125
|
|
|
124
126
|
const imports = new Set<string>();
|
package/src/kotlin/tests.ts
CHANGED
|
@@ -10,7 +10,15 @@ import type {
|
|
|
10
10
|
ResolvedWrapper,
|
|
11
11
|
} from '@workos/oagen';
|
|
12
12
|
import { planOperation } from '@workos/oagen';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
resolveApiClassName,
|
|
15
|
+
packageSegment,
|
|
16
|
+
resolveMethodName,
|
|
17
|
+
ktStringLiteral,
|
|
18
|
+
className,
|
|
19
|
+
propertyName,
|
|
20
|
+
buildExportedClassNameSet,
|
|
21
|
+
} from './naming.js';
|
|
14
22
|
import { mapTypeRef } from './type-map.js';
|
|
15
23
|
import {
|
|
16
24
|
groupByMount,
|
|
@@ -70,12 +78,13 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
70
78
|
const mountGroups = groupByMount(ctx);
|
|
71
79
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
72
80
|
|
|
81
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
73
82
|
for (const [mountName, group] of mountGroups) {
|
|
74
83
|
const content = generateServiceTestClass(mountName, group.operations, ctx, resolvedLookup);
|
|
75
84
|
if (!content) continue;
|
|
76
85
|
const pkg = packageSegment(mountName);
|
|
77
86
|
files.push({
|
|
78
|
-
path: `${TEST_PREFIX}com/workos/${pkg}/${
|
|
87
|
+
path: `${TEST_PREFIX}com/workos/${pkg}/${resolveApiClassName(mountName, exportedClasses)}Test.kt`,
|
|
79
88
|
content,
|
|
80
89
|
overwriteExisting: true,
|
|
81
90
|
});
|
|
@@ -203,7 +212,7 @@ function generateServiceTestClass(
|
|
|
203
212
|
}
|
|
204
213
|
|
|
205
214
|
const pkg = packageSegment(mountName);
|
|
206
|
-
const apiCls =
|
|
215
|
+
const apiCls = resolveApiClassName(mountName, buildExportedClassNameSet(ctx));
|
|
207
216
|
|
|
208
217
|
// If any operation would emit a disabled placeholder test, preregister
|
|
209
218
|
// the `Disabled` import before we serialize the header.
|
package/src/node/client.ts
CHANGED
|
@@ -2,7 +2,13 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fileName,
|
|
7
|
+
servicePropertyName,
|
|
8
|
+
resolveInterfaceName,
|
|
9
|
+
wireInterfaceName,
|
|
10
|
+
resolveServiceName,
|
|
11
|
+
} from './naming.js';
|
|
6
12
|
import { isInlineEnum } from './type-map.js';
|
|
7
13
|
import {
|
|
8
14
|
docComment,
|
|
@@ -89,7 +95,10 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
89
95
|
for (const service of spec.services) {
|
|
90
96
|
if (coveredServices.has(service.name)) continue;
|
|
91
97
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
92
|
-
|
|
98
|
+
// Accessor name uses the un-suffixed service name so that the public API
|
|
99
|
+
// stays `client.organizationMembership` even when the class itself was
|
|
100
|
+
// renamed to `OrganizationMembershipService` to avoid a model collision.
|
|
101
|
+
const propName = servicePropertyName(resolveServiceName(service, ctx));
|
|
93
102
|
if (existingProps.has(propName)) continue;
|
|
94
103
|
// Propagate `@deprecated` from the service class to the property so
|
|
95
104
|
// IDEs surface the strikethrough at `workos.xyz` access sites, not
|
package/src/node/manifest.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ApiSpec, EmitterContext, OperationsMap } from '@workos/oagen';
|
|
2
|
-
import { resolveMethodName, servicePropertyName } from './naming.js';
|
|
3
|
-
import { resolveResourceClassName } from './resources.js';
|
|
2
|
+
import { resolveMethodName, servicePropertyName, resolveServiceName } from './naming.js';
|
|
4
3
|
import { buildResolvedLookup, lookupResolved } from '../shared/resolved-ops.js';
|
|
5
4
|
|
|
6
5
|
export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): OperationsMap {
|
|
@@ -8,7 +7,10 @@ export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): Operatio
|
|
|
8
7
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
9
8
|
|
|
10
9
|
for (const service of spec.services) {
|
|
11
|
-
|
|
10
|
+
// Accessor name reflects the un-suffixed service mount target so the
|
|
11
|
+
// manifest matches `client.organizationMembership` (not the suffixed
|
|
12
|
+
// class name used to dodge model collisions).
|
|
13
|
+
const serviceProp = servicePropertyName(resolveServiceName(service, ctx));
|
|
12
14
|
for (const op of service.operations) {
|
|
13
15
|
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
14
16
|
const method = resolveMethodName(op, service, ctx);
|
package/src/node/naming.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { Operation, Service, EmitterContext } from '@workos/oagen';
|
|
|
2
2
|
import { toPascalCase, toCamelCase, toKebabCase, toSnakeCase } from '@workos/oagen';
|
|
3
3
|
import { buildResolvedLookup, lookupMethodName } from '../shared/resolved-ops.js';
|
|
4
4
|
import { stripUrnPrefix } from '../shared/naming-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
buildExportedClassNameSet as buildExportedClassNameSetShared,
|
|
7
|
+
resolveServiceTarget as resolveServiceTargetShared,
|
|
8
|
+
} from '../shared/service-name-collision.js';
|
|
5
9
|
|
|
6
10
|
/** Strip spec-noise suffixes (e.g., "Dto") from an IR name. */
|
|
7
11
|
export function stripNoiseSuffixes(name: string): string {
|
|
@@ -117,6 +121,26 @@ export function resolveServiceName(service: Service, ctx: EmitterContext): strin
|
|
|
117
121
|
return resolveClassName(service, ctx);
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Build the set of model + enum class names exported by the SDK. Used to
|
|
126
|
+
* detect collisions with operation-client class names — a colliding service
|
|
127
|
+
* gets a `Service` suffix appended.
|
|
128
|
+
*/
|
|
129
|
+
export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
|
|
130
|
+
return buildExportedClassNameSetShared(ctx, className);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a service's mount-target identifier, appending `Service` on
|
|
135
|
+
* collision with an exported model/enum class. The result feeds `className`
|
|
136
|
+
* and `fileName` so both the `export class` declaration and its file name
|
|
137
|
+
* stay aligned (e.g. `OrganizationMembershipService` /
|
|
138
|
+
* `organization-membership-service.ts`).
|
|
139
|
+
*/
|
|
140
|
+
export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
|
|
141
|
+
return resolveServiceTargetShared(target, exportedClasses, className);
|
|
142
|
+
}
|
|
143
|
+
|
|
120
144
|
/**
|
|
121
145
|
* Build a map from IR service name -> resolved service name.
|
|
122
146
|
*/
|
package/src/node/resources.ts
CHANGED
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
resolveInterfaceName,
|
|
54
54
|
resolveServiceName,
|
|
55
55
|
wireInterfaceName,
|
|
56
|
+
buildExportedClassNameSet,
|
|
57
|
+
resolveServiceTarget,
|
|
56
58
|
} from './naming.js';
|
|
57
59
|
import {
|
|
58
60
|
docComment,
|
|
@@ -102,21 +104,31 @@ export function hasCompatibleConstructor(className: string, ctx: EmitterContext)
|
|
|
102
104
|
*/
|
|
103
105
|
export function resolveResourceClassName(service: Service, ctx: EmitterContext): string {
|
|
104
106
|
const overlayName = resolveServiceName(service, ctx);
|
|
107
|
+
let base: string;
|
|
105
108
|
if (hasCompatibleConstructor(overlayName, ctx)) {
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
109
|
+
base = overlayName;
|
|
110
|
+
} else {
|
|
111
|
+
// Incompatible constructor — fall back to IR name, with `Endpoints` suffix
|
|
112
|
+
// if it would collide with the overlay name.
|
|
113
|
+
const irName = toPascalCase(service.name);
|
|
114
|
+
base = irName === overlayName ? `${irName}Endpoints` : irName;
|
|
115
|
+
}
|
|
116
|
+
// Cross-language `Service` suffix when the chosen class name would shadow
|
|
117
|
+
// an exported model/enum (e.g. tag `OrganizationMembership` + schema
|
|
118
|
+
// `OrganizationMembership`).
|
|
119
|
+
return resolveServiceTarget(base, buildExportedClassNameSet(ctx));
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
export function resolveResourceDir(service: Service, ctx: EmitterContext): string {
|
|
117
123
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
118
124
|
if (resolvedName === 'WebhooksEndpoints') return 'webhooks';
|
|
119
|
-
|
|
125
|
+
// Drop the collision-`Service` suffix when picking the directory so the
|
|
126
|
+
// resource and its model-interfaces share a folder (e.g.
|
|
127
|
+
// `organization-membership/` houses both `OrganizationMembershipService`
|
|
128
|
+
// and `OrganizationMembership`'s interface files).
|
|
129
|
+
const overlayName = resolveServiceName(service, ctx);
|
|
130
|
+
const dirBase = resolvedName === `${overlayName}Service` ? overlayName : resolvedName;
|
|
131
|
+
return resolveServiceDir(dirBase);
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
/** Standard pagination query params handled by PaginationOptions — not imported individually. */
|
package/src/node/tests.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
servicePropertyName,
|
|
12
12
|
resolveMethodName,
|
|
13
13
|
resolveInterfaceName,
|
|
14
|
+
resolveServiceName,
|
|
14
15
|
wireInterfaceName,
|
|
15
16
|
} from './naming.js';
|
|
16
17
|
import { generateFixtures } from './fixtures.js';
|
|
@@ -142,7 +143,9 @@ function generateServiceTest(
|
|
|
142
143
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
143
144
|
const serviceDir = resolveResourceDir(service, ctx);
|
|
144
145
|
const serviceClass = resolvedName;
|
|
145
|
-
|
|
146
|
+
// Accessor stays un-suffixed so `client.organizationMembership` resolves even
|
|
147
|
+
// when the class was renamed to dodge a model-name collision.
|
|
148
|
+
const serviceProp = mountAccessors?.get(service.name) ?? servicePropertyName(resolveServiceName(service, ctx));
|
|
146
149
|
const testPath = `src/${serviceDir}/${fileName(resolvedName)}.spec.ts`;
|
|
147
150
|
|
|
148
151
|
const plans = service.operations.map((op) => ({
|
package/src/php/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApiSpec, Service, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { toPascalCase, toCamelCase } from '@workos/oagen';
|
|
3
|
-
import { className, servicePropertyName } from './naming.js';
|
|
3
|
+
import { className, servicePropertyName, buildExportedClassNameSet, resolveServiceTarget } from './naming.js';
|
|
4
4
|
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
5
5
|
import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
|
|
6
6
|
|
|
@@ -61,11 +61,12 @@ export function buildServiceAccessPaths(services: Service[], ctx: EmitterContext
|
|
|
61
61
|
|
|
62
62
|
function deduplicateByMount(services: Service[], ctx: EmitterContext): { name: string; propName: string }[] {
|
|
63
63
|
const seen = new Map<string, { name: string; propName: string }>();
|
|
64
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
64
65
|
for (const service of services) {
|
|
65
66
|
const target = getMountTarget(service, ctx);
|
|
66
67
|
if (!seen.has(target)) {
|
|
67
68
|
seen.set(target, {
|
|
68
|
-
name: className(target),
|
|
69
|
+
name: className(resolveServiceTarget(target, exportedClasses)),
|
|
69
70
|
propName: servicePropertyName(target),
|
|
70
71
|
});
|
|
71
72
|
}
|
package/src/php/naming.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { Service, Operation, EmitterContext, Enum } from '@workos/oagen';
|
|
|
2
2
|
import { toPascalCase, toCamelCase, toSnakeCase } from '@workos/oagen';
|
|
3
3
|
import { buildResolvedLookup, lookupMethodName } from '../shared/resolved-ops.js';
|
|
4
4
|
import { stripUrnPrefix, applyAcronymFixes } from '../shared/naming-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
buildExportedClassNameSet as buildExportedClassNameSetShared,
|
|
7
|
+
resolveServiceTarget as resolveServiceTargetShared,
|
|
8
|
+
} from '../shared/service-name-collision.js';
|
|
5
9
|
|
|
6
10
|
/** Namespace grouping result (shared with client.ts). */
|
|
7
11
|
export interface NamespaceGroup {
|
|
@@ -90,6 +94,24 @@ export function className(name: string): string {
|
|
|
90
94
|
return result;
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Build the set of model + enum class names exported by the SDK. Used to
|
|
99
|
+
* detect collisions with operation-client class names — a colliding service
|
|
100
|
+
* gets a `Service` suffix appended.
|
|
101
|
+
*/
|
|
102
|
+
export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
|
|
103
|
+
return buildExportedClassNameSetShared(ctx, className);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a service's mount-target identifier, appending `Service` on
|
|
108
|
+
* collision with an exported model/enum class. Used in `\Service\…` files
|
|
109
|
+
* to avoid `use WorkOS\Resource\X; class X` PHP fatal errors.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
|
|
112
|
+
return resolveServiceTargetShared(target, exportedClasses, className);
|
|
113
|
+
}
|
|
114
|
+
|
|
93
115
|
/** PascalCase file name (without extension) — PSR-4 convention. */
|
|
94
116
|
export function fileName(name: string): string {
|
|
95
117
|
return className(name);
|
package/src/php/resources.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Service, Operation, Model, EmitterContext, GeneratedFile, ResolvedOperation } from '@workos/oagen';
|
|
2
2
|
import { planOperation, toCamelCase, toPascalCase } from '@workos/oagen';
|
|
3
3
|
import { mapTypeRef, mapTypeRefForPHPDoc } from './type-map.js';
|
|
4
|
-
import { className, fieldName, resolveMethodName } from './naming.js';
|
|
4
|
+
import { className, fieldName, resolveMethodName, buildExportedClassNameSet, resolveServiceTarget } from './naming.js';
|
|
5
5
|
import { isListWrapperModel } from './models.js';
|
|
6
6
|
import {
|
|
7
7
|
groupByMount,
|
|
@@ -43,9 +43,10 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
43
43
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
44
44
|
: services.map((s) => ({ name: className(s.name), operations: s.operations }));
|
|
45
45
|
|
|
46
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
46
47
|
for (const { name: mountName, operations } of entries) {
|
|
47
48
|
if (operations.length === 0) continue;
|
|
48
|
-
const resourceName = className(mountName);
|
|
49
|
+
const resourceName = className(resolveServiceTarget(mountName, exportedClasses));
|
|
49
50
|
const mergedService: Service = { name: mountName, operations };
|
|
50
51
|
const lines: string[] = [];
|
|
51
52
|
|
package/src/python/client.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
2
|
import { toPascalCase } from '@workos/oagen';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
className,
|
|
5
|
+
resolveServiceDir,
|
|
6
|
+
servicePropertyName,
|
|
7
|
+
buildMountDirMap,
|
|
8
|
+
dirToModule,
|
|
9
|
+
buildExportedClassNameSet,
|
|
10
|
+
resolveServiceTarget,
|
|
11
|
+
} from './naming.js';
|
|
4
12
|
import { resolveResourceClassName, collectParameterGroupClassNames } from './resources.js';
|
|
5
13
|
import { getMountTarget, groupByMount } from '../shared/resolved-ops.js';
|
|
6
14
|
import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
|
|
@@ -149,9 +157,10 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
149
157
|
|
|
150
158
|
// Import resource classes (both sync and async)
|
|
151
159
|
const serviceDirMap = buildMountDirMap(ctx);
|
|
160
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
152
161
|
for (const service of topLevelServices) {
|
|
153
162
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
154
|
-
const clsName = className(resolvedName);
|
|
163
|
+
const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
|
|
155
164
|
const dirName = serviceDirMap.get(service.name) ?? resolveServiceDir(resolvedName);
|
|
156
165
|
const importLine = `from .${dirToModule(dirName)}._resource import ${clsName}, Async${clsName}`;
|
|
157
166
|
if (importLine.length > 88) {
|
|
@@ -185,7 +194,7 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
185
194
|
const generatedProps = new Set<string>();
|
|
186
195
|
for (const service of topLevelServices) {
|
|
187
196
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
188
|
-
const clsName = className(resolvedName);
|
|
197
|
+
const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
|
|
189
198
|
const prop = servicePropertyName(resolvedName);
|
|
190
199
|
const readable = clsName.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
191
200
|
lines.push('');
|
|
@@ -208,7 +217,7 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
208
217
|
const asyncGeneratedProps = new Set<string>();
|
|
209
218
|
for (const service of topLevelServices) {
|
|
210
219
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
211
|
-
const clsName = className(resolvedName);
|
|
220
|
+
const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
|
|
212
221
|
const prop = servicePropertyName(resolvedName);
|
|
213
222
|
const readable = clsName.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
214
223
|
lines.push('');
|
|
@@ -255,6 +264,7 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
255
264
|
const files: GeneratedFile[] = [];
|
|
256
265
|
const topLevel = deduplicateByMount(spec.services, ctx);
|
|
257
266
|
const serviceDirMap = buildMountDirMap(ctx);
|
|
267
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
258
268
|
|
|
259
269
|
// Build a map from mount target -> operations so we can discover parameter
|
|
260
270
|
// group dataclasses that need re-exporting from __init__.py.
|
|
@@ -262,6 +272,7 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
262
272
|
|
|
263
273
|
for (const service of topLevel) {
|
|
264
274
|
const resolvedName = resolveResourceClassName(service, ctx);
|
|
275
|
+
const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
|
|
265
276
|
const dirName = serviceDirMap.get(service.name) ?? resolveServiceDir(resolvedName);
|
|
266
277
|
const lines: string[] = [];
|
|
267
278
|
|
|
@@ -274,7 +285,7 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
274
285
|
// public re-exports. Otherwise consumers importing
|
|
275
286
|
// `from workos.user_management import RoleSingle` get a private-import
|
|
276
287
|
// warning under strict mode. Models barrel uses the same convention.
|
|
277
|
-
const resourceImports = [
|
|
288
|
+
const resourceImports = [clsName, `Async${clsName}`, ...groupClassNames];
|
|
278
289
|
const aliasedImports = resourceImports.map((n) => `${n} as ${n}`);
|
|
279
290
|
lines.push(`from ._resource import ${aliasedImports.join(', ')}`);
|
|
280
291
|
lines.push('from .models import *');
|
package/src/python/naming.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { Operation, Service, EmitterContext } from '@workos/oagen';
|
|
|
2
2
|
import { toPascalCase, toSnakeCase } from '@workos/oagen';
|
|
3
3
|
import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
|
|
4
4
|
import { stripUrnPrefix, applyAcronymFixes } from '../shared/naming-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
buildExportedClassNameSet as buildExportedClassNameSetShared,
|
|
7
|
+
resolveServiceTarget as resolveServiceTargetShared,
|
|
8
|
+
} from '../shared/service-name-collision.js';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Python class names that collide with builtins or typing imports.
|
|
@@ -136,6 +140,27 @@ export function resolveMethodName(op: Operation, _service: Service, ctx: Emitter
|
|
|
136
140
|
return toSnakeCase(op.name);
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Build the set of model + enum class names exported by the SDK. Used to
|
|
145
|
+
* detect collisions with operation-client class names — a colliding service
|
|
146
|
+
* gets a `Service` suffix appended.
|
|
147
|
+
*/
|
|
148
|
+
export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
|
|
149
|
+
return buildExportedClassNameSetShared(ctx, className);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a service's mount-target identifier, appending `Service` on
|
|
154
|
+
* collision with an exported model/enum class. Feeds `className`/`fileName`
|
|
155
|
+
* so the class declaration, file, and any qualified references stay aligned.
|
|
156
|
+
*
|
|
157
|
+
* Accessor names (`servicePropertyName`) intentionally use the RAW target —
|
|
158
|
+
* `client.organization_membership` reads better than the suffixed form.
|
|
159
|
+
*/
|
|
160
|
+
export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
|
|
161
|
+
return resolveServiceTargetShared(target, exportedClasses, className);
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
/** Resolve the SDK class name for a service, using resolved operations' mountOn. */
|
|
140
165
|
export function resolveClassName(service: Service, ctx: EmitterContext): string {
|
|
141
166
|
// Use resolved ops mountOn as canonical class name (flat pattern like PHP)
|
package/src/python/resources.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
buildMountDirMap,
|
|
24
24
|
dirToModule,
|
|
25
25
|
relativeImportPrefix,
|
|
26
|
+
buildExportedClassNameSet,
|
|
27
|
+
resolveServiceTarget,
|
|
26
28
|
} from './naming.js';
|
|
27
29
|
import {
|
|
28
30
|
buildResolvedLookup,
|
|
@@ -1012,10 +1014,11 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
1012
1014
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
1013
1015
|
: services.map((s) => ({ name: resolveClassName(s, ctx), operations: s.operations }));
|
|
1014
1016
|
|
|
1017
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
1015
1018
|
for (const { name: mountName, operations: allOperations } of entries) {
|
|
1016
1019
|
if (allOperations.length === 0) continue;
|
|
1017
1020
|
const dirName = moduleName(mountName);
|
|
1018
|
-
const resourceClassName = className(mountName);
|
|
1021
|
+
const resourceClassName = className(resolveServiceTarget(mountName, exportedClasses));
|
|
1019
1022
|
const importPrefix = relativeImportPrefix(dirName);
|
|
1020
1023
|
|
|
1021
1024
|
const lines: string[] = [];
|
package/src/ruby/client.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ApiSpec, EmitterContext, GeneratedFile, Service, Model, Enum } from '@workos/oagen';
|
|
2
2
|
import { assignModelsToServices } from '@workos/oagen';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
servicePropertyName,
|
|
5
|
+
resolveClassName,
|
|
6
|
+
className,
|
|
7
|
+
fileName,
|
|
8
|
+
buildMountDirMap,
|
|
9
|
+
buildExportedClassNameSet,
|
|
10
|
+
resolveServiceTarget,
|
|
11
|
+
} from './naming.js';
|
|
4
12
|
import { classifyUnassignedModel } from './models.js';
|
|
5
13
|
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
6
14
|
import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
|
|
@@ -89,8 +97,12 @@ function buildInflectionMap(spec: ApiSpec, ctx: EmitterContext): Map<string, str
|
|
|
89
97
|
|
|
90
98
|
inflections.set('workos', 'WorkOS');
|
|
91
99
|
|
|
100
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
92
101
|
for (const service of buildTopLevelServices(spec, ctx)) {
|
|
93
|
-
const target =
|
|
102
|
+
const target = resolveServiceTarget(
|
|
103
|
+
getMountTarget(service, ctx) || resolveClassName(service, ctx),
|
|
104
|
+
exportedClasses,
|
|
105
|
+
);
|
|
94
106
|
const cls = className(target);
|
|
95
107
|
const file = fileName(target);
|
|
96
108
|
if (rubyCamelize(file) !== cls) inflections.set(file, cls);
|
|
@@ -203,10 +215,11 @@ function generateClientClass(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
203
215
|
lines.push(' class Client < BaseClient');
|
|
204
216
|
|
|
205
217
|
const topLevelServices = buildTopLevelServices(spec, ctx);
|
|
218
|
+
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
206
219
|
for (const service of topLevelServices) {
|
|
207
|
-
const
|
|
208
|
-
const cls = className(
|
|
209
|
-
const prop = servicePropertyName(
|
|
220
|
+
const rawTarget = getMountTarget(service, ctx) || resolveClassName(service, ctx);
|
|
221
|
+
const cls = className(resolveServiceTarget(rawTarget, exportedClasses));
|
|
222
|
+
const prop = servicePropertyName(rawTarget);
|
|
210
223
|
lines.push('');
|
|
211
224
|
lines.push(` def ${prop}`);
|
|
212
225
|
lines.push(` @${prop} ||= WorkOS::${cls}.new(self)`);
|
package/src/ruby/manifest.ts
CHANGED
|
@@ -7,9 +7,14 @@ import { servicePropertyName } from './naming.js';
|
|
|
7
7
|
* Uses each resolved operation's actual mountOn (not the service default) so
|
|
8
8
|
* operations remounted via operationHints land on the correct service prop.
|
|
9
9
|
* Split operations emit one entry per wrapper (keyed by wrapper name + variant).
|
|
10
|
+
*
|
|
11
|
+
* The accessor (`service` field) uses the raw mountOn — accessor names stay
|
|
12
|
+
* unsuffixed even when the underlying service class gets a `Service` suffix
|
|
13
|
+
* on collision.
|
|
10
14
|
*/
|
|
11
15
|
export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): OperationsMap {
|
|
12
16
|
void spec;
|
|
17
|
+
void ctx;
|
|
13
18
|
const manifest: OperationsMap = {};
|
|
14
19
|
|
|
15
20
|
for (const r of ctx.resolvedOperations ?? []) {
|