@workos/oagen-emitters 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11893 -3226
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +298 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +3 -0
- package/src/node/client.ts +78 -115
- package/src/node/enums.ts +9 -0
- package/src/node/errors.ts +37 -232
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +9 -1
- package/src/node/index.ts +2 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +374 -364
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +32 -12
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +13 -71
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +171 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +209 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/node/client.test.ts +18 -12
- package/test/node/enums.test.ts +2 -0
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +2 -0
- package/test/node/naming.test.ts +23 -0
- package/test/node/resources.test.ts +99 -69
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +644 -0
- package/test/php/tests.test.ts +118 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -746
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { Service, Operation, EmitterContext, Enum } from '@workos/oagen';
|
|
2
|
+
import { toPascalCase, toCamelCase, toSnakeCase } from '@workos/oagen';
|
|
3
|
+
import { buildResolvedLookup, lookupMethodName } from '../shared/resolved-ops.js';
|
|
4
|
+
import { stripUrnPrefix } from '../shared/naming-utils.js';
|
|
5
|
+
|
|
6
|
+
/** Namespace grouping result (shared with client.ts). */
|
|
7
|
+
export interface NamespaceGroup {
|
|
8
|
+
prefix: string;
|
|
9
|
+
entries: { service: Service; subProp: string; resolvedName: string }[];
|
|
10
|
+
baseEntry?: { service: Service; resolvedName: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Grouping result returned by groupServicesByNamespace. */
|
|
14
|
+
export interface NamespaceGrouping {
|
|
15
|
+
standalone: { service: Service; prop: string; resolvedName: string }[];
|
|
16
|
+
namespaces: NamespaceGroup[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Map of lowercase acronym forms to their correct casing.
|
|
21
|
+
*/
|
|
22
|
+
const ACRONYM_FIXES: [RegExp, string][] = [
|
|
23
|
+
[/Workos/g, 'WorkOS'],
|
|
24
|
+
[/Sso/g, 'SSO'],
|
|
25
|
+
[/Mfa/g, 'MFA'],
|
|
26
|
+
[/Jwt/g, 'JWT'],
|
|
27
|
+
[/Cors/g, 'CORS'],
|
|
28
|
+
[/Saml/g, 'SAML'],
|
|
29
|
+
[/Scim/g, 'SCIM'],
|
|
30
|
+
[/Rbac/g, 'RBAC'],
|
|
31
|
+
[/Oauth/g, 'OAuth'],
|
|
32
|
+
[/Oidc/g, 'OIDC'],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* PHP reserved class names that would collide with builtins.
|
|
37
|
+
*/
|
|
38
|
+
const PHP_RESERVED_CLASS_NAMES = new Set([
|
|
39
|
+
'Array',
|
|
40
|
+
'List',
|
|
41
|
+
'Callable',
|
|
42
|
+
'Iterable',
|
|
43
|
+
'Mixed',
|
|
44
|
+
'Never',
|
|
45
|
+
'Null',
|
|
46
|
+
'Object',
|
|
47
|
+
'Self',
|
|
48
|
+
'Static',
|
|
49
|
+
'Void',
|
|
50
|
+
'True',
|
|
51
|
+
'False',
|
|
52
|
+
'Int',
|
|
53
|
+
'Float',
|
|
54
|
+
'String',
|
|
55
|
+
'Bool',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// ─── Enum deduplication ───────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
let enumAliasMap = new Map<string, string>();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize enum deduplication by hashing sorted enum case values.
|
|
64
|
+
* Enums with identical value sets are aliased to the one with the shortest PHP class name.
|
|
65
|
+
*/
|
|
66
|
+
export function initializeEnumDedup(enums: Enum[]): void {
|
|
67
|
+
enumAliasMap = new Map();
|
|
68
|
+
const groups = new Map<string, Enum[]>();
|
|
69
|
+
|
|
70
|
+
for (const e of enums) {
|
|
71
|
+
const hash = [...e.values]
|
|
72
|
+
.sort((a, b) => String(a.value).localeCompare(String(b.value)))
|
|
73
|
+
.map((v) => String(v.value))
|
|
74
|
+
.join('\0');
|
|
75
|
+
if (!groups.has(hash)) groups.set(hash, []);
|
|
76
|
+
groups.get(hash)!.push(e);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const [, group] of groups) {
|
|
80
|
+
if (group.length <= 1) continue;
|
|
81
|
+
// Pick shortest PHP class name as canonical
|
|
82
|
+
const sorted = [...group].sort((a, b) => className(a.name).length - className(b.name).length);
|
|
83
|
+
const canonical = sorted[0];
|
|
84
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
85
|
+
enumAliasMap.set(sorted[i].name, canonical.name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Resolve an enum name to its canonical (deduplicated) name. */
|
|
91
|
+
export function resolveEnumName(name: string): string {
|
|
92
|
+
return enumAliasMap.get(name) ?? name;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** PHP class name for an enum, with dedup resolution + PascalCase + acronym preservation. */
|
|
96
|
+
export function enumClassName(name: string): string {
|
|
97
|
+
return className(resolveEnumName(name));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** PascalCase class name with acronym preservation. */
|
|
101
|
+
export function className(name: string): string {
|
|
102
|
+
let result = toPascalCase(stripUrnPrefix(name));
|
|
103
|
+
for (const [pattern, replacement] of ACRONYM_FIXES) {
|
|
104
|
+
result = result.replace(pattern, replacement);
|
|
105
|
+
}
|
|
106
|
+
if (PHP_RESERVED_CLASS_NAMES.has(result)) {
|
|
107
|
+
result += 'Model';
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** PascalCase file name (without extension) — PSR-4 convention. */
|
|
113
|
+
export function fileName(name: string): string {
|
|
114
|
+
return className(name);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** camelCase method name. */
|
|
118
|
+
export function methodName(name: string): string {
|
|
119
|
+
return toCamelCase(name);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Resolve the SDK method name for an operation, using resolved operations first. */
|
|
123
|
+
export function resolveMethodName(op: Operation, _service: Service, ctx: EmitterContext): string {
|
|
124
|
+
const lookup = buildResolvedLookup(ctx);
|
|
125
|
+
const resolved = lookupMethodName(op, lookup);
|
|
126
|
+
if (resolved) return toCamelCase(resolved);
|
|
127
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
128
|
+
const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
|
|
129
|
+
if (existing) return existing.methodName;
|
|
130
|
+
return toCamelCase(op.name);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** camelCase field/property name. */
|
|
134
|
+
export function fieldName(name: string): string {
|
|
135
|
+
return toCamelCase(name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** snake_case name for fixtures and other snake_case contexts. */
|
|
139
|
+
export function snakeName(name: string): string {
|
|
140
|
+
return toSnakeCase(stripUrnPrefix(name));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** snake_case wire name (preserves the original API field name). */
|
|
144
|
+
export function wireName(name: string): string {
|
|
145
|
+
return toSnakeCase(name);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** camelCase property name for service accessors on the client. */
|
|
149
|
+
export function servicePropertyName(name: string): string {
|
|
150
|
+
return toCamelCase(name);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Resolve the effective service name, using the overlay-resolved class name. */
|
|
154
|
+
export function resolveServiceName(service: Service, ctx: EmitterContext): string {
|
|
155
|
+
return resolveClassName(service, ctx);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Build a map from IR service name to resolved service name. */
|
|
159
|
+
export function buildServiceNameMap(services: Service[], ctx: EmitterContext): Map<string, string> {
|
|
160
|
+
const map = new Map<string, string>();
|
|
161
|
+
for (const service of services) {
|
|
162
|
+
map.set(service.name, resolveServiceName(service, ctx));
|
|
163
|
+
}
|
|
164
|
+
return map;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Resolve the SDK class name for a service, using resolved operations' mountOn. */
|
|
168
|
+
export function resolveClassName(service: Service, ctx: EmitterContext): string {
|
|
169
|
+
// Use resolved ops mountOn as canonical class name
|
|
170
|
+
for (const r of ctx.resolvedOperations ?? []) {
|
|
171
|
+
if (r.service.name === service.name) return r.mountOn;
|
|
172
|
+
}
|
|
173
|
+
// Fallback to overlay, then IR name
|
|
174
|
+
if (ctx.overlayLookup?.methodByOperation) {
|
|
175
|
+
for (const op of service.operations) {
|
|
176
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
177
|
+
const existing = ctx.overlayLookup.methodByOperation.get(httpKey);
|
|
178
|
+
if (existing) return existing.className;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return toPascalCase(service.name);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Resolve the type name for a model, checking overlay first. */
|
|
185
|
+
export function resolveTypeName(name: string, ctx: EmitterContext): string {
|
|
186
|
+
const existing = ctx.overlayLookup?.interfaceByName?.get(name);
|
|
187
|
+
if (existing) return existing;
|
|
188
|
+
return toPascalCase(stripUrnPrefix(name));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Service grouping ─────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Group services by shared camelCase prefix for nested namespaces.
|
|
195
|
+
*/
|
|
196
|
+
export function groupServicesByNamespace(services: Service[], ctx: EmitterContext): NamespaceGrouping {
|
|
197
|
+
// Build entries, deduplicating props — when the overlay causes two services to
|
|
198
|
+
// resolve to the same accessor name (e.g., OrganizationDomains → Organizations),
|
|
199
|
+
// fall back to the IR name for the duplicate to keep both reachable.
|
|
200
|
+
const usedProps = new Set<string>();
|
|
201
|
+
const entries = services.map((service) => {
|
|
202
|
+
const resolvedName = resolveClassName(service, ctx);
|
|
203
|
+
let prop = servicePropertyName(resolvedName);
|
|
204
|
+
if (usedProps.has(prop)) {
|
|
205
|
+
// Collision — fall back to the raw IR service name
|
|
206
|
+
prop = servicePropertyName(toPascalCase(service.name));
|
|
207
|
+
}
|
|
208
|
+
usedProps.add(prop);
|
|
209
|
+
return { service, prop, resolvedName };
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const allProps = new Set(entries.map((e) => e.prop));
|
|
213
|
+
const VIRTUAL_NAMESPACES = new Set(['userManagement']);
|
|
214
|
+
|
|
215
|
+
// Count how many property names contain each possible camelCase prefix
|
|
216
|
+
// For PHP we use the snake_case version for prefix detection then convert back
|
|
217
|
+
const snakeEntries = entries.map((e) => ({ ...e, snakeProp: toSnakeCase(e.prop) }));
|
|
218
|
+
const prefixCount = new Map<string, number>();
|
|
219
|
+
for (const entry of snakeEntries) {
|
|
220
|
+
prefixCount.set(entry.snakeProp, (prefixCount.get(entry.snakeProp) || 0) + 1);
|
|
221
|
+
const parts = entry.snakeProp.split('_');
|
|
222
|
+
for (let len = 1; len < parts.length; len++) {
|
|
223
|
+
const prefix = parts.slice(0, len).join('_');
|
|
224
|
+
prefixCount.set(prefix, (prefixCount.get(prefix) || 0) + 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const entryPrefix = new Map<string, string>();
|
|
229
|
+
for (const entry of snakeEntries) {
|
|
230
|
+
const parts = entry.snakeProp.split('_');
|
|
231
|
+
for (let len = parts.length - 1; len >= 1; len--) {
|
|
232
|
+
const prefix = parts.slice(0, len).join('_');
|
|
233
|
+
const camelPrefix = toCamelCase(prefix);
|
|
234
|
+
if (
|
|
235
|
+
(prefixCount.get(prefix) ?? 0) >= 2 &&
|
|
236
|
+
prefix !== entry.snakeProp &&
|
|
237
|
+
(allProps.has(camelPrefix) || VIRTUAL_NAMESPACES.has(camelPrefix))
|
|
238
|
+
) {
|
|
239
|
+
entryPrefix.set(entry.prop, camelPrefix);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const namespacesMap = new Map<string, NamespaceGroup['entries']>();
|
|
246
|
+
const standalone: typeof entries = [];
|
|
247
|
+
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
const prefix = entryPrefix.get(entry.prop);
|
|
250
|
+
if (prefix) {
|
|
251
|
+
if (!namespacesMap.has(prefix)) namespacesMap.set(prefix, []);
|
|
252
|
+
// Compute sub-property: remove prefix from the camelCase name
|
|
253
|
+
const snakePrefix = toSnakeCase(prefix);
|
|
254
|
+
const snakeProp = toSnakeCase(entry.prop);
|
|
255
|
+
const subSnake = snakeProp.slice(snakePrefix.length + 1);
|
|
256
|
+
const subProp = toCamelCase(subSnake);
|
|
257
|
+
namespacesMap.get(prefix)!.push({ service: entry.service, subProp, resolvedName: entry.resolvedName });
|
|
258
|
+
} else {
|
|
259
|
+
standalone.push(entry);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const namespacePrefixes = new Set(namespacesMap.keys());
|
|
264
|
+
const colliding = new Map<string, (typeof entries)[0]>();
|
|
265
|
+
const filteredStandalone = standalone.filter((entry) => {
|
|
266
|
+
if (namespacePrefixes.has(entry.prop)) {
|
|
267
|
+
colliding.set(entry.prop, entry);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const namespaces: NamespaceGroup[] = [...namespacesMap].map(([prefix, nsEntries]) => ({
|
|
274
|
+
prefix,
|
|
275
|
+
entries: nsEntries,
|
|
276
|
+
baseEntry: colliding.get(prefix)
|
|
277
|
+
? { service: colliding.get(prefix)!.service, resolvedName: colliding.get(prefix)!.resolvedName }
|
|
278
|
+
: undefined,
|
|
279
|
+
}));
|
|
280
|
+
return { standalone: filteredStandalone, namespaces };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Build a map from IR service name to the resolved directory name. */
|
|
284
|
+
export function buildServiceDirMap(grouping: NamespaceGrouping): Map<string, string> {
|
|
285
|
+
const map = new Map<string, string>();
|
|
286
|
+
for (const entry of grouping.standalone) {
|
|
287
|
+
map.set(entry.service.name, toPascalCase(entry.resolvedName));
|
|
288
|
+
}
|
|
289
|
+
for (const ns of grouping.namespaces) {
|
|
290
|
+
if (ns.baseEntry) {
|
|
291
|
+
map.set(ns.baseEntry.service.name, toPascalCase(ns.prefix));
|
|
292
|
+
}
|
|
293
|
+
for (const entry of ns.entries) {
|
|
294
|
+
map.set(entry.service.name, `${toPascalCase(ns.prefix)}/${toPascalCase(entry.subProp)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return map;
|
|
298
|
+
}
|