@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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, GeneratedFile, Service, Model, Enum } from '@workos/oagen';
|
|
2
|
+
import { assignModelsToServices } from '@workos/oagen';
|
|
3
|
+
import { servicePropertyName, resolveClassName, className, fileName, buildMountDirMap } from './naming.js';
|
|
4
|
+
import { classifyUnassignedModel } from './models.js';
|
|
5
|
+
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
6
|
+
import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
|
|
7
|
+
import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ruby `Client#accessor` wiring for each non-spec service id.
|
|
11
|
+
*
|
|
12
|
+
* Entries omitted from this map (e.g. `webhook_verification`) extend an
|
|
13
|
+
* already-generated service class via `@oagen-ignore` blocks and need no
|
|
14
|
+
* dedicated accessor.
|
|
15
|
+
*
|
|
16
|
+
* `ctorArg = 'self'` means the helper takes the client (`Foo.new(self)`).
|
|
17
|
+
* `ctorArg = ''` means the accessor returns the constant directly (modules
|
|
18
|
+
* with module-level functions, e.g. `WorkOS::PKCE`).
|
|
19
|
+
*/
|
|
20
|
+
const NON_SPEC_ACCESSORS: Record<string, { prop: string; className: string; ctorArg: 'self' | '' }> = {
|
|
21
|
+
passwordless: { prop: 'passwordless', className: 'Passwordless', ctorArg: 'self' },
|
|
22
|
+
vault: { prop: 'vault', className: 'Vault', ctorArg: 'self' },
|
|
23
|
+
actions: { prop: 'actions', className: 'Actions', ctorArg: 'self' },
|
|
24
|
+
session_manager: { prop: 'session_manager', className: 'SessionManager', ctorArg: 'self' },
|
|
25
|
+
pkce: { prop: 'pkce', className: 'PKCE', ctorArg: '' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hand-maintained class names whose file basename does NOT camelCase to the
|
|
30
|
+
* expected Ruby class name under Zeitwerk's default inflector. Each entry
|
|
31
|
+
* adds an `inflect("file" => "Class")` override so the autoloader can
|
|
32
|
+
* resolve `WorkOS::PKCE` (rather than the default `WorkOS::Pkce`).
|
|
33
|
+
*/
|
|
34
|
+
const NON_SPEC_INFLECTIONS: ReadonlyArray<readonly [string, string]> = [['pkce', 'PKCE']];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate:
|
|
38
|
+
* - lib/workos.rb — sets up a Zeitwerk loader for the gem
|
|
39
|
+
* - lib/workos/client.rb — client class with service accessors
|
|
40
|
+
*
|
|
41
|
+
* The HTTP runtime (request execution, retries, error mapping, pagination)
|
|
42
|
+
* lives in hand-maintained files flagged with `@oagen-ignore-file`:
|
|
43
|
+
* - lib/workos/base_client.rb
|
|
44
|
+
* - lib/workos/errors.rb
|
|
45
|
+
* - lib/workos/configuration.rb
|
|
46
|
+
* - lib/workos/hash_provider.rb
|
|
47
|
+
* - lib/workos/types/list_struct.rb
|
|
48
|
+
*/
|
|
49
|
+
export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
50
|
+
const files: GeneratedFile[] = [];
|
|
51
|
+
files.push(generateInflectionsFile(spec, ctx));
|
|
52
|
+
files.push(generateMainEntryFile(spec, ctx));
|
|
53
|
+
files.push(generateClientClass(spec, ctx));
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build map: top-level service -> resolved class name (deduplicated by mount target). */
|
|
58
|
+
export function buildTopLevelServices(spec: ApiSpec, ctx: EmitterContext): Service[] {
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
const out: Service[] = [];
|
|
61
|
+
for (const service of spec.services) {
|
|
62
|
+
const target = getMountTarget(service, ctx) || resolveClassName(service, ctx);
|
|
63
|
+
if (seen.has(target)) continue;
|
|
64
|
+
seen.add(target);
|
|
65
|
+
const canonical =
|
|
66
|
+
spec.services.find((s) => (getMountTarget(s, ctx) || resolveClassName(s, ctx)) === target && s.name === target) ??
|
|
67
|
+
service;
|
|
68
|
+
out.push(canonical);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Simulate Zeitwerk::Inflector's default inflection so we can emit the minimal
|
|
75
|
+
* set of overrides. Zeitwerk camelizes `foo_bar` -> `FooBar`; we only add
|
|
76
|
+
* inflector entries when the emitter's canonical className disagrees.
|
|
77
|
+
*/
|
|
78
|
+
function rubyCamelize(basename: string): string {
|
|
79
|
+
return basename
|
|
80
|
+
.split('_')
|
|
81
|
+
.filter((p) => p.length > 0)
|
|
82
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
83
|
+
.join('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Build the inflection map: file basename -> class name for all generated constants. */
|
|
87
|
+
function buildInflectionMap(spec: ApiSpec, ctx: EmitterContext): Map<string, string> {
|
|
88
|
+
const inflections = new Map<string, string>();
|
|
89
|
+
|
|
90
|
+
inflections.set('workos', 'WorkOS');
|
|
91
|
+
|
|
92
|
+
for (const service of buildTopLevelServices(spec, ctx)) {
|
|
93
|
+
const target = getMountTarget(service, ctx) || resolveClassName(service, ctx);
|
|
94
|
+
const cls = className(target);
|
|
95
|
+
const file = fileName(target);
|
|
96
|
+
if (rubyCamelize(file) !== cls) inflections.set(file, cls);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const seenClasses = new Set<string>();
|
|
100
|
+
for (const model of spec.models as Model[]) {
|
|
101
|
+
if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
|
|
102
|
+
const cls = className(model.name);
|
|
103
|
+
if (seenClasses.has(cls)) continue;
|
|
104
|
+
seenClasses.add(cls);
|
|
105
|
+
const file = fileName(model.name);
|
|
106
|
+
if (rubyCamelize(file) !== cls) inflections.set(file, cls);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const seenEnums = new Set<string>();
|
|
110
|
+
for (const enumDef of spec.enums as Enum[]) {
|
|
111
|
+
const cls = className(enumDef.name);
|
|
112
|
+
if (seenEnums.has(cls)) continue;
|
|
113
|
+
seenEnums.add(cls);
|
|
114
|
+
const file = fileName(enumDef.name);
|
|
115
|
+
if (rubyCamelize(file) !== cls) inflections.set(file, cls);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const [file, cls] of NON_SPEC_INFLECTIONS) inflections.set(file, cls);
|
|
119
|
+
|
|
120
|
+
return inflections;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Generate lib/workos/inflections.rb — Zeitwerk inflection overrides (T40/C5). */
|
|
124
|
+
function generateInflectionsFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
125
|
+
const inflections = buildInflectionMap(spec, ctx);
|
|
126
|
+
const inflectEntries = [...inflections.entries()]
|
|
127
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
128
|
+
.map(([fileBase, cls]) => ` "${fileBase}" => "${cls}"`)
|
|
129
|
+
.join(',\n');
|
|
130
|
+
|
|
131
|
+
const lines: string[] = [];
|
|
132
|
+
lines.push('# Zeitwerk inflection overrides for the WorkOS gem.');
|
|
133
|
+
lines.push('# Maps file basenames to class/module names where the default');
|
|
134
|
+
lines.push('# CamelCase inference disagrees with the canonical class name.');
|
|
135
|
+
lines.push('WORKOS_INFLECTIONS = {');
|
|
136
|
+
lines.push(inflectEntries);
|
|
137
|
+
lines.push('}.freeze');
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
path: 'lib/workos/inflections.rb',
|
|
141
|
+
content: lines.join('\n'),
|
|
142
|
+
integrateTarget: true,
|
|
143
|
+
overwriteExisting: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Generate lib/workos.rb — Zeitwerk bootstrap for the gem. */
|
|
148
|
+
function generateMainEntryFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
149
|
+
const modelSubdirs = collectModelSubdirs(spec, ctx);
|
|
150
|
+
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
lines.push(`require 'zeitwerk'`);
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push('module WorkOS');
|
|
155
|
+
lines.push('end');
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('loader = Zeitwerk::Loader.for_gem');
|
|
158
|
+
lines.push(`require_relative 'workos/inflections'`);
|
|
159
|
+
lines.push('loader.inflector.inflect(WORKOS_INFLECTIONS)');
|
|
160
|
+
for (const dir of modelSubdirs) {
|
|
161
|
+
lines.push(`loader.collapse("#{__dir__}/workos/${dir}")`);
|
|
162
|
+
}
|
|
163
|
+
lines.push(`loader.ignore("#{__dir__}/workos/errors.rb")`);
|
|
164
|
+
lines.push(`loader.ignore("#{__dir__}/workos/inflections.rb")`);
|
|
165
|
+
lines.push('loader.setup');
|
|
166
|
+
lines.push('');
|
|
167
|
+
lines.push(`require 'workos/errors'`);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
path: 'lib/workos.rb',
|
|
171
|
+
content: lines.join('\n'),
|
|
172
|
+
integrateTarget: true,
|
|
173
|
+
overwriteExisting: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Collect the set of mount-target subfolders that models.ts will populate.
|
|
179
|
+
* Used by the Zeitwerk bootstrap to emit `loader.collapse` directives, which
|
|
180
|
+
* keep the generated namespace flat while the filesystem is grouped.
|
|
181
|
+
*/
|
|
182
|
+
function collectModelSubdirs(spec: ApiSpec, ctx: EmitterContext): string[] {
|
|
183
|
+
const modelToService = assignModelsToServices(spec.models as Model[], spec.services);
|
|
184
|
+
const mountDirMap = buildMountDirMap(ctx);
|
|
185
|
+
const subdirs = new Set<string>();
|
|
186
|
+
for (const model of spec.models as Model[]) {
|
|
187
|
+
if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
|
|
188
|
+
const service = modelToService.get(model.name);
|
|
189
|
+
const dir = service
|
|
190
|
+
? (mountDirMap.get(service) ?? classifyUnassignedModel(model.name))
|
|
191
|
+
: classifyUnassignedModel(model.name);
|
|
192
|
+
subdirs.add(dir);
|
|
193
|
+
}
|
|
194
|
+
return [...subdirs].sort();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Generate lib/workos/client.rb — thin service-wiring client. */
|
|
198
|
+
function generateClientClass(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
lines.push('module WorkOS');
|
|
201
|
+
lines.push(' class Client < BaseClient');
|
|
202
|
+
|
|
203
|
+
const topLevelServices = buildTopLevelServices(spec, ctx);
|
|
204
|
+
for (const service of topLevelServices) {
|
|
205
|
+
const target = getMountTarget(service, ctx) || resolveClassName(service, ctx);
|
|
206
|
+
const cls = className(target);
|
|
207
|
+
const prop = servicePropertyName(target);
|
|
208
|
+
lines.push('');
|
|
209
|
+
lines.push(` def ${prop}`);
|
|
210
|
+
lines.push(` @${prop} ||= WorkOS::${cls}.new(self)`);
|
|
211
|
+
lines.push(' end');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Non-spec service accessors. Emitted inside @oagen-ignore so user edits
|
|
215
|
+
// (added/removed accessors, renames) survive subsequent regenerations.
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push(' # @oagen-ignore-start — non-spec service accessors (hand-maintained)');
|
|
218
|
+
for (const { id } of NON_SPEC_SERVICES) {
|
|
219
|
+
const wiring = NON_SPEC_ACCESSORS[id];
|
|
220
|
+
if (!wiring) continue;
|
|
221
|
+
const init = wiring.ctorArg ? `.new(${wiring.ctorArg})` : '';
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push(` def ${wiring.prop}`);
|
|
224
|
+
lines.push(` @${wiring.prop} ||= WorkOS::${wiring.className}${init}`);
|
|
225
|
+
lines.push(' end');
|
|
226
|
+
}
|
|
227
|
+
lines.push(' # @oagen-ignore-end');
|
|
228
|
+
|
|
229
|
+
lines.push(' end');
|
|
230
|
+
lines.push('end');
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
path: 'lib/workos/client.rb',
|
|
234
|
+
content: lines.join('\n'),
|
|
235
|
+
integrateTarget: true,
|
|
236
|
+
overwriteExisting: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { toUpperSnakeCase } from '@workos/oagen';
|
|
3
|
+
import { className, fileName } from './naming.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate Ruby enum class files.
|
|
7
|
+
*
|
|
8
|
+
* Each enum becomes a class under `WorkOS::Types::` with uppercase constants
|
|
9
|
+
* and a frozen `ALL` array of all values.
|
|
10
|
+
*/
|
|
11
|
+
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
12
|
+
void ctx;
|
|
13
|
+
if (enums.length === 0) return [];
|
|
14
|
+
|
|
15
|
+
const files: GeneratedFile[] = [];
|
|
16
|
+
const aliasOf = collectEnumAliasOf(enums);
|
|
17
|
+
|
|
18
|
+
for (const enumDef of enums) {
|
|
19
|
+
const cls = className(enumDef.name);
|
|
20
|
+
|
|
21
|
+
// If this enum duplicates another (by value set), emit a Ruby constant
|
|
22
|
+
// alias. Zeitwerk autoloads the canonical when the alias is first
|
|
23
|
+
// referenced.
|
|
24
|
+
const canonicalName = aliasOf.get(enumDef.name);
|
|
25
|
+
if (canonicalName) {
|
|
26
|
+
const canonicalCls = className(canonicalName);
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
lines.push('module WorkOS');
|
|
29
|
+
lines.push(' module Types');
|
|
30
|
+
lines.push(` ${cls} = ${canonicalCls}`);
|
|
31
|
+
lines.push(' end');
|
|
32
|
+
lines.push('end');
|
|
33
|
+
files.push({
|
|
34
|
+
path: `lib/workos/types/${fileName(enumDef.name)}.rb`,
|
|
35
|
+
content: lines.join('\n'),
|
|
36
|
+
integrateTarget: true,
|
|
37
|
+
overwriteExisting: true,
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deduplicate repeated string values.
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
const uniqueValues: typeof enumDef.values = [];
|
|
45
|
+
for (const v of enumDef.values) {
|
|
46
|
+
const str = String(v.value);
|
|
47
|
+
if (!seen.has(str)) {
|
|
48
|
+
seen.add(str);
|
|
49
|
+
uniqueValues.push({ ...v, value: str });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (uniqueValues.length === 0) {
|
|
54
|
+
// No values — emit a placeholder string-alias class.
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
lines.push('module WorkOS');
|
|
57
|
+
lines.push(' module Types');
|
|
58
|
+
lines.push(` class ${cls}`);
|
|
59
|
+
lines.push(' ALL = [].freeze');
|
|
60
|
+
lines.push(' end');
|
|
61
|
+
lines.push(' end');
|
|
62
|
+
lines.push('end');
|
|
63
|
+
files.push({
|
|
64
|
+
path: `lib/workos/types/${fileName(enumDef.name)}.rb`,
|
|
65
|
+
content: lines.join('\n'),
|
|
66
|
+
integrateTarget: true,
|
|
67
|
+
overwriteExisting: true,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determine string vs integer enum.
|
|
73
|
+
const allIntegers = uniqueValues.every((v) => typeof v.value === 'number' && Number.isInteger(v.value));
|
|
74
|
+
|
|
75
|
+
// Reserve ALL for the frozen list constant at the bottom; any enum value
|
|
76
|
+
// whose upper-snake form collides gets a VALUE_ prefix.
|
|
77
|
+
const RESERVED_MEMBER_NAMES = new Set(['ALL']);
|
|
78
|
+
const usedNames = new Set<string>();
|
|
79
|
+
const memberLines: string[] = [];
|
|
80
|
+
const allEntries: string[] = [];
|
|
81
|
+
|
|
82
|
+
for (const v of uniqueValues) {
|
|
83
|
+
let member = toUpperSnakeCase(String(v.value));
|
|
84
|
+
// Ruby constants must start with an uppercase letter.
|
|
85
|
+
if (!/^[A-Z]/.test(member)) member = `VALUE_${member}`;
|
|
86
|
+
if (RESERVED_MEMBER_NAMES.has(member)) member = `VALUE_${member}`;
|
|
87
|
+
if (usedNames.has(member)) {
|
|
88
|
+
let suffix = 2;
|
|
89
|
+
while (usedNames.has(`${member}_${suffix}`)) suffix++;
|
|
90
|
+
member = `${member}_${suffix}`;
|
|
91
|
+
}
|
|
92
|
+
usedNames.add(member);
|
|
93
|
+
const valueLit = allIntegers ? String(v.value) : `'${String(v.value).replace(/'/g, "\\'")}'`;
|
|
94
|
+
if (v.deprecated) {
|
|
95
|
+
memberLines.push(` # @deprecated`);
|
|
96
|
+
}
|
|
97
|
+
memberLines.push(` ${member} = ${valueLit}`);
|
|
98
|
+
allEntries.push(member);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
lines.push('module WorkOS');
|
|
103
|
+
lines.push(' module Types');
|
|
104
|
+
lines.push(` class ${cls}`);
|
|
105
|
+
lines.push(...memberLines);
|
|
106
|
+
lines.push(` ALL = [${allEntries.join(', ')}].freeze`);
|
|
107
|
+
lines.push(' end');
|
|
108
|
+
lines.push(' end');
|
|
109
|
+
lines.push('end');
|
|
110
|
+
|
|
111
|
+
files.push({
|
|
112
|
+
path: `lib/workos/types/${fileName(enumDef.name)}.rb`,
|
|
113
|
+
content: lines.join('\n'),
|
|
114
|
+
integrateTarget: true,
|
|
115
|
+
overwriteExisting: true,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect when two or more enums have the same value set — pick the lexicographically
|
|
124
|
+
* first as canonical and return a map of aliasName -> canonicalName for the rest.
|
|
125
|
+
*/
|
|
126
|
+
function collectEnumAliasOf(enums: Enum[]): Map<string, string> {
|
|
127
|
+
const hashGroups = new Map<string, string[]>();
|
|
128
|
+
for (const e of enums) {
|
|
129
|
+
const hash = [...e.values]
|
|
130
|
+
.map((v) => String(v.value))
|
|
131
|
+
.sort()
|
|
132
|
+
.join('|');
|
|
133
|
+
if (!hashGroups.has(hash)) hashGroups.set(hash, []);
|
|
134
|
+
hashGroups.get(hash)!.push(e.name);
|
|
135
|
+
}
|
|
136
|
+
const aliasOf = new Map<string, string>();
|
|
137
|
+
for (const names of hashGroups.values()) {
|
|
138
|
+
if (names.length <= 1) continue;
|
|
139
|
+
const sorted = [...names].sort();
|
|
140
|
+
const canonical = sorted[0];
|
|
141
|
+
for (let i = 1; i < sorted.length; i++) aliasOf.set(sorted[i], canonical);
|
|
142
|
+
}
|
|
143
|
+
return aliasOf;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Collect the set of enum names that were emitted. */
|
|
147
|
+
export function collectEnumSymbols(enums: Enum[]): string[] {
|
|
148
|
+
return enums.map((e) => e.name);
|
|
149
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Emitter,
|
|
3
|
+
EmitterContext,
|
|
4
|
+
FormatCommand,
|
|
5
|
+
GeneratedFile,
|
|
6
|
+
ApiSpec,
|
|
7
|
+
Model,
|
|
8
|
+
Enum,
|
|
9
|
+
Service,
|
|
10
|
+
} from '@workos/oagen';
|
|
11
|
+
import { generateModels } from './models.js';
|
|
12
|
+
import { generateEnums } from './enums.js';
|
|
13
|
+
import { generateResources } from './resources.js';
|
|
14
|
+
import { generateClient } from './client.js';
|
|
15
|
+
import { generateTests } from './tests.js';
|
|
16
|
+
import { buildOperationsMap } from './manifest.js';
|
|
17
|
+
import { generateRbiFiles } from './rbi.js';
|
|
18
|
+
|
|
19
|
+
/** Ensure every generated file's content ends with a trailing newline. */
|
|
20
|
+
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
21
|
+
for (const f of files) {
|
|
22
|
+
if (f.content && !f.content.endsWith('\n')) {
|
|
23
|
+
f.content += '\n';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const rubyEmitter: Emitter = {
|
|
30
|
+
language: 'ruby',
|
|
31
|
+
|
|
32
|
+
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
33
|
+
return ensureTrailingNewlines(generateModels(models, ctx));
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
37
|
+
return ensureTrailingNewlines(generateEnums(enums, ctx));
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
41
|
+
return ensureTrailingNewlines(generateResources(services, ctx));
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
45
|
+
return ensureTrailingNewlines(generateClient(spec, ctx));
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
generateErrors(): GeneratedFile[] {
|
|
49
|
+
// Hand-maintained in the target SDK — base_client.rb, errors.rb,
|
|
50
|
+
// configuration.rb, hash_provider.rb, types/list_struct.rb,
|
|
51
|
+
// test/test_helper.rb, and version.rb all carry `@oagen-ignore-file`
|
|
52
|
+
// and are not regenerated.
|
|
53
|
+
return [];
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
generateTypeSignatures(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
57
|
+
return ensureTrailingNewlines(generateRbiFiles(spec, ctx));
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
61
|
+
return ensureTrailingNewlines(generateTests(spec, ctx));
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
buildOperationsMap(spec: ApiSpec, ctx: EmitterContext) {
|
|
65
|
+
return buildOperationsMap(spec, ctx);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
fileHeader(): string {
|
|
69
|
+
return `# frozen_string_literal: true\n\n# This file is auto-generated by oagen. Do not edit.`;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
formatCommand(targetDir: string): FormatCommand | null {
|
|
73
|
+
void targetDir;
|
|
74
|
+
// The WorkOS Ruby SDK uses standardrb. oagen appends the list of written
|
|
75
|
+
// files as positional args after `args`; we filter to .rb files and pipe
|
|
76
|
+
// them to `bundle exec standardrb --fix`. cwd is set to targetDir by the
|
|
77
|
+
// harness so bundler picks up the SDK's Gemfile.
|
|
78
|
+
//
|
|
79
|
+
// Note: errors are NOT swallowed here — if standardrb fails (missing
|
|
80
|
+
// bundle, syntax error, etc.) the engine surfaces it. Silence only hides
|
|
81
|
+
// the fact that formatting never happened.
|
|
82
|
+
return {
|
|
83
|
+
cmd: 'bash',
|
|
84
|
+
args: [
|
|
85
|
+
'-c',
|
|
86
|
+
'RB_FILES=$(for f in "$@"; do case "$f" in *.rb) echo "$f";; esac; done); ' +
|
|
87
|
+
'if [ -n "$RB_FILES" ]; then printf "%s\\n" "$RB_FILES" | xargs bundle exec standardrb --fix --format quiet; fi',
|
|
88
|
+
'--',
|
|
89
|
+
],
|
|
90
|
+
batchSize: 1000,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, OperationsMap } from '@workos/oagen';
|
|
2
|
+
import { servicePropertyName } from './naming.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build operation-to-SDK-method mapping for the manifest.
|
|
6
|
+
*
|
|
7
|
+
* Uses each resolved operation's actual mountOn (not the service default) so
|
|
8
|
+
* operations remounted via operationHints land on the correct service prop.
|
|
9
|
+
* Split operations emit one entry per wrapper (keyed by wrapper name + variant).
|
|
10
|
+
*/
|
|
11
|
+
export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): OperationsMap {
|
|
12
|
+
void spec;
|
|
13
|
+
const manifest: OperationsMap = {};
|
|
14
|
+
|
|
15
|
+
for (const r of ctx.resolvedOperations ?? []) {
|
|
16
|
+
const op = r.operation;
|
|
17
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
18
|
+
const propName = servicePropertyName(r.mountOn);
|
|
19
|
+
manifest[httpKey] = { sdkMethod: r.methodName, service: propName };
|
|
20
|
+
if (r.wrappers && r.wrappers.length > 0) {
|
|
21
|
+
for (const w of r.wrappers) {
|
|
22
|
+
manifest[`${httpKey}#${w.targetVariant}`] = { sdkMethod: w.name, service: propName };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return manifest;
|
|
28
|
+
}
|