@workos/oagen-emitters 0.4.0 → 0.6.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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +35 -224
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -15234
- package/dist/plugin-Dws9b6T7.mjs +21441 -0
- package/dist/plugin-Dws9b6T7.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +5 -5
- package/oagen.config.ts +5 -373
- package/package.json +17 -41
- package/smoke/sdk-dotnet.ts +11 -5
- package/smoke/sdk-elixir.ts +11 -5
- package/smoke/sdk-go.ts +10 -4
- package/smoke/sdk-kotlin.ts +11 -5
- package/smoke/sdk-node.ts +11 -5
- package/smoke/sdk-php.ts +9 -4
- package/smoke/sdk-python.ts +10 -4
- package/smoke/sdk-ruby.ts +10 -4
- package/smoke/sdk-rust.ts +11 -5
- package/src/dotnet/index.ts +9 -7
- package/src/dotnet/manifest.ts +5 -11
- package/src/dotnet/models.ts +58 -82
- package/src/dotnet/naming.ts +44 -6
- package/src/dotnet/resources.ts +350 -29
- package/src/dotnet/tests.ts +44 -24
- package/src/dotnet/type-map.ts +44 -17
- package/src/dotnet/wrappers.ts +21 -10
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +13 -8
- package/src/go/manifest.ts +5 -11
- package/src/go/models.ts +6 -1
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +14 -0
- package/src/kotlin/client.ts +7 -2
- package/src/kotlin/enums.ts +30 -3
- package/src/kotlin/index.ts +3 -3
- package/src/kotlin/manifest.ts +9 -15
- package/src/kotlin/models.ts +97 -6
- package/src/kotlin/naming.ts +7 -1
- package/src/kotlin/resources.ts +370 -39
- package/src/kotlin/tests.ts +120 -6
- package/src/node/client.ts +38 -11
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/index.ts +3 -3
- package/src/node/manifest.ts +4 -11
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +156 -52
- package/src/node/tests.ts +76 -27
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/index.ts +3 -3
- package/src/php/manifest.ts +5 -11
- package/src/php/models.ts +0 -33
- package/src/php/resources.ts +199 -18
- package/src/php/tests.ts +26 -2
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +6 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +13 -3
- package/src/python/enums.ts +28 -3
- package/src/python/index.ts +38 -30
- package/src/python/manifest.ts +5 -12
- package/src/python/models.ts +138 -1
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +28 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +131 -7
- package/src/shared/naming-utils.ts +36 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/test/dotnet/client.test.ts +2 -2
- package/test/dotnet/manifest.test.ts +13 -12
- package/test/dotnet/models.test.ts +7 -9
- package/test/dotnet/resources.test.ts +135 -3
- package/test/dotnet/tests.test.ts +5 -5
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +1 -1
- package/test/kotlin/resources.test.ts +210 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +134 -26
- package/test/node/utils.test.ts +140 -0
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +66 -1
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/manifest.test.ts +7 -7
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsconfig.json +1 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
package/smoke/sdk-php.ts
CHANGED
|
@@ -166,14 +166,19 @@ class CaptureProxy {
|
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
167
|
|
|
168
168
|
function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
|
|
169
|
-
const manifestPath = resolve(sdkPath, '
|
|
169
|
+
const manifestPath = resolve(sdkPath, '.oagen-manifest.json');
|
|
170
170
|
if (!existsSync(manifestPath)) {
|
|
171
|
-
console.warn(`Warning: No
|
|
171
|
+
console.warn(`Warning: No .oagen-manifest.json found at ${manifestPath}`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
175
|
+
const operations = parsed?.operations;
|
|
176
|
+
if (!operations || typeof operations !== 'object') {
|
|
177
|
+
console.warn('Warning: .oagen-manifest.json has no operations field');
|
|
172
178
|
return null;
|
|
173
179
|
}
|
|
174
|
-
const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
175
180
|
const manifest = new Map<string, ManifestEntry>();
|
|
176
|
-
for (const [httpKey, entry] of Object.entries(
|
|
181
|
+
for (const [httpKey, entry] of Object.entries(operations)) {
|
|
177
182
|
manifest.set(httpKey, entry as ManifestEntry);
|
|
178
183
|
}
|
|
179
184
|
return manifest;
|
package/smoke/sdk-python.ts
CHANGED
|
@@ -191,15 +191,21 @@ function createProxyServer(
|
|
|
191
191
|
// ---------------------------------------------------------------------------
|
|
192
192
|
|
|
193
193
|
function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
|
|
194
|
-
const manifestPath = resolve(sdkPath, '
|
|
194
|
+
const manifestPath = resolve(sdkPath, '.oagen-manifest.json');
|
|
195
195
|
if (!existsSync(manifestPath)) {
|
|
196
|
-
console.warn(`Warning: No
|
|
196
|
+
console.warn(`Warning: No .oagen-manifest.json found at ${manifestPath}`);
|
|
197
|
+
console.warn(' Method resolution will rely on heuristic tiers -- most operations may be skipped.');
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
201
|
+
const operations = parsed?.operations;
|
|
202
|
+
if (!operations || typeof operations !== 'object') {
|
|
203
|
+
console.warn('Warning: .oagen-manifest.json has no operations field');
|
|
197
204
|
console.warn(' Method resolution will rely on heuristic tiers -- most operations may be skipped.');
|
|
198
205
|
return null;
|
|
199
206
|
}
|
|
200
|
-
const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
201
207
|
const manifest = new Map<string, ManifestEntry>();
|
|
202
|
-
for (const [httpKey, entry] of Object.entries(
|
|
208
|
+
for (const [httpKey, entry] of Object.entries(operations)) {
|
|
203
209
|
manifest.set(httpKey, entry as ManifestEntry);
|
|
204
210
|
}
|
|
205
211
|
return manifest;
|
package/smoke/sdk-ruby.ts
CHANGED
|
@@ -78,15 +78,21 @@ interface MethodResolution {
|
|
|
78
78
|
// ---------------------------------------------------------------------------
|
|
79
79
|
|
|
80
80
|
function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
|
|
81
|
-
const manifestPath = resolve(sdkPath, '
|
|
81
|
+
const manifestPath = resolve(sdkPath, '.oagen-manifest.json');
|
|
82
82
|
if (!existsSync(manifestPath)) {
|
|
83
|
-
console.warn(`Warning: No
|
|
83
|
+
console.warn(`Warning: No .oagen-manifest.json found at ${manifestPath}`);
|
|
84
|
+
console.warn(' Method resolution will rely on heuristic tiers -- most operations may be skipped.');
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
88
|
+
const operations = parsed?.operations;
|
|
89
|
+
if (!operations || typeof operations !== 'object') {
|
|
90
|
+
console.warn('Warning: .oagen-manifest.json has no operations field');
|
|
84
91
|
console.warn(' Method resolution will rely on heuristic tiers -- most operations may be skipped.');
|
|
85
92
|
return null;
|
|
86
93
|
}
|
|
87
|
-
const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
88
94
|
const manifest = new Map<string, ManifestEntry>();
|
|
89
|
-
for (const [httpKey, entry] of Object.entries(
|
|
95
|
+
for (const [httpKey, entry] of Object.entries(operations)) {
|
|
90
96
|
manifest.set(httpKey, entry as ManifestEntry);
|
|
91
97
|
}
|
|
92
98
|
return manifest;
|
package/smoke/sdk-rust.ts
CHANGED
|
@@ -167,15 +167,21 @@ function createProxyServer(
|
|
|
167
167
|
// ---------------------------------------------------------------------------
|
|
168
168
|
|
|
169
169
|
function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
|
|
170
|
-
const manifestPath = resolve(sdkPath, '
|
|
170
|
+
const manifestPath = resolve(sdkPath, '.oagen-manifest.json');
|
|
171
171
|
if (!existsSync(manifestPath)) {
|
|
172
|
-
console.warn(`Warning: No
|
|
173
|
-
console.warn(' Method resolution will rely on heuristic tiers
|
|
172
|
+
console.warn(`Warning: No .oagen-manifest.json found at ${manifestPath}`);
|
|
173
|
+
console.warn(' Method resolution will rely on heuristic tiers -- most operations may be skipped.');
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
177
|
+
const operations = parsed?.operations;
|
|
178
|
+
if (!operations || typeof operations !== 'object') {
|
|
179
|
+
console.warn('Warning: .oagen-manifest.json has no operations field');
|
|
180
|
+
console.warn(' Method resolution will rely on heuristic tiers -- most operations may be skipped.');
|
|
174
181
|
return null;
|
|
175
182
|
}
|
|
176
|
-
const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
177
183
|
const manifest = new Map<string, ManifestEntry>();
|
|
178
|
-
for (const [httpKey, entry] of Object.entries(
|
|
184
|
+
for (const [httpKey, entry] of Object.entries(operations)) {
|
|
179
185
|
manifest.set(httpKey, entry as ManifestEntry);
|
|
180
186
|
}
|
|
181
187
|
return manifest;
|
package/src/dotnet/index.ts
CHANGED
|
@@ -11,13 +11,13 @@ import type {
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
|
|
14
|
-
import { generateModels } from './models.js';
|
|
14
|
+
import { generateModels, primeModelAliases } from './models.js';
|
|
15
15
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
16
16
|
import { generateEnums, primeEnumAliases } from './enums.js';
|
|
17
17
|
import { generateResources } from './resources.js';
|
|
18
18
|
import { generateClient } from './client.js';
|
|
19
19
|
import { generateTests } from './tests.js';
|
|
20
|
-
import {
|
|
20
|
+
import { buildOperationsMap } from './manifest.js';
|
|
21
21
|
import { generateWrapperOptionsClasses } from './wrappers.js';
|
|
22
22
|
import { groupByMount } from '../shared/resolved-ops.js';
|
|
23
23
|
import { discriminatedUnions } from './type-map.js';
|
|
@@ -151,6 +151,7 @@ export const dotnetEmitter: Emitter = {
|
|
|
151
151
|
const c = fixNamespace(ctx);
|
|
152
152
|
const synEnums = getSyntheticEnums();
|
|
153
153
|
primeEnumAliases(synEnums.length > 0 ? [...c.spec.enums, ...synEnums] : c.spec.enums);
|
|
154
|
+
primeModelAliases(enrichModelsFromSpec(c.spec.models));
|
|
154
155
|
const files = generateResources(services, c);
|
|
155
156
|
|
|
156
157
|
// Also generate wrapper options classes
|
|
@@ -201,11 +202,12 @@ export const dotnetEmitter: Emitter = {
|
|
|
201
202
|
const c = fixNamespace(ctx);
|
|
202
203
|
const synEnumsForTests = getSyntheticEnums();
|
|
203
204
|
primeEnumAliases(synEnumsForTests.length > 0 ? [...spec.enums, ...synEnumsForTests] : spec.enums);
|
|
205
|
+
primeModelAliases(enrichModelsFromSpec(c.spec.models));
|
|
204
206
|
return prefixTestPaths(ensureTrailingNewlines(generateTests(spec, c)));
|
|
205
207
|
},
|
|
206
208
|
|
|
207
|
-
|
|
208
|
-
return
|
|
209
|
+
buildOperationsMap(spec: ApiSpec, ctx: EmitterContext) {
|
|
210
|
+
return buildOperationsMap(spec, fixNamespace(ctx));
|
|
209
211
|
},
|
|
210
212
|
|
|
211
213
|
fileHeader(): string {
|
|
@@ -229,7 +231,7 @@ export const dotnetEmitter: Emitter = {
|
|
|
229
231
|
args: ['format', workspace, '--no-restore', '--include'],
|
|
230
232
|
// Keep batches small enough to stay under argv length limits while
|
|
231
233
|
// still amortizing MSBuild startup across many files.
|
|
232
|
-
batchSize:
|
|
234
|
+
batchSize: 500,
|
|
233
235
|
};
|
|
234
236
|
},
|
|
235
237
|
};
|
|
@@ -239,8 +241,8 @@ function findDotnetWorkspace(targetDir: string): string | null {
|
|
|
239
241
|
if (!fs.existsSync(targetDir)) return null;
|
|
240
242
|
const entries = fs.readdirSync(targetDir);
|
|
241
243
|
const sln = entries.find((e) => e.endsWith('.sln') || e.endsWith('.slnx'));
|
|
242
|
-
if (sln) return path.
|
|
244
|
+
if (sln) return path.resolve(targetDir, sln);
|
|
243
245
|
const csproj = entries.find((e) => e.endsWith('.csproj'));
|
|
244
|
-
if (csproj) return path.
|
|
246
|
+
if (csproj) return path.resolve(targetDir, csproj);
|
|
245
247
|
return null;
|
|
246
248
|
}
|
package/src/dotnet/manifest.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { ApiSpec, EmitterContext,
|
|
1
|
+
import type { ApiSpec, EmitterContext, OperationsMap } from '@workos/oagen';
|
|
2
2
|
import { resolveMethodName } from './naming.js';
|
|
3
3
|
import { buildServiceAccessPaths } from './client.js';
|
|
4
4
|
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Build operation-to-SDK-method mapping for the manifest.
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
10
|
-
const manifest:
|
|
9
|
+
export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): OperationsMap {
|
|
10
|
+
const manifest: OperationsMap = {};
|
|
11
11
|
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
12
12
|
|
|
13
13
|
for (const service of spec.services) {
|
|
@@ -26,11 +26,5 @@ export function generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedF
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
return
|
|
30
|
-
{
|
|
31
|
-
path: 'smoke-manifest.json',
|
|
32
|
-
content: JSON.stringify(manifest, null, 2),
|
|
33
|
-
integrateTarget: false,
|
|
34
|
-
},
|
|
35
|
-
];
|
|
29
|
+
return manifest;
|
|
36
30
|
}
|
package/src/dotnet/models.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { Model, EmitterContext, GeneratedFile, TypeRef } from '@workos/oagen';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mapTypeRef,
|
|
4
|
+
isValueTypeRef,
|
|
5
|
+
isEnumRef,
|
|
6
|
+
emitJsonPropertyAttributes,
|
|
7
|
+
setModelAliases,
|
|
8
|
+
isModelAlias,
|
|
9
|
+
} from './type-map.js';
|
|
3
10
|
import {
|
|
4
11
|
articleFor,
|
|
5
|
-
className,
|
|
6
|
-
escapeXml,
|
|
7
12
|
fieldName,
|
|
8
13
|
humanize,
|
|
9
14
|
emitXmlDoc,
|
|
10
15
|
deprecationMessage,
|
|
11
16
|
escapeCsAttributeString,
|
|
17
|
+
modelClassName,
|
|
12
18
|
} from './naming.js';
|
|
13
19
|
|
|
14
20
|
// Import and re-export shared model detection utilities
|
|
@@ -35,90 +41,23 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
35
41
|
|
|
36
42
|
const files: GeneratedFile[] = [];
|
|
37
43
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
// an already-aliased child type also collapse. Terminates when a full
|
|
41
|
-
// round produces no new aliases.
|
|
42
|
-
const eligibleModels = models.filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m));
|
|
43
|
-
const aliasOf = new Map<string, string>();
|
|
44
|
-
while (true) {
|
|
45
|
-
const hashGroups = new Map<string, string[]>();
|
|
46
|
-
for (const model of eligibleModels) {
|
|
47
|
-
const hash = structuralHash(model, aliasOf);
|
|
48
|
-
if (!hashGroups.has(hash)) hashGroups.set(hash, []);
|
|
49
|
-
hashGroups.get(hash)!.push(model.name);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let added = false;
|
|
53
|
-
for (const [hash, names] of hashGroups) {
|
|
54
|
-
if (names.length <= 1) continue;
|
|
55
|
-
if (hash === '') continue;
|
|
56
|
-
const sorted = [...names].sort();
|
|
57
|
-
const canonical = sorted[0];
|
|
58
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
59
|
-
const name = sorted[i];
|
|
60
|
-
if (aliasOf.get(name) !== canonical) {
|
|
61
|
-
aliasOf.set(name, canonical);
|
|
62
|
-
added = true;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (!added) break;
|
|
67
|
-
}
|
|
44
|
+
// Compute and publish model aliases so mapTypeRef rewrites references.
|
|
45
|
+
primeModelAliases(models);
|
|
68
46
|
|
|
69
47
|
for (const model of models) {
|
|
70
48
|
if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
|
|
71
49
|
|
|
72
|
-
const csClassName =
|
|
73
|
-
const canonicalName = aliasOf.get(model.name);
|
|
74
|
-
|
|
75
|
-
if (canonicalName) {
|
|
76
|
-
// Emit alias as subclass of canonical
|
|
77
|
-
const canonicalClass = className(canonicalName);
|
|
78
|
-
const lines: string[] = [];
|
|
79
|
-
lines.push(`namespace ${ctx.namespacePascal}`);
|
|
80
|
-
lines.push('{');
|
|
81
|
-
if (model.description) {
|
|
82
|
-
const descLines = model.description
|
|
83
|
-
.split('\n')
|
|
84
|
-
.map((l) => l.trim())
|
|
85
|
-
.filter((l) => l);
|
|
86
|
-
lines.push(` /// <summary>${escapeXml(descLines[0])}</summary>`);
|
|
87
|
-
if (descLines.length > 1) {
|
|
88
|
-
lines.push(` /// <remarks>`);
|
|
89
|
-
for (const remark of descLines.slice(1)) {
|
|
90
|
-
lines.push(` /// ${escapeXml(remark)}`);
|
|
91
|
-
}
|
|
92
|
-
lines.push(` /// Structurally identical to <see cref="${canonicalClass}"/>.`);
|
|
93
|
-
lines.push(` /// </remarks>`);
|
|
94
|
-
} else {
|
|
95
|
-
lines.push(` /// <remarks>Structurally identical to <see cref="${canonicalClass}"/>.</remarks>`);
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
const human = humanize(model.name);
|
|
99
|
-
lines.push(` /// <summary>Represents ${articleFor(human)} ${human}.</summary>`);
|
|
100
|
-
lines.push(` /// <remarks>Structurally identical to <see cref="${canonicalClass}"/>.</remarks>`);
|
|
101
|
-
}
|
|
102
|
-
lines.push(` public class ${csClassName} : ${canonicalClass} { }`);
|
|
103
|
-
lines.push('}');
|
|
50
|
+
const csClassName = modelClassName(model.name);
|
|
104
51
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
overwriteExisting: true,
|
|
109
|
-
});
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
52
|
+
// Skip alias models — all references are already rewritten to the
|
|
53
|
+
// canonical type by mapTypeRef, so the alias class would be dead code.
|
|
54
|
+
if (isModelAlias(model.name)) continue;
|
|
112
55
|
|
|
113
56
|
const lines: string[] = [];
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const needsSystem = model.fields.some((f) => {
|
|
119
|
-
const csType = mapTypeRef(f.type);
|
|
120
|
-
return csType.includes('DateTimeOffset');
|
|
121
|
-
});
|
|
57
|
+
const fieldTypes = model.fields.map((f) => mapTypeRef(f.type));
|
|
58
|
+
const needsCollections = fieldTypes.some((t) => t.startsWith('List<') || t.startsWith('Dictionary<'));
|
|
59
|
+
const needsSystem = fieldTypes.some((t) => t.includes('DateTimeOffset'));
|
|
60
|
+
const needsJsonAttrs = model.fields.some((f) => f.required && isEnumRef(f.type));
|
|
122
61
|
|
|
123
62
|
lines.push(`namespace ${ctx.namespacePascal}`);
|
|
124
63
|
lines.push('{');
|
|
@@ -128,8 +67,10 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
128
67
|
if (needsCollections) {
|
|
129
68
|
lines.push(' using System.Collections.Generic;');
|
|
130
69
|
}
|
|
131
|
-
|
|
132
|
-
|
|
70
|
+
if (needsJsonAttrs) {
|
|
71
|
+
lines.push(' using Newtonsoft.Json;');
|
|
72
|
+
lines.push(' using STJS = System.Text.Json.Serialization;');
|
|
73
|
+
}
|
|
133
74
|
lines.push('');
|
|
134
75
|
|
|
135
76
|
// XML doc comment
|
|
@@ -305,6 +246,41 @@ function singleValueConstInitializer(ref: TypeRef, enumConstByName: Map<string,
|
|
|
305
246
|
return JSON.stringify(wire);
|
|
306
247
|
}
|
|
307
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Compute and publish the model alias map. Safe to call multiple times
|
|
251
|
+
* (idempotent for a given set of models). Must be invoked before any emitter
|
|
252
|
+
* phase that calls `mapTypeRef` with model references.
|
|
253
|
+
*/
|
|
254
|
+
export function primeModelAliases(models: Model[]): void {
|
|
255
|
+
const eligibleModels = models.filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m));
|
|
256
|
+
const aliasOf = new Map<string, string>();
|
|
257
|
+
while (true) {
|
|
258
|
+
const hashGroups = new Map<string, string[]>();
|
|
259
|
+
for (const model of eligibleModels) {
|
|
260
|
+
const hash = structuralHash(model, aliasOf);
|
|
261
|
+
if (!hashGroups.has(hash)) hashGroups.set(hash, []);
|
|
262
|
+
hashGroups.get(hash)!.push(model.name);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let added = false;
|
|
266
|
+
for (const [hash, names] of hashGroups) {
|
|
267
|
+
if (names.length <= 1) continue;
|
|
268
|
+
if (hash === '') continue;
|
|
269
|
+
const sorted = [...names].sort();
|
|
270
|
+
const canonical = sorted[0];
|
|
271
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
272
|
+
const name = sorted[i];
|
|
273
|
+
if (aliasOf.get(name) !== canonical) {
|
|
274
|
+
aliasOf.set(name, canonical);
|
|
275
|
+
added = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!added) break;
|
|
280
|
+
}
|
|
281
|
+
setModelAliases(aliasOf);
|
|
282
|
+
}
|
|
283
|
+
|
|
308
284
|
/**
|
|
309
285
|
* Normalize a TypeRef for structural comparison.
|
|
310
286
|
* Enum references are normalized to their values (not names) so that
|
package/src/dotnet/naming.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Operation, Service, EmitterContext } from '@workos/oagen';
|
|
2
2
|
import { toPascalCase, toSnakeCase } from '@workos/oagen';
|
|
3
|
-
import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
|
|
3
|
+
import { buildResolvedLookup, lookupMethodName, lookupResolved, getMountTarget } from '../shared/resolved-ops.js';
|
|
4
4
|
import { stripUrnPrefix } from '../shared/naming-utils.js';
|
|
5
5
|
|
|
6
6
|
/** PascalCase class/type name. */
|
|
@@ -8,6 +8,17 @@ export function className(name: string): string {
|
|
|
8
8
|
return toPascalCase(stripUrnPrefix(name));
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** Display name for a model type, including consumer-friendly aliases. */
|
|
12
|
+
export function modelClassName(name: string): string {
|
|
13
|
+
switch (name) {
|
|
14
|
+
case 'EmailChangeConfirmationUser':
|
|
15
|
+
case 'UserlandUser':
|
|
16
|
+
return 'User';
|
|
17
|
+
default:
|
|
18
|
+
return className(name);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/** PascalCase file name (without extension). */
|
|
12
23
|
export function fileName(name: string): string {
|
|
13
24
|
return toPascalCase(stripUrnPrefix(name));
|
|
@@ -57,15 +68,42 @@ export function resolveServiceDir(resolvedServiceName: string): string {
|
|
|
57
68
|
return moduleName(resolvedServiceName);
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
function trimAsyncSuffix(name: string): string {
|
|
72
|
+
return name.endsWith('Async') ? name.slice(0, -5) : name;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Append Async once for TAP-style method names. */
|
|
76
|
+
export function appendAsyncSuffix(name: string): string {
|
|
77
|
+
return name.endsWith('Async') ? name : `${name}Async`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Resolve the stable method stem for an operation (without any Async suffix). */
|
|
81
|
+
export function resolveMethodStem(op: Operation, _service: Service, ctx: EmitterContext): string {
|
|
62
82
|
const lookup = buildResolvedLookup(ctx);
|
|
63
83
|
const resolved = lookupMethodName(op, lookup);
|
|
64
|
-
if (resolved)
|
|
84
|
+
if (resolved) {
|
|
85
|
+
return trimMountedResourceFromMethod(methodName(trimAsyncSuffix(resolved)), resolveClassName(_service, ctx));
|
|
86
|
+
}
|
|
65
87
|
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
66
88
|
const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
|
|
67
|
-
if (existing)
|
|
68
|
-
|
|
89
|
+
if (existing) {
|
|
90
|
+
return trimMountedResourceFromMethod(
|
|
91
|
+
methodName(trimAsyncSuffix(existing.methodName)),
|
|
92
|
+
resolveClassName(_service, ctx),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return trimMountedResourceFromMethod(methodName(trimAsyncSuffix(op.name)), resolveClassName(_service, ctx));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Resolve the SDK method name for an operation. */
|
|
99
|
+
export function resolveMethodName(op: Operation, service: Service, ctx: EmitterContext): string {
|
|
100
|
+
const stem = resolveMethodStem(op, service, ctx);
|
|
101
|
+
const resolved = lookupResolved(op, buildResolvedLookup(ctx));
|
|
102
|
+
if (resolved?.urlBuilder ?? false) {
|
|
103
|
+
return stem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return appendAsyncSuffix(stem);
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
/** Resolve the SDK class name for a service. */
|