@workos/oagen-emitters 0.3.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.
Files changed (128) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint.yml +1 -1
  3. package/.github/workflows/release-please.yml +2 -2
  4. package/.github/workflows/release.yml +1 -1
  5. package/.husky/pre-push +11 -0
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +15 -0
  9. package/README.md +35 -224
  10. package/dist/index.d.mts +12 -1
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2 -12737
  13. package/dist/plugin-BSop9f9z.mjs +21471 -0
  14. package/dist/plugin-BSop9f9z.mjs.map +1 -0
  15. package/dist/plugin.d.mts +7 -0
  16. package/dist/plugin.d.mts.map +1 -0
  17. package/dist/plugin.mjs +2 -0
  18. package/docs/sdk-architecture/dotnet.md +336 -0
  19. package/oagen.config.ts +5 -343
  20. package/package.json +10 -34
  21. package/smoke/sdk-dotnet.ts +45 -12
  22. package/src/dotnet/client.ts +89 -0
  23. package/src/dotnet/enums.ts +323 -0
  24. package/src/dotnet/fixtures.ts +236 -0
  25. package/src/dotnet/index.ts +248 -0
  26. package/src/dotnet/manifest.ts +36 -0
  27. package/src/dotnet/models.ts +320 -0
  28. package/src/dotnet/naming.ts +368 -0
  29. package/src/dotnet/resources.ts +943 -0
  30. package/src/dotnet/tests.ts +713 -0
  31. package/src/dotnet/type-map.ts +228 -0
  32. package/src/dotnet/wrappers.ts +197 -0
  33. package/src/go/client.ts +35 -3
  34. package/src/go/enums.ts +4 -0
  35. package/src/go/index.ts +15 -7
  36. package/src/go/models.ts +6 -1
  37. package/src/go/naming.ts +5 -17
  38. package/src/go/resources.ts +534 -73
  39. package/src/go/tests.ts +39 -3
  40. package/src/go/type-map.ts +8 -3
  41. package/src/go/wrappers.ts +79 -21
  42. package/src/index.ts +15 -0
  43. package/src/kotlin/client.ts +58 -0
  44. package/src/kotlin/enums.ts +189 -0
  45. package/src/kotlin/index.ts +92 -0
  46. package/src/kotlin/manifest.ts +55 -0
  47. package/src/kotlin/models.ts +486 -0
  48. package/src/kotlin/naming.ts +229 -0
  49. package/src/kotlin/overrides.ts +25 -0
  50. package/src/kotlin/resources.ts +998 -0
  51. package/src/kotlin/tests.ts +1133 -0
  52. package/src/kotlin/type-map.ts +123 -0
  53. package/src/kotlin/wrappers.ts +168 -0
  54. package/src/node/client.ts +84 -7
  55. package/src/node/field-plan.ts +12 -14
  56. package/src/node/fixtures.ts +39 -3
  57. package/src/node/index.ts +1 -0
  58. package/src/node/models.ts +281 -37
  59. package/src/node/resources.ts +319 -95
  60. package/src/node/tests.ts +108 -29
  61. package/src/node/type-map.ts +1 -31
  62. package/src/node/utils.ts +96 -6
  63. package/src/node/wrappers.ts +31 -1
  64. package/src/php/client.ts +11 -3
  65. package/src/php/models.ts +0 -33
  66. package/src/php/naming.ts +2 -21
  67. package/src/php/resources.ts +275 -19
  68. package/src/php/tests.ts +118 -18
  69. package/src/php/type-map.ts +16 -2
  70. package/src/php/wrappers.ts +7 -2
  71. package/src/plugin.ts +50 -0
  72. package/src/python/client.ts +50 -32
  73. package/src/python/enums.ts +35 -10
  74. package/src/python/index.ts +35 -27
  75. package/src/python/models.ts +139 -2
  76. package/src/python/naming.ts +2 -22
  77. package/src/python/resources.ts +234 -17
  78. package/src/python/tests.ts +260 -16
  79. package/src/python/type-map.ts +16 -2
  80. package/src/ruby/client.ts +238 -0
  81. package/src/ruby/enums.ts +149 -0
  82. package/src/ruby/index.ts +93 -0
  83. package/src/ruby/manifest.ts +35 -0
  84. package/src/ruby/models.ts +360 -0
  85. package/src/ruby/naming.ts +187 -0
  86. package/src/ruby/rbi.ts +313 -0
  87. package/src/ruby/resources.ts +799 -0
  88. package/src/ruby/tests.ts +459 -0
  89. package/src/ruby/type-map.ts +97 -0
  90. package/src/ruby/wrappers.ts +161 -0
  91. package/src/shared/model-utils.ts +357 -16
  92. package/src/shared/naming-utils.ts +83 -0
  93. package/src/shared/non-spec-services.ts +13 -0
  94. package/src/shared/resolved-ops.ts +75 -1
  95. package/src/shared/wrapper-utils.ts +12 -1
  96. package/test/dotnet/client.test.ts +121 -0
  97. package/test/dotnet/enums.test.ts +193 -0
  98. package/test/dotnet/errors.test.ts +9 -0
  99. package/test/dotnet/manifest.test.ts +82 -0
  100. package/test/dotnet/models.test.ts +258 -0
  101. package/test/dotnet/resources.test.ts +387 -0
  102. package/test/dotnet/tests.test.ts +202 -0
  103. package/test/entrypoint.test.ts +89 -0
  104. package/test/go/client.test.ts +6 -6
  105. package/test/go/resources.test.ts +156 -7
  106. package/test/kotlin/models.test.ts +135 -0
  107. package/test/kotlin/resources.test.ts +210 -0
  108. package/test/kotlin/tests.test.ts +176 -0
  109. package/test/node/client.test.ts +74 -0
  110. package/test/node/models.test.ts +134 -1
  111. package/test/node/resources.test.ts +343 -34
  112. package/test/node/utils.test.ts +140 -0
  113. package/test/php/client.test.ts +2 -1
  114. package/test/php/models.test.ts +5 -4
  115. package/test/php/resources.test.ts +103 -0
  116. package/test/php/tests.test.ts +67 -0
  117. package/test/plugin.test.ts +50 -0
  118. package/test/python/client.test.ts +56 -0
  119. package/test/python/models.test.ts +99 -0
  120. package/test/python/resources.test.ts +294 -0
  121. package/test/python/tests.test.ts +91 -0
  122. package/test/ruby/client.test.ts +81 -0
  123. package/test/ruby/resources.test.ts +386 -0
  124. package/test/shared/resolved-ops.test.ts +122 -0
  125. package/tsdown.config.ts +1 -1
  126. package/dist/index.mjs.map +0 -1
  127. package/scripts/generate-php.js +0 -13
  128. package/scripts/git-push-with-published-oagen.sh +0 -21
@@ -0,0 +1,229 @@
1
+ import type { Operation, Service, EmitterContext } from '@workos/oagen';
2
+ import { toPascalCase, toCamelCase, 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 (matches the primary class). */
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
+ /** camelCase method name. */
22
+ export function methodName(name: string): string {
23
+ return toCamelCase(name);
24
+ }
25
+
26
+ /** camelCase Kotlin property / local variable name. */
27
+ export function propertyName(name: string): string {
28
+ const camel = toCamelCase(name);
29
+ // `object` is a Kotlin reserved word. Instead of backtick-escaping it
30
+ // (forcing callers to write `event.\`object\``), rename to `objectType`
31
+ // and rely on @JsonProperty("object") for wire mapping.
32
+ if (camel === 'object') return 'objectType';
33
+ return escapeReserved(camel);
34
+ }
35
+
36
+ /** camelCase alias (kept for parity with other emitters). */
37
+ export const fieldName = propertyName;
38
+ export const localName = propertyName;
39
+
40
+ /** PascalCase directory segment for a service / mount group. */
41
+ export function moduleName(name: string): string {
42
+ return toPascalCase(name);
43
+ }
44
+
45
+ /** Lower-case Kotlin package segment for a service / mount group. */
46
+ export function packageSegment(name: string): string {
47
+ // Kotlin package convention: all-lowercase, no separators.
48
+ return toPascalCase(name).toLowerCase();
49
+ }
50
+
51
+ /** Kotlin service class name for a mount group (e.g., `Organizations`). */
52
+ export function apiClassName(name: string): string {
53
+ if (className(name) === 'SSO') return 'Sso';
54
+ return className(name);
55
+ }
56
+
57
+ /** Accessor property exposed on the WorkOS client (camelCase). */
58
+ export function servicePropertyName(name: string): string {
59
+ return toCamelCase(name);
60
+ }
61
+
62
+ /** Resolve the effective service (mount target) name. */
63
+ export function resolveServiceName(service: Service, ctx: EmitterContext): string {
64
+ return resolveClassName(service, ctx);
65
+ }
66
+
67
+ /** Build a map from IR service name -> resolved mount-target name (PascalCase). */
68
+ export function buildServiceNameMap(services: Service[], ctx: EmitterContext): Map<string, string> {
69
+ const map = new Map<string, string>();
70
+ for (const service of services) {
71
+ map.set(service.name, resolveServiceName(service, ctx));
72
+ }
73
+ return map;
74
+ }
75
+
76
+ /** Resolve the SDK method name (camelCase) for an operation. */
77
+ export function resolveMethodName(op: Operation, service: Service, ctx: EmitterContext): string {
78
+ const lookup = buildResolvedLookup(ctx);
79
+ const resolved = lookupMethodName(op, lookup);
80
+ if (resolved) {
81
+ return trimMountedResourceFromMethod(methodName(resolved), resolveClassName(service, ctx));
82
+ }
83
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
84
+ const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
85
+ if (existing) {
86
+ return trimMountedResourceFromMethod(methodName(existing.methodName), resolveClassName(service, ctx));
87
+ }
88
+ return trimMountedResourceFromMethod(methodName(op.name), resolveClassName(service, ctx));
89
+ }
90
+
91
+ /** Resolve the SDK class name (PascalCase) for a service. */
92
+ export function resolveClassName(service: Service, ctx: EmitterContext): string {
93
+ for (const r of ctx.resolvedOperations ?? []) {
94
+ if (r.service.name === service.name) return className(r.mountOn);
95
+ }
96
+ if (ctx.overlayLookup?.methodByOperation) {
97
+ for (const op of service.operations) {
98
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
99
+ const existing = ctx.overlayLookup.methodByOperation.get(httpKey);
100
+ if (existing) return className(existing.className);
101
+ }
102
+ }
103
+ return className(service.name);
104
+ }
105
+
106
+ /** Build a map from IR service name -> mount-target directory (PascalCase). */
107
+ export function buildMountDirMap(ctx: EmitterContext): Map<string, string> {
108
+ const map = new Map<string, string>();
109
+ for (const service of ctx.spec.services) {
110
+ const target = getMountTarget(service, ctx);
111
+ map.set(service.name, moduleName(target));
112
+ }
113
+ return map;
114
+ }
115
+
116
+ function splitPascalWords(name: string): string[] {
117
+ return name.match(/[A-Z]+(?:[a-z]+|(?=[A-Z]|$))|[A-Z]?[a-z]+|[0-9]+/g) ?? [name];
118
+ }
119
+
120
+ function singularize(word: string): string {
121
+ if (word.endsWith('ies') && word.length > 3) {
122
+ return `${word.slice(0, -3)}y`;
123
+ }
124
+ if (word.endsWith('s') && !word.endsWith('ss')) {
125
+ return word.slice(0, -1);
126
+ }
127
+ return word;
128
+ }
129
+
130
+ function wordsMatch(left: string, right: string): boolean {
131
+ return singularize(left.toLowerCase()) === singularize(right.toLowerCase());
132
+ }
133
+
134
+ /**
135
+ * Trim the mount-target resource words from the start of a method name.
136
+ * E.g. `listOrganizations` on OrganizationsApi becomes `list`.
137
+ */
138
+ function trimMountedResourceFromMethod(method: string, mountName: string): string {
139
+ const methodWords = splitPascalWords(method);
140
+ if (methodWords.length < 2) return method;
141
+
142
+ const mountWords = splitPascalWords(className(mountName));
143
+ if (mountWords.length === 0) return method;
144
+
145
+ let matched = 0;
146
+ while (
147
+ matched < mountWords.length &&
148
+ matched + 1 < methodWords.length &&
149
+ wordsMatch(methodWords[matched + 1], mountWords[matched])
150
+ ) {
151
+ matched++;
152
+ }
153
+
154
+ if (matched === 0) return method;
155
+
156
+ return [methodWords[0], ...methodWords.slice(matched + 1)].join('');
157
+ }
158
+
159
+ /** Kotlin hard/soft keywords that must be back-ticked when used as identifiers. */
160
+ const KOTLIN_RESERVED = new Set([
161
+ 'as',
162
+ 'break',
163
+ 'class',
164
+ 'continue',
165
+ 'do',
166
+ 'else',
167
+ 'false',
168
+ 'for',
169
+ 'fun',
170
+ 'if',
171
+ 'in',
172
+ 'interface',
173
+ 'is',
174
+ 'null',
175
+ 'object',
176
+ 'package',
177
+ 'return',
178
+ 'super',
179
+ 'this',
180
+ 'throw',
181
+ 'true',
182
+ 'try',
183
+ 'typealias',
184
+ 'typeof',
185
+ 'val',
186
+ 'var',
187
+ 'when',
188
+ 'while',
189
+ ]);
190
+
191
+ /** Escape a Kotlin identifier if it collides with a reserved word. */
192
+ export function escapeReserved(name: string): string {
193
+ return KOTLIN_RESERVED.has(name) ? `\`${name}\`` : name;
194
+ }
195
+
196
+ /** Escape a string literal for Kotlin source. */
197
+ export function ktStringLiteral(value: string): string {
198
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
199
+ }
200
+
201
+ /** Escape any scalar as a Kotlin literal expression. */
202
+ export function ktLiteral(value: string | number | boolean): string {
203
+ if (typeof value === 'string') return ktStringLiteral(value);
204
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
205
+ return String(value);
206
+ }
207
+
208
+ /**
209
+ * Map a wire field name to the expression that reads it off a WorkOS client
210
+ * instance (used for inferFromClient fields).
211
+ */
212
+ export function clientFieldExpression(field: string): string {
213
+ switch (field) {
214
+ case 'client_id':
215
+ return 'clientId';
216
+ case 'client_secret':
217
+ return 'apiKey';
218
+ default:
219
+ return propertyName(field);
220
+ }
221
+ }
222
+
223
+ /** Convert snake_case / camelCase to a human-readable lowercase phrase for docs. */
224
+ export function humanize(name: string): string {
225
+ return name
226
+ .replace(/_/g, ' ')
227
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
228
+ .toLowerCase();
229
+ }
@@ -0,0 +1,25 @@
1
+ import type { Operation } from '@workos/oagen';
2
+
3
+ /**
4
+ * Operations whose generated implementation would be wrong (URL builders
5
+ * served as HTTP calls). Hand-maintained code in `workos-kotlin` owns these
6
+ * method names instead; the emitter must skip resources, tests, and the
7
+ * smoke manifest for any operation in this set.
8
+ *
9
+ * Keys are `"METHOD path"` in the same form used by the smoke manifest.
10
+ */
11
+ export const HANDWRITTEN_OVERRIDE_OPS: ReadonlySet<string> = new Set([
12
+ // AuthKit authorization URL (H09 / H10 are hand-built).
13
+ 'GET /user_management/authorize',
14
+ // AuthKit logout URL (H17 — URL, not HTTP call).
15
+ 'GET /user_management/sessions/logout',
16
+ // SSO authorization URL (H14 / H15 are hand-built).
17
+ 'GET /sso/authorize',
18
+ // SSO logout URL (H17 — URL, not HTTP call).
19
+ 'GET /sso/logout',
20
+ ]);
21
+
22
+ /** True if the operation is owned by a hand-maintained override. */
23
+ export function isHandwrittenOverride(op: Operation): boolean {
24
+ return HANDWRITTEN_OVERRIDE_OPS.has(`${op.httpMethod.toUpperCase()} ${op.path}`);
25
+ }