@workos/oagen-emitters 0.18.4 → 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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-Cciic50q.mjs → plugin-BXDPA9pJ.mjs} +140 -75
- package/dist/plugin-BXDPA9pJ.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +4 -4
- package/src/dotnet/enums.ts +11 -5
- package/src/dotnet/models.ts +11 -5
- package/src/dotnet/resources.ts +3 -3
- package/src/dotnet/tests.ts +3 -3
- package/src/go/resources.ts +3 -3
- package/src/go/tests.ts +3 -3
- package/src/kotlin/enums.ts +21 -11
- package/src/kotlin/models.ts +19 -7
- package/src/kotlin/resources.ts +2 -2
- package/src/kotlin/tests.ts +2 -2
- package/src/node/enums.ts +8 -5
- package/src/node/models.ts +29 -21
- package/src/node/resources.ts +12 -1
- package/src/node/tests.ts +7 -2
- package/src/php/enums.ts +18 -5
- package/src/php/index.ts +11 -3
- package/src/php/models.ts +11 -5
- package/src/php/resources.ts +6 -4
- package/src/php/tests.ts +6 -3
- package/src/python/enums.ts +39 -28
- package/src/python/models.ts +27 -18
- package/src/python/resources.ts +3 -3
- package/src/python/tests.ts +3 -3
- package/src/ruby/enums.ts +28 -19
- package/src/ruby/models.ts +23 -12
- package/src/ruby/rbi.ts +17 -6
- package/src/ruby/resources.ts +2 -2
- package/src/ruby/tests.ts +2 -2
- package/src/rust/enums.ts +9 -1
- package/src/rust/models.ts +18 -5
- package/src/rust/resources.ts +8 -1
- package/src/rust/tests.ts +2 -2
- package/src/shared/resolved-ops.ts +47 -0
- package/test/shared/synthetic-enum-seed.test.ts +79 -0
- package/dist/plugin-Cciic50q.mjs.map +0 -1
package/src/php/tests.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { isListWrapperModel } from './models.js';
|
|
|
20
20
|
import { generateFixtures } from './fixtures.js';
|
|
21
21
|
import {
|
|
22
22
|
getMountTarget,
|
|
23
|
-
|
|
23
|
+
scopedMountGroups,
|
|
24
24
|
buildHiddenParams,
|
|
25
25
|
getOpDefaults,
|
|
26
26
|
getOpInferFromClient,
|
|
@@ -39,9 +39,12 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
39
39
|
|
|
40
40
|
// Collect all operations per mount target using resolved per-operation mounts.
|
|
41
41
|
// This correctly handles operationHint mountOn overrides (e.g., audit_logs_retention → AuditLogs).
|
|
42
|
-
|
|
42
|
+
// In a scoped (`--services`) run this returns only the selected post-mount
|
|
43
|
+
// services so we emit per-service test files for those alone. ClientTest.php
|
|
44
|
+
// and fixtures below are built from `spec` and stay full.
|
|
45
|
+
const mountGroupsFromResolved = scopedMountGroups(ctx);
|
|
43
46
|
const mountGroups = new Map<string, { op: Operation; service: Service; resolvedOp?: ResolvedOperation }[]>();
|
|
44
|
-
if (mountGroupsFromResolved.size > 0) {
|
|
47
|
+
if (mountGroupsFromResolved.size > 0 || ctx.scopedServices?.size) {
|
|
45
48
|
for (const [target, group] of mountGroupsFromResolved) {
|
|
46
49
|
mountGroups.set(
|
|
47
50
|
target,
|
package/src/python/enums.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
|
2
2
|
import { toUpperSnakeCase } from '@workos/oagen';
|
|
3
3
|
import { className, fileName, buildMountDirMap, dirToModule } from './naming.js';
|
|
4
4
|
import { computeSchemaPlacement } from './shared-schemas.js';
|
|
5
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Convert a PascalCase class name to a human-readable lowercase string,
|
|
@@ -38,6 +39,10 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
38
39
|
for (const enumDef of enums) {
|
|
39
40
|
const service = enumToService.get(enumDef.name);
|
|
40
41
|
const dirName = resolveDir(service);
|
|
42
|
+
// FR-1.4: write per-enum FILES only when in scope; the enum barrel is built
|
|
43
|
+
// separately (collectGeneratedEnumSymbolsByDir over the full set), so an
|
|
44
|
+
// out-of-scope enum left on disk stays exported and importable.
|
|
45
|
+
const enumInScope = isEnumInScope(enumDef.name, ctx);
|
|
41
46
|
|
|
42
47
|
// If this enum is an alias for a canonical enum, generate a type alias file
|
|
43
48
|
const canonicalName = aliasOf.get(enumDef.name);
|
|
@@ -73,12 +78,14 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
73
78
|
lines.push(' raise AttributeError(f"module {__name__!r} has no attribute {name!r}")');
|
|
74
79
|
}
|
|
75
80
|
lines.push(`__all__ = ["${aliasCls}"]`);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
if (enumInScope) {
|
|
82
|
+
files.push({
|
|
83
|
+
path: `src/${ctx.namespace}/${dirName}/models/${fileName(enumDef.name)}.py`,
|
|
84
|
+
content: lines.join('\n'),
|
|
85
|
+
integrateTarget: true,
|
|
86
|
+
overwriteExisting: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
82
89
|
|
|
83
90
|
// Also generate compat alias files for dedup aliases (they may have compat aliases too)
|
|
84
91
|
for (const aliasName of compatAliases.get(enumDef.name) ?? []) {
|
|
@@ -107,12 +114,14 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
107
114
|
`__all__ = ["${aliasName}"]`,
|
|
108
115
|
].join('\n');
|
|
109
116
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
if (enumInScope) {
|
|
118
|
+
files.push({
|
|
119
|
+
path: `src/${ctx.namespace}/${dirName}/models/${fileName(aliasName)}.py`,
|
|
120
|
+
content: compatContent,
|
|
121
|
+
integrateTarget: true,
|
|
122
|
+
overwriteExisting: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
continue;
|
|
@@ -241,26 +250,28 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
241
250
|
);
|
|
242
251
|
}
|
|
243
252
|
|
|
244
|
-
|
|
245
|
-
path: `src/${ctx.namespace}/${dirName}/models/${fileName(enumDef.name)}.py`,
|
|
246
|
-
content: lines.join('\n'),
|
|
247
|
-
integrateTarget: true,
|
|
248
|
-
overwriteExisting: true,
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
for (const aliasName of compatAliases.get(enumDef.name) ?? []) {
|
|
253
|
+
if (enumInScope) {
|
|
252
254
|
files.push({
|
|
253
|
-
path: `src/${ctx.namespace}/${dirName}/models/${fileName(
|
|
254
|
-
content:
|
|
255
|
-
'from typing import TypeAlias',
|
|
256
|
-
`from .${fileName(enumDef.name)} import ${cls}`,
|
|
257
|
-
'',
|
|
258
|
-
`${aliasName}: TypeAlias = ${cls}`,
|
|
259
|
-
`__all__ = ["${aliasName}"]`,
|
|
260
|
-
].join('\n'),
|
|
255
|
+
path: `src/${ctx.namespace}/${dirName}/models/${fileName(enumDef.name)}.py`,
|
|
256
|
+
content: lines.join('\n'),
|
|
261
257
|
integrateTarget: true,
|
|
262
258
|
overwriteExisting: true,
|
|
263
259
|
});
|
|
260
|
+
|
|
261
|
+
for (const aliasName of compatAliases.get(enumDef.name) ?? []) {
|
|
262
|
+
files.push({
|
|
263
|
+
path: `src/${ctx.namespace}/${dirName}/models/${fileName(aliasName)}.py`,
|
|
264
|
+
content: [
|
|
265
|
+
'from typing import TypeAlias',
|
|
266
|
+
`from .${fileName(enumDef.name)} import ${cls}`,
|
|
267
|
+
'',
|
|
268
|
+
`${aliasName}: TypeAlias = ${cls}`,
|
|
269
|
+
`__all__ = ["${aliasName}"]`,
|
|
270
|
+
].join('\n'),
|
|
271
|
+
integrateTarget: true,
|
|
272
|
+
overwriteExisting: true,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
264
275
|
}
|
|
265
276
|
}
|
|
266
277
|
|
package/src/python/models.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { mapTypeRef } from './type-map.js';
|
|
|
4
4
|
import { className, domainFieldName, fileName, buildMountDirMap, dirToModule } from './naming.js';
|
|
5
5
|
import { collectGeneratedEnumSymbolsByDir } from './enums.js';
|
|
6
6
|
import { computeSchemaPlacement } from './shared-schemas.js';
|
|
7
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Generate Python dataclass model files from IR Model definitions.
|
|
@@ -169,12 +170,16 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
169
170
|
dispLines.push(` return cast("${variantTypeName}", dispatch_cls.from_dict(data))`);
|
|
170
171
|
dispLines.push(` return ${unknownClassName}.from_dict(data)`);
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
// FR-1.4: write the file only when in scope; the barrel tracking below is
|
|
174
|
+
// unconditional so out-of-scope models (left on disk) stay exported.
|
|
175
|
+
if (isModelInScope(model.name, ctx)) {
|
|
176
|
+
files.push({
|
|
177
|
+
path: `src/${ctx.namespace}/${dirName}/models/${fileName(model.name)}.py`,
|
|
178
|
+
content: dispLines.join('\n'),
|
|
179
|
+
integrateTarget: true,
|
|
180
|
+
overwriteExisting: true,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
178
183
|
|
|
179
184
|
if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
|
|
180
185
|
emittedModelSymbolsByDir.get(dirName)!.push(model.name);
|
|
@@ -216,12 +221,14 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
216
221
|
}
|
|
217
222
|
lines.push('');
|
|
218
223
|
lines.push(`${modelClassName}: TypeAlias = ${canonicalClassName}`);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
if (isModelInScope(model.name, ctx)) {
|
|
225
|
+
files.push({
|
|
226
|
+
path: `src/${ctx.namespace}/${dirName}/models/${fileName(model.name)}.py`,
|
|
227
|
+
content: lines.join('\n'),
|
|
228
|
+
integrateTarget: true,
|
|
229
|
+
overwriteExisting: true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
225
232
|
if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
|
|
226
233
|
emittedModelSymbolsByDir.get(dirName)!.push(model.name);
|
|
227
234
|
const aliasNatural = originalModelToService.get(model.name);
|
|
@@ -457,12 +464,14 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
457
464
|
|
|
458
465
|
lines.push(' return result');
|
|
459
466
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
467
|
+
if (isModelInScope(model.name, ctx)) {
|
|
468
|
+
files.push({
|
|
469
|
+
path: `src/${ctx.namespace}/${dirName}/models/${fileName(model.name)}.py`,
|
|
470
|
+
content: lines.join('\n'),
|
|
471
|
+
integrateTarget: true,
|
|
472
|
+
overwriteExisting: true,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
466
475
|
if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
|
|
467
476
|
emittedModelSymbolsByDir.get(dirName)!.push(model.name);
|
|
468
477
|
const regularNatural = originalModelToService.get(model.name);
|
package/src/python/resources.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
buildResolvedLookup,
|
|
31
31
|
lookupMethodName,
|
|
32
32
|
lookupResolved,
|
|
33
|
-
|
|
33
|
+
scopedMountGroups,
|
|
34
34
|
getOpDefaults,
|
|
35
35
|
getOpInferFromClient,
|
|
36
36
|
buildHiddenParams as buildHiddenParamsShared,
|
|
@@ -1056,12 +1056,12 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
1056
1056
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
1057
1057
|
const files: GeneratedFile[] = [];
|
|
1058
1058
|
const mountDirMap = buildMountDirMap(ctx);
|
|
1059
|
-
const mountGroups =
|
|
1059
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
1060
1060
|
|
|
1061
1061
|
// Build mount group entries. When resolved operations are available, group by
|
|
1062
1062
|
// mount target. Otherwise fall back to one group per service (for tests).
|
|
1063
1063
|
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
1064
|
-
mountGroups.size > 0
|
|
1064
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
1065
1065
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
1066
1066
|
: services.map((s) => ({ name: resolveClassName(s, ctx), operations: s.operations }));
|
|
1067
1067
|
|
package/src/python/tests.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { generateFixtures, generateModelFixture } from './fixtures.js';
|
|
|
25
25
|
import { isListWrapperModel, isListMetadataModel } from './models.js';
|
|
26
26
|
import { collectNonPaginatedResponseModelNames, collectReferencedListMetadataModels } from '../shared/model-utils.js';
|
|
27
27
|
import {
|
|
28
|
-
|
|
28
|
+
scopedMountGroups,
|
|
29
29
|
buildResolvedLookup,
|
|
30
30
|
lookupResolved,
|
|
31
31
|
buildHiddenParams,
|
|
@@ -118,9 +118,9 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
118
118
|
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
119
119
|
|
|
120
120
|
// Generate per-mount-target test files (merges all sub-services into one file)
|
|
121
|
-
const mountGroups =
|
|
121
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
122
122
|
const testEntries: Array<{ name: string; operations: Operation[]; resolvedOps?: ResolvedOperation[] }> =
|
|
123
|
-
mountGroups.size > 0
|
|
123
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
124
124
|
? [...mountGroups].map(([name, group]) => ({
|
|
125
125
|
name,
|
|
126
126
|
operations: group.operations,
|
package/src/ruby/enums.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { toUpperSnakeCase } from '@workos/oagen';
|
|
3
3
|
import { className, fileName } from './naming.js';
|
|
4
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Generate Ruby enum class files.
|
|
@@ -9,7 +10,6 @@ import { className, fileName } from './naming.js';
|
|
|
9
10
|
* and a frozen `ALL` array of all values.
|
|
10
11
|
*/
|
|
11
12
|
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
12
|
-
void ctx;
|
|
13
13
|
if (enums.length === 0) return [];
|
|
14
14
|
|
|
15
15
|
const files: GeneratedFile[] = [];
|
|
@@ -17,6 +17,9 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
17
17
|
|
|
18
18
|
for (const enumDef of enums) {
|
|
19
19
|
const cls = className(enumDef.name);
|
|
20
|
+
// FR-1.4: write the per-enum file only when in scope. Out-of-scope enum
|
|
21
|
+
// files are left untouched on disk; Zeitwerk autoloads them by path.
|
|
22
|
+
const enumInScope = isEnumInScope(enumDef.name, ctx);
|
|
20
23
|
|
|
21
24
|
// If this enum duplicates another (by value set), emit a Ruby constant
|
|
22
25
|
// alias. Zeitwerk autoloads the canonical when the alias is first
|
|
@@ -30,12 +33,14 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
30
33
|
lines.push(` ${cls} = ${canonicalCls}`);
|
|
31
34
|
lines.push(' end');
|
|
32
35
|
lines.push('end');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (enumInScope) {
|
|
37
|
+
files.push({
|
|
38
|
+
path: `lib/workos/types/${fileName(enumDef.name)}.rb`,
|
|
39
|
+
content: lines.join('\n'),
|
|
40
|
+
integrateTarget: true,
|
|
41
|
+
overwriteExisting: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
39
44
|
continue;
|
|
40
45
|
}
|
|
41
46
|
|
|
@@ -60,12 +65,14 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
60
65
|
lines.push(' end');
|
|
61
66
|
lines.push(' end');
|
|
62
67
|
lines.push('end');
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
if (enumInScope) {
|
|
69
|
+
files.push({
|
|
70
|
+
path: `lib/workos/types/${fileName(enumDef.name)}.rb`,
|
|
71
|
+
content: lines.join('\n'),
|
|
72
|
+
integrateTarget: true,
|
|
73
|
+
overwriteExisting: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
69
76
|
continue;
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -108,12 +115,14 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
108
115
|
lines.push(' end');
|
|
109
116
|
lines.push('end');
|
|
110
117
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
if (enumInScope) {
|
|
119
|
+
files.push({
|
|
120
|
+
path: `lib/workos/types/${fileName(enumDef.name)}.rb`,
|
|
121
|
+
content: lines.join('\n'),
|
|
122
|
+
integrateTarget: true,
|
|
123
|
+
overwriteExisting: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
return files;
|
package/src/ruby/models.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isListMetadataModel,
|
|
7
7
|
collectNonPaginatedResponseModelNames,
|
|
8
8
|
} from '../shared/model-utils.js';
|
|
9
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
9
10
|
|
|
10
11
|
/** Folder under lib/workos/ for models not owned by any service. */
|
|
11
12
|
export const SHARED_MODEL_DIR = 'shared';
|
|
@@ -119,12 +120,17 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
119
120
|
lines.push('module WorkOS');
|
|
120
121
|
lines.push(` ${cls} = ${canonCls}`);
|
|
121
122
|
lines.push('end');
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
// FR-1.4: write the per-model file only when in scope. Zeitwerk autoloads
|
|
124
|
+
// by path, so there is no barrel to keep in sync; out-of-scope alias files
|
|
125
|
+
// are left untouched on disk.
|
|
126
|
+
if (isModelInScope(model.name, ctx)) {
|
|
127
|
+
files.push({
|
|
128
|
+
path: `lib/workos/${dirFor(model.name)}/${file}.rb`,
|
|
129
|
+
content: lines.join('\n'),
|
|
130
|
+
integrateTarget: true,
|
|
131
|
+
overwriteExisting: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
128
134
|
continue;
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -214,12 +220,17 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
214
220
|
lines.push(' end');
|
|
215
221
|
lines.push('end');
|
|
216
222
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
// FR-1.4: write the per-model file only when in scope. Zeitwerk autoloads by
|
|
224
|
+
// path, so there is no barrel to keep in sync; out-of-scope model files are
|
|
225
|
+
// left untouched on disk.
|
|
226
|
+
if (isModelInScope(model.name, ctx)) {
|
|
227
|
+
files.push({
|
|
228
|
+
path: `lib/workos/${dirFor(model.name)}/${file}.rb`,
|
|
229
|
+
content: lines.join('\n'),
|
|
230
|
+
integrateTarget: true,
|
|
231
|
+
overwriteExisting: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
return files;
|
package/src/ruby/rbi.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
buildResolvedLookup,
|
|
17
17
|
groupByMount,
|
|
18
|
+
isMountInScope,
|
|
19
|
+
isModelInScope,
|
|
18
20
|
lookupResolved,
|
|
19
21
|
buildHiddenParams,
|
|
20
22
|
collectGroupedParamNames,
|
|
@@ -116,12 +118,17 @@ export function generateRbiFiles(spec: ApiSpec, ctx: EmitterContext): GeneratedF
|
|
|
116
118
|
lines.push(' end');
|
|
117
119
|
lines.push('end');
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
// FR-1.4: write the per-model .rbi only when in scope. The client.rbi
|
|
122
|
+
// aggregate (section 3) stays on the full set so sigs for out-of-scope
|
|
123
|
+
// services whose .rb/.rbi still exist keep resolving.
|
|
124
|
+
if (isModelInScope(model.name, ctx)) {
|
|
125
|
+
files.push({
|
|
126
|
+
path: `rbi/workos/${fileName(model.name)}.rbi`,
|
|
127
|
+
content: lines.join('\n'),
|
|
128
|
+
integrateTarget: true,
|
|
129
|
+
overwriteExisting: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
// 2. Generate service RBI files
|
|
@@ -137,6 +144,10 @@ export function generateRbiFiles(spec: ApiSpec, ctx: EmitterContext): GeneratedF
|
|
|
137
144
|
const exportedClasses = buildExportedClassNameSet(ctx);
|
|
138
145
|
|
|
139
146
|
for (const [mountTarget, group] of groups) {
|
|
147
|
+
// Scoped run: emit per-service .rbi only for selected mount targets. The
|
|
148
|
+
// client.rbi aggregate loop below intentionally stays on the FULL `groups`
|
|
149
|
+
// set so it keeps emitting sigs for every service whose .rb still exists.
|
|
150
|
+
if (!isMountInScope(mountTarget, ctx)) continue;
|
|
140
151
|
const resolvedTarget = resolveServiceTarget(mountTarget, exportedClasses);
|
|
141
152
|
const cls = className(resolvedTarget);
|
|
142
153
|
const lines: string[] = [];
|
package/src/ruby/resources.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { mapTypeRefForYard } from './type-map.js';
|
|
|
15
15
|
import {
|
|
16
16
|
buildResolvedLookup,
|
|
17
17
|
lookupResolved,
|
|
18
|
-
|
|
18
|
+
scopedMountGroups,
|
|
19
19
|
getOpDefaults,
|
|
20
20
|
getOpInferFromClient,
|
|
21
21
|
buildHiddenParams,
|
|
@@ -33,7 +33,7 @@ import { buildGroupOwnerMap, collectVariantsForMountTarget, emitInlineVariantCla
|
|
|
33
33
|
export function generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
34
34
|
const files: GeneratedFile[] = [];
|
|
35
35
|
|
|
36
|
-
const groups =
|
|
36
|
+
const groups = scopedMountGroups(ctx);
|
|
37
37
|
const lookup = buildResolvedLookup(ctx);
|
|
38
38
|
const modelNames = new Set(ctx.spec.models.map((m) => m.name));
|
|
39
39
|
const enumNames = new Set(ctx.spec.enums.map((e) => e.name));
|
package/src/ruby/tests.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from './naming.js';
|
|
14
14
|
import {
|
|
15
15
|
buildResolvedLookup,
|
|
16
|
-
|
|
16
|
+
scopedMountGroups,
|
|
17
17
|
lookupResolved,
|
|
18
18
|
buildHiddenParams,
|
|
19
19
|
collectBodyFieldTypes,
|
|
@@ -34,7 +34,7 @@ import { buildGroupOwnerMap, pickVariantParamType } from './parameter-groups.js'
|
|
|
34
34
|
export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
35
35
|
const files: GeneratedFile[] = [];
|
|
36
36
|
|
|
37
|
-
const groups =
|
|
37
|
+
const groups = scopedMountGroups(ctx);
|
|
38
38
|
const models = spec.models as Model[];
|
|
39
39
|
const modelByName = new Map<string, Model>();
|
|
40
40
|
for (const m of models) modelByName.set(m.name, m);
|
package/src/rust/enums.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { typeName, moduleName, variantName } from './naming.js';
|
|
3
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Generate one Rust source file per enum under `src/enums/`, plus a
|
|
@@ -17,7 +18,7 @@ import { typeName, moduleName, variantName } from './naming.js';
|
|
|
17
18
|
* variant and re-serialize as the canonical wire string.
|
|
18
19
|
* - `Display`, `FromStr`, and `AsRef<str>` are implemented for ergonomics.
|
|
19
20
|
*/
|
|
20
|
-
export function generateEnums(enums: Enum[],
|
|
21
|
+
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
21
22
|
const files: GeneratedFile[] = [];
|
|
22
23
|
const seen = new Set<string>();
|
|
23
24
|
const moduleNames: string[] = [];
|
|
@@ -27,8 +28,15 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
27
28
|
const mod = moduleName(e.name);
|
|
28
29
|
if (seen.has(mod)) continue;
|
|
29
30
|
seen.add(mod);
|
|
31
|
+
// The barrel (`src/enums/mod.rs`) must declare every enum's module so Rust
|
|
32
|
+
// compiles even in a scoped run — `moduleNames` is collected from the FULL
|
|
33
|
+
// enum set regardless of scope.
|
|
30
34
|
moduleNames.push(mod);
|
|
31
35
|
|
|
36
|
+
// Only the per-enum `.rs` FILE write is scoped (FR-1.4). In a scoped run we
|
|
37
|
+
// skip emitting files for out-of-scope enums, but the barrel above still
|
|
38
|
+
// declares their modules (their existing `.rs` files stay untouched on disk).
|
|
39
|
+
if (!isEnumInScope(e.name, ctx)) continue;
|
|
32
40
|
files.push({
|
|
33
41
|
path: `src/enums/${mod}.rs`,
|
|
34
42
|
content: renderEnum(e),
|
package/src/rust/models.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Model, EmitterContext, GeneratedFile, Field, TypeRef } from '@work
|
|
|
2
2
|
import { typeName, domainFieldName, moduleName } from './naming.js';
|
|
3
3
|
import { mapTypeRef, makeOptional, UnionRegistry } from './type-map.js';
|
|
4
4
|
import { applySecretRedaction } from './secret.js';
|
|
5
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
5
6
|
|
|
6
7
|
const HEADER_PLACEHOLDER = ''; // engine prepends fileHeader()
|
|
7
8
|
|
|
@@ -32,15 +33,27 @@ export function generateModels(models: Model[], ctx: EmitterContext, registry: U
|
|
|
32
33
|
const mod = moduleName(model.name);
|
|
33
34
|
if (seen.has(mod)) continue;
|
|
34
35
|
seen.add(mod);
|
|
36
|
+
// The barrel (`src/models/mod.rs`) must declare every model's module so
|
|
37
|
+
// Rust compiles even in a scoped run — `moduleNames` is collected from the
|
|
38
|
+
// FULL model set regardless of scope.
|
|
35
39
|
moduleNames.push(mod);
|
|
36
40
|
|
|
41
|
+
// renderModel registers inline unions into `registry` as a side effect, and
|
|
42
|
+
// `_unions.rs` is rendered later (in generateClient) from that registry — so
|
|
43
|
+
// it MUST run for every model, even out-of-scope ones, or scoped runs drop
|
|
44
|
+
// unions. Compute content unconditionally; only the per-model `.rs` FILE write
|
|
45
|
+
// is scoped (FR-1.4). The barrel above still declares every module, and an
|
|
46
|
+
// out-of-scope model's existing `.rs` file stays untouched on disk.
|
|
37
47
|
const hintPath = ctx.overlayLookup?.fileBySymbol?.get(model.name);
|
|
38
48
|
const path = hintPath ?? `src/models/${mod}.rs`;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
const content = renderModel(model, registry, taggedVariantFields.get(model.name));
|
|
50
|
+
if (isModelInScope(model.name, ctx)) {
|
|
51
|
+
files.push({
|
|
52
|
+
path,
|
|
53
|
+
content,
|
|
54
|
+
overwriteExisting: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
// Always include the unions module in the barrel so downstream stages
|
package/src/rust/resources.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { fieldName, domainFieldName, methodName, typeName, moduleName, variantNa
|
|
|
14
14
|
import { mapTypeRef, makeOptional, UnionRegistry } from './type-map.js';
|
|
15
15
|
import { applySecretRedaction } from './secret.js';
|
|
16
16
|
import { parsePathTemplate } from '../shared/path-template.js';
|
|
17
|
-
import { groupByMount, buildResolvedLookup } from '../shared/resolved-ops.js';
|
|
17
|
+
import { groupByMount, buildResolvedLookup, isMountInScope } from '../shared/resolved-ops.js';
|
|
18
18
|
import { resolveWrapperParams, type ResolvedWrapperParam } from '../shared/wrapper-utils.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -32,7 +32,14 @@ export function generateResources(_services: Service[], ctx: EmitterContext, reg
|
|
|
32
32
|
if (group.operations.length === 0) continue;
|
|
33
33
|
const basename = moduleName(mountName);
|
|
34
34
|
const struct = mountStructName(mountName);
|
|
35
|
+
// The barrel (`src/resources/mod.rs`) must list every mount's module so
|
|
36
|
+
// Rust compiles even in a scoped run — `exports` is collected from the
|
|
37
|
+
// FULL groupByMount set regardless of scope.
|
|
35
38
|
exports.push({ module: basename, struct });
|
|
39
|
+
// Only the per-service resource `.rs` FILE write is scoped. In a scoped
|
|
40
|
+
// run we skip emitting files for out-of-scope mounts, but the barrel above
|
|
41
|
+
// still references their modules (their existing `.rs` files stay on disk).
|
|
42
|
+
if (!isMountInScope(mountName, ctx)) continue;
|
|
36
43
|
files.push({
|
|
37
44
|
path: `src/resources/${basename}.rs`,
|
|
38
45
|
content: renderMountGroup(mountName, group.resolvedOps, ctx, registry, lookup),
|
package/src/rust/tests.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
TypeRef,
|
|
12
12
|
} from '@workos/oagen';
|
|
13
13
|
import { methodName, moduleName, typeName } from './naming.js';
|
|
14
|
-
import {
|
|
14
|
+
import { scopedMountGroups } from '../shared/resolved-ops.js';
|
|
15
15
|
import { exampleFor, generateFixtures } from './fixtures.js';
|
|
16
16
|
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
17
17
|
import { isInlineEnvelopeList } from './resources.js';
|
|
@@ -43,7 +43,7 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
43
43
|
overwriteExisting: true,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
const groups =
|
|
46
|
+
const groups = scopedMountGroups(ctx);
|
|
47
47
|
const modelMap = new Map(spec.models.map((m) => [m.name, m]));
|
|
48
48
|
const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
|
|
49
49
|
|
|
@@ -94,6 +94,53 @@ export function groupByMount(ctx: EmitterContext): Map<string, MountGroup> {
|
|
|
94
94
|
return groups;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Like {@link groupByMount}, but for a scoped (`--services`) run returns ONLY the
|
|
99
|
+
* mount groups the run selected (`ctx.scopedServices`, POST-MOUNT names). When
|
|
100
|
+
* scoping is inactive the full set is returned unchanged.
|
|
101
|
+
*
|
|
102
|
+
* Use this for PER-SERVICE resource/test emission. Do NOT use it for
|
|
103
|
+
* aggregate/barrel files (Rust `mod.rs`, Ruby `client.rbi`, the root client) —
|
|
104
|
+
* those must continue to list every service, so they keep calling
|
|
105
|
+
* {@link groupByMount} over the full set; otherwise a scoped run would drop
|
|
106
|
+
* sibling modules and break the build/type-check.
|
|
107
|
+
*/
|
|
108
|
+
export function scopedMountGroups(ctx: EmitterContext): Map<string, MountGroup> {
|
|
109
|
+
const groups = groupByMount(ctx);
|
|
110
|
+
const scope = ctx.scopedServices;
|
|
111
|
+
if (!scope || scope.size === 0) return groups;
|
|
112
|
+
return new Map([...groups].filter(([mountName]) => scope.has(mountName)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* True when a POST-MOUNT service name should be emitted in the current run.
|
|
117
|
+
* Inactive scoping (no `ctx.scopedServices`) ⇒ everything is in scope. Use this
|
|
118
|
+
* for inline per-service gates (e.g. manifest loops keyed by `getMountTarget`).
|
|
119
|
+
*/
|
|
120
|
+
export function isMountInScope(mountName: string, ctx: EmitterContext): boolean {
|
|
121
|
+
const scope = ctx.scopedServices;
|
|
122
|
+
return !scope || scope.size === 0 || scope.has(mountName);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* True when a MODEL's per-model FILE should be written in the current run (FR-1.4).
|
|
127
|
+
* A scoped run sets `ctx.scopedModelNames` to the models reachable from the
|
|
128
|
+
* selected services; out-of-scope models are left untouched on disk. Inactive
|
|
129
|
+
* scoping ⇒ everything is in scope. NOTE: gate only the per-model FILE write —
|
|
130
|
+
* the model must still appear in barrels/indexes (built from the full set) so the
|
|
131
|
+
* untouched on-disk file stays importable.
|
|
132
|
+
*/
|
|
133
|
+
export function isModelInScope(modelName: string, ctx: EmitterContext): boolean {
|
|
134
|
+
const scope = ctx.scopedModelNames;
|
|
135
|
+
return !scope || scope.has(modelName);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Like {@link isModelInScope} but for an ENUM's per-enum file (`ctx.scopedEnumNames`). */
|
|
139
|
+
export function isEnumInScope(enumName: string, ctx: EmitterContext): boolean {
|
|
140
|
+
const scope = ctx.scopedEnumNames;
|
|
141
|
+
return !scope || scope.has(enumName);
|
|
142
|
+
}
|
|
143
|
+
|
|
97
144
|
/**
|
|
98
145
|
* Get the mount target for an IR service.
|
|
99
146
|
* Checks the first resolved operation that belongs to this service.
|