@workos/oagen-emitters 0.2.1 → 0.4.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.
Files changed (136) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +129 -0
  5. package/dist/index.d.mts +13 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +14549 -3385
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/sdk-architecture/dotnet.md +336 -0
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +328 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +45 -12
  18. package/smoke/sdk-go.ts +116 -42
  19. package/smoke/sdk-php.ts +28 -26
  20. package/smoke/sdk-python.ts +5 -2
  21. package/src/dotnet/client.ts +89 -0
  22. package/src/dotnet/enums.ts +323 -0
  23. package/src/dotnet/fixtures.ts +236 -0
  24. package/src/dotnet/index.ts +246 -0
  25. package/src/dotnet/manifest.ts +36 -0
  26. package/src/dotnet/models.ts +344 -0
  27. package/src/dotnet/naming.ts +330 -0
  28. package/src/dotnet/resources.ts +622 -0
  29. package/src/dotnet/tests.ts +693 -0
  30. package/src/dotnet/type-map.ts +201 -0
  31. package/src/dotnet/wrappers.ts +186 -0
  32. package/src/go/client.ts +141 -0
  33. package/src/go/enums.ts +196 -0
  34. package/src/go/fixtures.ts +212 -0
  35. package/src/go/index.ts +84 -0
  36. package/src/go/manifest.ts +36 -0
  37. package/src/go/models.ts +254 -0
  38. package/src/go/naming.ts +179 -0
  39. package/src/go/resources.ts +827 -0
  40. package/src/go/tests.ts +751 -0
  41. package/src/go/type-map.ts +82 -0
  42. package/src/go/wrappers.ts +261 -0
  43. package/src/index.ts +4 -0
  44. package/src/kotlin/client.ts +53 -0
  45. package/src/kotlin/enums.ts +162 -0
  46. package/src/kotlin/index.ts +92 -0
  47. package/src/kotlin/manifest.ts +55 -0
  48. package/src/kotlin/models.ts +395 -0
  49. package/src/kotlin/naming.ts +223 -0
  50. package/src/kotlin/overrides.ts +25 -0
  51. package/src/kotlin/resources.ts +667 -0
  52. package/src/kotlin/tests.ts +1019 -0
  53. package/src/kotlin/type-map.ts +123 -0
  54. package/src/kotlin/wrappers.ts +168 -0
  55. package/src/node/client.ts +128 -115
  56. package/src/node/enums.ts +9 -0
  57. package/src/node/errors.ts +37 -232
  58. package/src/node/field-plan.ts +726 -0
  59. package/src/node/fixtures.ts +9 -1
  60. package/src/node/index.ts +3 -9
  61. package/src/node/models.ts +178 -21
  62. package/src/node/naming.ts +49 -111
  63. package/src/node/resources.ts +527 -397
  64. package/src/node/sdk-errors.ts +41 -0
  65. package/src/node/tests.ts +69 -19
  66. package/src/node/type-map.ts +4 -2
  67. package/src/node/utils.ts +13 -71
  68. package/src/node/wrappers.ts +151 -0
  69. package/src/php/client.ts +179 -0
  70. package/src/php/enums.ts +67 -0
  71. package/src/php/errors.ts +9 -0
  72. package/src/php/fixtures.ts +181 -0
  73. package/src/php/index.ts +96 -0
  74. package/src/php/manifest.ts +36 -0
  75. package/src/php/models.ts +310 -0
  76. package/src/php/naming.ts +279 -0
  77. package/src/php/resources.ts +636 -0
  78. package/src/php/tests.ts +609 -0
  79. package/src/php/type-map.ts +90 -0
  80. package/src/php/utils.ts +18 -0
  81. package/src/php/wrappers.ts +152 -0
  82. package/src/python/client.ts +345 -0
  83. package/src/python/enums.ts +313 -0
  84. package/src/python/fixtures.ts +196 -0
  85. package/src/python/index.ts +95 -0
  86. package/src/python/manifest.ts +38 -0
  87. package/src/python/models.ts +688 -0
  88. package/src/python/naming.ts +189 -0
  89. package/src/python/resources.ts +1322 -0
  90. package/src/python/tests.ts +1335 -0
  91. package/src/python/type-map.ts +93 -0
  92. package/src/python/wrappers.ts +191 -0
  93. package/src/shared/model-utils.ts +472 -0
  94. package/src/shared/naming-utils.ts +154 -0
  95. package/src/shared/non-spec-services.ts +54 -0
  96. package/src/shared/resolved-ops.ts +109 -0
  97. package/src/shared/wrapper-utils.ts +70 -0
  98. package/test/dotnet/client.test.ts +121 -0
  99. package/test/dotnet/enums.test.ts +193 -0
  100. package/test/dotnet/errors.test.ts +9 -0
  101. package/test/dotnet/manifest.test.ts +82 -0
  102. package/test/dotnet/models.test.ts +260 -0
  103. package/test/dotnet/resources.test.ts +255 -0
  104. package/test/dotnet/tests.test.ts +202 -0
  105. package/test/go/client.test.ts +92 -0
  106. package/test/go/enums.test.ts +132 -0
  107. package/test/go/errors.test.ts +9 -0
  108. package/test/go/models.test.ts +265 -0
  109. package/test/go/resources.test.ts +408 -0
  110. package/test/go/tests.test.ts +143 -0
  111. package/test/kotlin/models.test.ts +135 -0
  112. package/test/kotlin/tests.test.ts +176 -0
  113. package/test/node/client.test.ts +92 -12
  114. package/test/node/enums.test.ts +2 -0
  115. package/test/node/errors.test.ts +2 -41
  116. package/test/node/models.test.ts +2 -0
  117. package/test/node/naming.test.ts +23 -0
  118. package/test/node/resources.test.ts +315 -84
  119. package/test/node/serializers.test.ts +3 -1
  120. package/test/node/type-map.test.ts +11 -0
  121. package/test/php/client.test.ts +95 -0
  122. package/test/php/enums.test.ts +173 -0
  123. package/test/php/errors.test.ts +9 -0
  124. package/test/php/models.test.ts +497 -0
  125. package/test/php/resources.test.ts +682 -0
  126. package/test/php/tests.test.ts +185 -0
  127. package/test/python/client.test.ts +200 -0
  128. package/test/python/enums.test.ts +228 -0
  129. package/test/python/errors.test.ts +16 -0
  130. package/test/python/manifest.test.ts +74 -0
  131. package/test/python/models.test.ts +716 -0
  132. package/test/python/resources.test.ts +617 -0
  133. package/test/python/tests.test.ts +202 -0
  134. package/src/node/common.ts +0 -273
  135. package/src/node/config.ts +0 -71
  136. package/src/node/serializers.ts +0 -746
@@ -0,0 +1,330 @@
1
+ import type { Operation, Service, EmitterContext } from '@workos/oagen';
2
+ import { toPascalCase, toSnakeCase } from '@workos/oagen';
3
+ import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
4
+ import { stripUrnPrefix } from '../shared/naming-utils.js';
5
+
6
+ /** PascalCase class/type name. */
7
+ export function className(name: string): string {
8
+ return toPascalCase(stripUrnPrefix(name));
9
+ }
10
+
11
+ /** PascalCase file name (without extension). */
12
+ export function fileName(name: string): string {
13
+ return toPascalCase(stripUrnPrefix(name));
14
+ }
15
+
16
+ /** snake_case file name for fixtures/test data. */
17
+ export function fixtureFileName(name: string): string {
18
+ return toSnakeCase(stripUrnPrefix(name));
19
+ }
20
+
21
+ /** PascalCase method name. */
22
+ export function methodName(name: string): string {
23
+ return toPascalCase(name);
24
+ }
25
+
26
+ /** PascalCase property name. */
27
+ export function fieldName(name: string): string {
28
+ return toPascalCase(name);
29
+ }
30
+
31
+ /** PascalCase directory name for service modules. */
32
+ export function moduleName(name: string): string {
33
+ return toPascalCase(name);
34
+ }
35
+
36
+ /** PascalCase property name for service accessors on the client. */
37
+ export function servicePropertyName(name: string): string {
38
+ return className(name);
39
+ }
40
+
41
+ /** Resolve the effective service name using resolved operations. */
42
+ export function resolveServiceName(service: Service, ctx: EmitterContext): string {
43
+ return resolveClassName(service, ctx);
44
+ }
45
+
46
+ /** Build a map from IR service name to resolved service name. */
47
+ export function buildServiceNameMap(services: Service[], ctx: EmitterContext): Map<string, string> {
48
+ const map = new Map<string, string>();
49
+ for (const service of services) {
50
+ map.set(service.name, resolveServiceName(service, ctx));
51
+ }
52
+ return map;
53
+ }
54
+
55
+ /** Resolve the output directory for a service. */
56
+ export function resolveServiceDir(resolvedServiceName: string): string {
57
+ return moduleName(resolvedServiceName);
58
+ }
59
+
60
+ /** Resolve the SDK method name for an operation. */
61
+ export function resolveMethodName(op: Operation, _service: Service, ctx: EmitterContext): string {
62
+ const lookup = buildResolvedLookup(ctx);
63
+ const resolved = lookupMethodName(op, lookup);
64
+ if (resolved) return trimMountedResourceFromMethod(methodName(resolved), resolveClassName(_service, ctx));
65
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
66
+ const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
67
+ if (existing) return trimMountedResourceFromMethod(methodName(existing.methodName), resolveClassName(_service, ctx));
68
+ return trimMountedResourceFromMethod(methodName(op.name), resolveClassName(_service, ctx));
69
+ }
70
+
71
+ /** Resolve the SDK class name for a service. */
72
+ export function resolveClassName(service: Service, ctx: EmitterContext): string {
73
+ for (const r of ctx.resolvedOperations ?? []) {
74
+ if (r.service.name === service.name) return className(r.mountOn);
75
+ }
76
+ if (ctx.overlayLookup?.methodByOperation) {
77
+ for (const op of service.operations) {
78
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
79
+ const existing = ctx.overlayLookup.methodByOperation.get(httpKey);
80
+ if (existing) return className(existing.className);
81
+ }
82
+ }
83
+ return className(service.name);
84
+ }
85
+
86
+ /** Build a map from IR service name to mount-target directory name. */
87
+ export function buildMountDirMap(ctx: EmitterContext): Map<string, string> {
88
+ const map = new Map<string, string>();
89
+ for (const service of ctx.spec.services) {
90
+ const target = getMountTarget(service, ctx);
91
+ map.set(service.name, moduleName(target));
92
+ }
93
+ return map;
94
+ }
95
+
96
+ function splitPascalWords(name: string): string[] {
97
+ return name.match(/[A-Z]+(?:[a-z]+|(?=[A-Z]|$))|[A-Z]?[a-z]+|[0-9]+/g) ?? [name];
98
+ }
99
+
100
+ function singularize(word: string): string {
101
+ if (word.endsWith('ies') && word.length > 3) {
102
+ return `${word.slice(0, -3)}y`;
103
+ }
104
+ if (word.endsWith('s') && !word.endsWith('ss')) {
105
+ return word.slice(0, -1);
106
+ }
107
+ return word;
108
+ }
109
+
110
+ function wordsMatch(left: string, right: string): boolean {
111
+ return singularize(left.toLowerCase()) === singularize(right.toLowerCase());
112
+ }
113
+
114
+ function trimMountedResourceFromMethod(method: string, mountName: string): string {
115
+ const methodWords = splitPascalWords(method);
116
+ if (methodWords.length < 2) return method;
117
+
118
+ const mountWords = splitPascalWords(className(mountName));
119
+ if (mountWords.length === 0) return method;
120
+
121
+ let matched = 0;
122
+ while (
123
+ matched < mountWords.length &&
124
+ matched + 1 < methodWords.length &&
125
+ wordsMatch(methodWords[matched + 1], mountWords[matched])
126
+ ) {
127
+ matched++;
128
+ }
129
+
130
+ if (matched === 0) return method;
131
+
132
+ return [methodWords[0], ...methodWords.slice(matched + 1)].join('');
133
+ }
134
+
135
+ /** Service type name for the class declaration. */
136
+ export function serviceTypeName(name: string): string {
137
+ // Preserve pluralization for C# service names (OrganizationsService, not OrganizationService)
138
+ return `${className(name)}Service`;
139
+ }
140
+
141
+ /** camelCase for local variables. */
142
+ export function localName(name: string): string {
143
+ const pascal = toPascalCase(name);
144
+ if (!pascal) return pascal;
145
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
146
+ }
147
+
148
+ /** Escape a value as a C# literal. */
149
+ export function csLiteral(value: string | number | boolean): string {
150
+ if (typeof value === 'string') return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
151
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
152
+ return String(value);
153
+ }
154
+
155
+ /** Map a wire field name to the WorkOSClient property expression. */
156
+ export function clientFieldExpression(field: string): string {
157
+ switch (field) {
158
+ case 'client_id':
159
+ return 'ClientId';
160
+ case 'client_secret':
161
+ return 'ApiKey';
162
+ default:
163
+ return fieldName(field);
164
+ }
165
+ }
166
+
167
+ /** Convert an HTTP method string to the C# HttpMethod static property name. */
168
+ export function httpMethodCs(method: string): string {
169
+ const m = method.toLowerCase();
170
+ switch (m) {
171
+ case 'get':
172
+ return 'Get';
173
+ case 'post':
174
+ return 'Post';
175
+ case 'put':
176
+ return 'Put';
177
+ case 'patch':
178
+ return 'Patch';
179
+ case 'delete':
180
+ return 'Delete';
181
+ case 'head':
182
+ return 'Head';
183
+ case 'options':
184
+ return 'Options';
185
+ default:
186
+ return 'Get';
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Return the name of the Service base-class helper that handles the given
192
+ * HTTP method (e.g., `GetAsync`, `PostAsync`). Used by the resource emitter
193
+ * to produce one-line service methods instead of inlined WorkOSRequest blocks.
194
+ */
195
+ export function httpMethodHelperName(method: string): string {
196
+ const m = method.toLowerCase();
197
+ switch (m) {
198
+ case 'get':
199
+ return 'GetAsync';
200
+ case 'post':
201
+ return 'PostAsync';
202
+ case 'put':
203
+ return 'PutAsync';
204
+ case 'patch':
205
+ return 'PatchAsync';
206
+ case 'delete':
207
+ return 'DeleteAsync';
208
+ default:
209
+ return 'GetAsync';
210
+ }
211
+ }
212
+
213
+ /** Escape XML special characters for use in XML doc comments. */
214
+ export function escapeXml(s: string): string {
215
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
216
+ }
217
+
218
+ /**
219
+ * Distill a deprecation message from a spec description. Looks for common
220
+ * WorkOS patterns ("Deprecated. Use X.", "Use X instead.", etc.) and falls
221
+ * back to a generic message scoped to the item kind.
222
+ *
223
+ * Output is suitable for inlining into `[System.Obsolete("...")]`.
224
+ */
225
+ export function deprecationMessage(
226
+ description: string | undefined | null,
227
+ kind: 'field' | 'parameter' | 'operation' | 'value',
228
+ ): string {
229
+ const generic = `This ${kind} is deprecated.`;
230
+ if (!description) return generic;
231
+
232
+ const text = description.replace(/\s+/g, ' ').trim();
233
+ if (!text) return generic;
234
+
235
+ // Match: "Deprecated. Use `foo` instead." / "Deprecated: use Foo."
236
+ const deprecatedClause = text.match(/Deprecated[.:][\s]*(.*?)(?:\.|$)/i);
237
+ if (deprecatedClause?.[1]?.trim()) {
238
+ return `Deprecated. ${deprecatedClause[1].trim().replace(/\.$/, '')}.`;
239
+ }
240
+
241
+ // Match: "Use `foo` instead." anywhere in the description
242
+ const useInstead = text.match(/Use\s+`?([^`.\s]+)`?\s+instead/i);
243
+ if (useInstead) {
244
+ return `${generic.replace(/\.$/, '')} Use \`${useInstead[1]}\` instead.`;
245
+ }
246
+
247
+ return generic;
248
+ }
249
+
250
+ /**
251
+ * Escape a C# string literal for use inside `[System.Obsolete("...")]`.
252
+ * Doubles embedded quotes and escapes backslashes.
253
+ */
254
+ export function escapeCsAttributeString(s: string): string {
255
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
256
+ }
257
+
258
+ /**
259
+ * Emit an XML doc summary block from a possibly multi-line spec description.
260
+ * The first non-empty line becomes the `<summary>`. If there are additional
261
+ * non-empty lines, they are emitted as `<remarks>...</remarks>` so users get
262
+ * the full context in tooling (IntelliSense / `dotnet help`) instead of just
263
+ * the first sentence.
264
+ *
265
+ * Returns an empty array if `description` is null/empty so callers can spread
266
+ * the result unconditionally.
267
+ */
268
+ export function emitXmlDoc(description: string | undefined | null, indent: string): string[] {
269
+ if (!description) return [];
270
+ const lines = description
271
+ .split('\n')
272
+ .map((l) => l.trim())
273
+ .filter((l) => l);
274
+ if (lines.length === 0) return [];
275
+
276
+ const out: string[] = [];
277
+ out.push(`${indent}/// <summary>${escapeXml(lines[0])}</summary>`);
278
+ if (lines.length > 1) {
279
+ out.push(`${indent}/// <remarks>`);
280
+ for (const remark of lines.slice(1)) {
281
+ out.push(`${indent}/// ${escapeXml(remark)}`);
282
+ }
283
+ out.push(`${indent}/// </remarks>`);
284
+ }
285
+ return out;
286
+ }
287
+
288
+ /**
289
+ * Convert a snake_case or camelCase name to a human-readable string.
290
+ * Preserves acronyms (SSO, API, URL, JWT, OIDC, …) as uppercase tokens and
291
+ * lowercases the rest so generated XML docs read naturally.
292
+ */
293
+ export function humanize(name: string): string {
294
+ // Split on underscores and at camelCase boundaries, keeping acronym runs
295
+ // intact: `SSOConnection` → ["SSO", "Connection"]; `listUsers` → ["list",
296
+ // "Users"]; `api_key` → ["api", "key"].
297
+ const parts = name.split('_').flatMap((segment) =>
298
+ segment
299
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
300
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
301
+ .split(/\s+/)
302
+ .filter(Boolean),
303
+ );
304
+ return parts.map((p) => (isAcronymToken(p) ? p : p.toLowerCase())).join(' ');
305
+ }
306
+
307
+ /** True when a single token looks like a runtime acronym (2+ uppercase letters). */
308
+ function isAcronymToken(token: string): boolean {
309
+ if (token.length < 2) return false;
310
+ return /^[A-Z0-9]+$/.test(token);
311
+ }
312
+
313
+ /**
314
+ * Return the English indefinite article ("a" or "an") appropriate for the
315
+ * given humanized phrase. Uses the first spoken letter of the first word
316
+ * and accounts for common silent-h / vowel-like-consonant cases so
317
+ * generated docs read correctly.
318
+ */
319
+ export function articleFor(phrase: string): string {
320
+ const trimmed = phrase.trim();
321
+ if (!trimmed) return 'a';
322
+ const firstWord = trimmed.split(/\s+/)[0];
323
+ if (!firstWord) return 'a';
324
+ // Acronyms are voiced letter-by-letter — use the first letter's sound.
325
+ if (isAcronymToken(firstWord)) {
326
+ return /^[AEFHILMNORSX]/.test(firstWord) ? 'an' : 'a';
327
+ }
328
+ const first = firstWord.toLowerCase();
329
+ return /^[aeiou]/.test(first) ? 'an' : 'a';
330
+ }