@workos/oagen-emitters 0.4.0 → 0.5.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 +8 -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-BSop9f9z.mjs +21471 -0
- package/dist/plugin-BSop9f9z.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 +10 -34
- package/src/dotnet/index.ts +6 -4
- 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 +10 -5
- 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/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/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/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 +35 -27
- 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 +35 -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/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/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/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/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. */
|