@workos/oagen-emitters 0.15.2 → 0.16.1
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +48 -1
- package/dist/index.d.mts +51 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +852 -2
- package/dist/index.mjs.map +1 -0
- package/dist/{plugin-Xkr83G9A.mjs → plugin-CpO8rePT.mjs} +1219 -493
- package/dist/plugin-CpO8rePT.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -7
- package/src/dotnet/naming.ts +1 -1
- package/src/go/naming.ts +1 -1
- package/src/index.ts +15 -0
- package/src/node/enums.ts +17 -4
- package/src/node/index.ts +264 -4
- package/src/node/live-surface.ts +309 -0
- package/src/node/models.ts +69 -3
- package/src/node/naming.ts +204 -23
- package/src/node/resources.ts +39 -3
- package/src/node/tests.ts +29 -3
- package/src/node/utils.ts +140 -22
- package/src/snippets/dotnet.ts +159 -0
- package/src/snippets/go.ts +148 -0
- package/src/snippets/index.ts +8 -0
- package/src/snippets/kotlin.ts +144 -0
- package/src/snippets/php.ts +149 -0
- package/src/snippets/plugin.ts +36 -0
- package/src/snippets/python.ts +135 -0
- package/src/snippets/ruby.ts +152 -0
- package/src/snippets/rust.ts +189 -0
- package/test/node/enums.test.ts +239 -2
- package/test/node/live-surface.test.ts +771 -1
- package/test/node/models.test.ts +738 -3
- package/test/node/naming.test.ts +159 -0
- package/test/node/resources.test.ts +464 -0
- package/test/node/utils.test.ts +157 -2
- package/test/snippets/_helpers.ts +67 -0
- package/test/snippets/dotnet.test.ts +49 -0
- package/test/snippets/go.test.ts +94 -0
- package/test/snippets/kotlin.test.ts +53 -0
- package/test/snippets/php.test.ts +48 -0
- package/test/snippets/python.test.ts +73 -0
- package/test/snippets/ruby.test.ts +339 -0
- package/test/snippets/rust.test.ts +76 -0
- package/dist/plugin-Xkr83G9A.mjs.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { EmitterContext, ExampleBuilder, ResolvedOperation, SnippetArg, SnippetEmitter } from '@workos/oagen';
|
|
2
|
+
import { collectSnippetArgs, collectWrapperArgs } from '@workos/oagen';
|
|
3
|
+
import { apiClassName, methodName, packageSegment, propertyName } from '../kotlin/naming.js';
|
|
4
|
+
|
|
5
|
+
const INDENT = ' ';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Emits Java-syntax snippets (rendered as `.java`) backed by the Kotlin SDK's
|
|
9
|
+
* naming so JVM callers see the same method and options names whether they
|
|
10
|
+
* write Kotlin or Java. The WorkOS docs render the `.java` extension because
|
|
11
|
+
* Java is the most common JVM consumer; Kotlin call sites would look almost
|
|
12
|
+
* identical apart from `var`/`val` and named-argument syntax.
|
|
13
|
+
*/
|
|
14
|
+
export const kotlinSnippetEmitter: SnippetEmitter = {
|
|
15
|
+
language: 'java',
|
|
16
|
+
fileExtension: 'java',
|
|
17
|
+
|
|
18
|
+
renderOperation(resolved, ctx, examples) {
|
|
19
|
+
if (resolved.urlBuilder) return null;
|
|
20
|
+
|
|
21
|
+
const method =
|
|
22
|
+
resolved.wrappers && resolved.wrappers.length > 0
|
|
23
|
+
? methodName(resolved.wrappers[0]!.name)
|
|
24
|
+
: methodName(resolved.methodName);
|
|
25
|
+
|
|
26
|
+
return renderCall(resolved, ctx, examples, method);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function renderCall(
|
|
31
|
+
resolved: ResolvedOperation,
|
|
32
|
+
ctx: EmitterContext,
|
|
33
|
+
examples: ExampleBuilder,
|
|
34
|
+
method: string,
|
|
35
|
+
): string {
|
|
36
|
+
const apiClass = apiClassName(resolved.mountOn);
|
|
37
|
+
const accessor = propertyName(resolved.mountOn);
|
|
38
|
+
const optionsType = `${methodName(
|
|
39
|
+
resolved.wrappers && resolved.wrappers.length > 0 ? resolved.wrappers[0]!.name : resolved.methodName,
|
|
40
|
+
)}Options`;
|
|
41
|
+
const optionsTypeCapitalized = optionsType[0]!.toUpperCase() + optionsType.slice(1);
|
|
42
|
+
const apiSubPackage = packageSegment(resolved.mountOn);
|
|
43
|
+
|
|
44
|
+
let args: SnippetArg[];
|
|
45
|
+
let pathArgs: SnippetArg[];
|
|
46
|
+
let optionsArgs: SnippetArg[];
|
|
47
|
+
|
|
48
|
+
if (resolved.wrappers && resolved.wrappers.length > 0) {
|
|
49
|
+
args = collectWrapperArgs(resolved.wrappers[0]!, ctx, examples);
|
|
50
|
+
pathArgs = [];
|
|
51
|
+
optionsArgs = args;
|
|
52
|
+
} else {
|
|
53
|
+
const collected = collectSnippetArgs(resolved, ctx, examples);
|
|
54
|
+
args = collected.args;
|
|
55
|
+
pathArgs = args.filter((a) => a.source === 'path');
|
|
56
|
+
optionsArgs = args.filter((a) => a.source !== 'path');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
lines.push('import com.workos.WorkOS;');
|
|
61
|
+
if (optionsArgs.length > 0) {
|
|
62
|
+
lines.push(`import com.workos.${apiSubPackage}.${apiClass}Api.${optionsTypeCapitalized};`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('WorkOS workos = new WorkOS("sk_example_123456789");');
|
|
66
|
+
lines.push('');
|
|
67
|
+
|
|
68
|
+
const callParts: string[] = pathArgs.map((p) => renderValue(p.value));
|
|
69
|
+
|
|
70
|
+
if (optionsArgs.length > 0) {
|
|
71
|
+
lines.push(`${optionsTypeCapitalized} options = ${optionsTypeCapitalized}.builder()`);
|
|
72
|
+
for (const a of optionsArgs) {
|
|
73
|
+
const prop = propertyName(a.wireName);
|
|
74
|
+
const value = renderValue(a.value);
|
|
75
|
+
const indentedValue = indentContinuationLines(value, INDENT);
|
|
76
|
+
lines.push(`${INDENT}.${prop}(${indentedValue})`);
|
|
77
|
+
}
|
|
78
|
+
lines.push(`${INDENT}.build();`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
callParts.push('options');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (callParts.length === 0) {
|
|
84
|
+
lines.push(`workos.${accessor}.${method}();`);
|
|
85
|
+
} else {
|
|
86
|
+
lines.push(`workos.${accessor}.${method}(${callParts.join(', ')});`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Java literal rendering
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
function renderValue(value: unknown): string {
|
|
97
|
+
if (value === null || value === undefined) return 'null';
|
|
98
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
99
|
+
if (typeof value === 'number') return String(value);
|
|
100
|
+
if (typeof value === 'string') return javaString(value);
|
|
101
|
+
if (Array.isArray(value)) return renderList(value);
|
|
102
|
+
if (typeof value === 'object') return renderMap(value as Record<string, unknown>);
|
|
103
|
+
return 'null';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function javaString(s: string): string {
|
|
107
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderList(items: unknown[]): string {
|
|
111
|
+
if (items.length === 0) return 'List.of()';
|
|
112
|
+
const rendered = items.map((v) => renderValue(v));
|
|
113
|
+
const oneline = `List.of(${rendered.join(', ')})`;
|
|
114
|
+
if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
|
|
115
|
+
const lines: string[] = ['List.of('];
|
|
116
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
117
|
+
const trailing = i < rendered.length - 1 ? ',' : '';
|
|
118
|
+
lines.push(`${INDENT}${indentContinuationLines(rendered[i]!, INDENT)}${trailing}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push(')');
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderMap(obj: Record<string, unknown>): string {
|
|
125
|
+
const entries = Object.entries(obj);
|
|
126
|
+
if (entries.length === 0) return 'Map.of()';
|
|
127
|
+
const rendered = entries.map(([k, v]) => ({ key: k, value: renderValue(v) }));
|
|
128
|
+
const oneline = `Map.of(${rendered.map((e) => `"${e.key}", ${e.value}`).join(', ')})`;
|
|
129
|
+
if (oneline.length <= 80 && rendered.every((e) => !e.value.includes('\n'))) return oneline;
|
|
130
|
+
const lines: string[] = ['Map.of('];
|
|
131
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
132
|
+
const e = rendered[i]!;
|
|
133
|
+
const trailing = i < rendered.length - 1 ? ',' : '';
|
|
134
|
+
lines.push(`${INDENT}"${e.key}", ${indentContinuationLines(e.value, INDENT)}${trailing}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push(')');
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function indentContinuationLines(s: string, indent: string): string {
|
|
141
|
+
if (!s.includes('\n')) return s;
|
|
142
|
+
const lines = s.split('\n');
|
|
143
|
+
return lines.map((line, i) => (i === 0 ? line : `${indent}${line}`)).join('\n');
|
|
144
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { SnippetArg, SnippetEmitter } from '@workos/oagen';
|
|
2
|
+
import { collectSnippetArgs, collectWrapperArgs } from '@workos/oagen';
|
|
3
|
+
import { fieldName, servicePropertyName } from '../php/naming.js';
|
|
4
|
+
|
|
5
|
+
const INDENT = ' ';
|
|
6
|
+
|
|
7
|
+
export const phpSnippetEmitter: SnippetEmitter = {
|
|
8
|
+
language: 'php',
|
|
9
|
+
fileExtension: 'php',
|
|
10
|
+
|
|
11
|
+
renderOperation(resolved, ctx, examples) {
|
|
12
|
+
if (resolved.urlBuilder) return null;
|
|
13
|
+
|
|
14
|
+
const accessor = servicePropertyName(resolved.mountOn);
|
|
15
|
+
const method = fieldName(
|
|
16
|
+
resolved.wrappers && resolved.wrappers.length > 0 ? resolved.wrappers[0]!.name : resolved.methodName,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
let args: SnippetArg[];
|
|
20
|
+
let collisionNames: Set<string>;
|
|
21
|
+
if (resolved.wrappers && resolved.wrappers.length > 0) {
|
|
22
|
+
args = collectWrapperArgs(resolved.wrappers[0]!, ctx, examples);
|
|
23
|
+
collisionNames = new Set();
|
|
24
|
+
} else {
|
|
25
|
+
const collected = collectSnippetArgs(resolved, ctx, examples);
|
|
26
|
+
args = collected.args;
|
|
27
|
+
collisionNames = collected.collisionNames;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return renderCall(accessor, method, toPhpArgs(args, collisionNames));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface PhpArg {
|
|
35
|
+
keyword: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toPhpArgs(args: SnippetArg[], collisions: Set<string>): PhpArg[] {
|
|
40
|
+
const seen = new Set<string>();
|
|
41
|
+
const out: PhpArg[] = [];
|
|
42
|
+
for (const a of args) {
|
|
43
|
+
const keyword = phpKeyword(a, collisions);
|
|
44
|
+
if (seen.has(keyword)) continue;
|
|
45
|
+
seen.add(keyword);
|
|
46
|
+
out.push({ keyword, value: renderValue(a.value) });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function phpKeyword(arg: SnippetArg, collisions: Set<string>): string {
|
|
52
|
+
const base = fieldName(arg.wireName);
|
|
53
|
+
if (arg.source === 'body' && collisions.has(arg.wireName)) return `body${capitalize(base)}`;
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function capitalize(s: string): string {
|
|
58
|
+
return s.length === 0 ? s : s[0]!.toUpperCase() + s.slice(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderCall(accessor: string, method: string, args: PhpArg[]): string {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
lines.push('<?php');
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('use WorkOS\\WorkOS;');
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push('$workos = new WorkOS(');
|
|
68
|
+
lines.push(`${INDENT}apiKey: 'sk_example_123456789',`);
|
|
69
|
+
lines.push(`${INDENT}clientId: 'client_123456789',`);
|
|
70
|
+
lines.push(');');
|
|
71
|
+
lines.push('');
|
|
72
|
+
|
|
73
|
+
const target = `$workos->${accessor}()->${method}`;
|
|
74
|
+
if (args.length === 0) {
|
|
75
|
+
lines.push(`${target}();`);
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (args.length === 1 && !args[0]!.value.includes('\n')) {
|
|
80
|
+
const a = args[0]!;
|
|
81
|
+
lines.push(`${target}(${a.keyword}: ${a.value});`);
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lines.push(`${target}(`);
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const a = args[i]!;
|
|
88
|
+
const trailing = i < args.length - 1 ? ',' : ',';
|
|
89
|
+
const valueIndented = indentContinuationLines(a.value, INDENT);
|
|
90
|
+
lines.push(`${INDENT}${a.keyword}: ${valueIndented}${trailing}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push(');');
|
|
93
|
+
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// PHP literal rendering
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
function renderValue(value: unknown): string {
|
|
102
|
+
if (value === null || value === undefined) return 'null';
|
|
103
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
104
|
+
if (typeof value === 'number') return String(value);
|
|
105
|
+
if (typeof value === 'string') return phpString(value);
|
|
106
|
+
if (Array.isArray(value)) return renderArray(value);
|
|
107
|
+
if (typeof value === 'object') return renderAssoc(value as Record<string, unknown>);
|
|
108
|
+
return 'null';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function phpString(s: string): string {
|
|
112
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderArray(items: unknown[]): string {
|
|
116
|
+
if (items.length === 0) return '[]';
|
|
117
|
+
const rendered = items.map((v) => renderValue(v));
|
|
118
|
+
const oneline = `[${rendered.join(', ')}]`;
|
|
119
|
+
if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
|
|
120
|
+
const lines: string[] = ['['];
|
|
121
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
122
|
+
const trailing = i < rendered.length - 1 ? ',' : ',';
|
|
123
|
+
lines.push(`${INDENT}${indentContinuationLines(rendered[i]!, INDENT)}${trailing}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push(']');
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderAssoc(obj: Record<string, unknown>): string {
|
|
130
|
+
const entries = Object.entries(obj);
|
|
131
|
+
if (entries.length === 0) return '[]';
|
|
132
|
+
const rendered = entries.map(([k, v]) => ({ key: k, value: renderValue(v) }));
|
|
133
|
+
const oneline = `[${rendered.map((e) => `'${e.key}' => ${e.value}`).join(', ')}]`;
|
|
134
|
+
if (oneline.length <= 80 && rendered.every((e) => !e.value.includes('\n'))) return oneline;
|
|
135
|
+
const lines: string[] = ['['];
|
|
136
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
137
|
+
const e = rendered[i]!;
|
|
138
|
+
const trailing = i < rendered.length - 1 ? ',' : ',';
|
|
139
|
+
lines.push(`${INDENT}'${e.key}' => ${indentContinuationLines(e.value, INDENT)}${trailing}`);
|
|
140
|
+
}
|
|
141
|
+
lines.push(']');
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function indentContinuationLines(s: string, indent: string): string {
|
|
146
|
+
if (!s.includes('\n')) return s;
|
|
147
|
+
const lines = s.split('\n');
|
|
148
|
+
return lines.map((line, i) => (i === 0 ? line : `${indent}${line}`)).join('\n');
|
|
149
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SnippetEmitter } from '@workos/oagen';
|
|
2
|
+
import { dotnetSnippetEmitter } from './dotnet.js';
|
|
3
|
+
import { goSnippetEmitter } from './go.js';
|
|
4
|
+
import { kotlinSnippetEmitter } from './kotlin.js';
|
|
5
|
+
import { phpSnippetEmitter } from './php.js';
|
|
6
|
+
import { pythonSnippetEmitter } from './python.js';
|
|
7
|
+
import { rubySnippetEmitter } from './ruby.js';
|
|
8
|
+
import { rustSnippetEmitter } from './rust.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bundle of snippet emitters for every WorkOS SDK language we currently
|
|
12
|
+
* generate call-site samples for. Node is intentionally absent — the docs
|
|
13
|
+
* pipeline still owns hand-authored TypeScript samples there.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { runSnippetEmitters, workosSnippetsPlugin } from '@workos/oagen-emitters';
|
|
17
|
+
*
|
|
18
|
+
* const snippets = runSnippetEmitters(workosSnippetsPlugin.snippets, ctx);
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Each entry mirrors a published WorkOS SDK and reuses that emitter's naming
|
|
22
|
+
* helpers (`src/<lang>/naming.ts`), so generated samples stay in lockstep
|
|
23
|
+
* with the SDK they document — method names, mount-target casing, parameter
|
|
24
|
+
* names, and reserved-word handling all match what the real SDK exposes.
|
|
25
|
+
*/
|
|
26
|
+
export const workosSnippetsPlugin: { snippets: SnippetEmitter[] } = {
|
|
27
|
+
snippets: [
|
|
28
|
+
rubySnippetEmitter,
|
|
29
|
+
pythonSnippetEmitter,
|
|
30
|
+
phpSnippetEmitter,
|
|
31
|
+
goSnippetEmitter,
|
|
32
|
+
dotnetSnippetEmitter,
|
|
33
|
+
kotlinSnippetEmitter,
|
|
34
|
+
rustSnippetEmitter,
|
|
35
|
+
],
|
|
36
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { SnippetArg, SnippetEmitter } from '@workos/oagen';
|
|
2
|
+
import { collectSnippetArgs, collectWrapperArgs, toSnakeCase } from '@workos/oagen';
|
|
3
|
+
import { fieldName, safeParamName } from '../python/naming.js';
|
|
4
|
+
|
|
5
|
+
const INDENT = ' ';
|
|
6
|
+
|
|
7
|
+
export const pythonSnippetEmitter: SnippetEmitter = {
|
|
8
|
+
language: 'python',
|
|
9
|
+
fileExtension: 'py',
|
|
10
|
+
|
|
11
|
+
renderOperation(resolved, ctx, examples) {
|
|
12
|
+
if (resolved.urlBuilder) return null;
|
|
13
|
+
|
|
14
|
+
const accessor = toSnakeCase(resolved.mountOn);
|
|
15
|
+
|
|
16
|
+
if (resolved.wrappers && resolved.wrappers.length > 0) {
|
|
17
|
+
const wrapper = resolved.wrappers[0]!;
|
|
18
|
+
const args = collectWrapperArgs(wrapper, ctx, examples);
|
|
19
|
+
return renderCall(accessor, wrapper.name, toPyArgs(args, new Set()));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { args, collisionNames } = collectSnippetArgs(resolved, ctx, examples);
|
|
23
|
+
return renderCall(accessor, resolved.methodName, toPyArgs(args, collisionNames));
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface PyArg {
|
|
28
|
+
keyword: string;
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toPyArgs(args: SnippetArg[], collisionNames: Set<string>): PyArg[] {
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const out: PyArg[] = [];
|
|
35
|
+
for (const a of args) {
|
|
36
|
+
const keyword = pythonKeyword(a, collisionNames);
|
|
37
|
+
if (seen.has(keyword)) continue;
|
|
38
|
+
seen.add(keyword);
|
|
39
|
+
out.push({ keyword, value: renderValue(a.value) });
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pythonKeyword(arg: SnippetArg, collisions: Set<string>): string {
|
|
45
|
+
if (arg.source === 'body') {
|
|
46
|
+
const base = fieldName(arg.wireName);
|
|
47
|
+
return collisions.has(arg.wireName) ? `body_${base}` : base;
|
|
48
|
+
}
|
|
49
|
+
return safeParamName(arg.wireName);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderCall(accessor: string, method: string, args: PyArg[]): string {
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
lines.push('from workos import WorkOSClient');
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('client = WorkOSClient(api_key="sk_example_123456789", client_id="client_123456789")');
|
|
57
|
+
lines.push('');
|
|
58
|
+
|
|
59
|
+
const target = `client.${accessor}.${method}`;
|
|
60
|
+
if (args.length === 0) {
|
|
61
|
+
lines.push(`${target}()`);
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (args.length === 1 && !args[0]!.value.includes('\n')) {
|
|
66
|
+
const a = args[0]!;
|
|
67
|
+
lines.push(`${target}(${a.keyword}=${a.value})`);
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lines.push(`${target}(`);
|
|
72
|
+
for (let i = 0; i < args.length; i++) {
|
|
73
|
+
const a = args[i]!;
|
|
74
|
+
const trailing = i < args.length - 1 ? ',' : '';
|
|
75
|
+
const valueIndented = indentContinuationLines(a.value, INDENT);
|
|
76
|
+
lines.push(`${INDENT}${a.keyword}=${valueIndented}${trailing}`);
|
|
77
|
+
}
|
|
78
|
+
lines.push(')');
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Python literal rendering
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
function renderValue(value: unknown): string {
|
|
87
|
+
if (value === null || value === undefined) return 'None';
|
|
88
|
+
if (typeof value === 'boolean') return value ? 'True' : 'False';
|
|
89
|
+
if (typeof value === 'number') return String(value);
|
|
90
|
+
if (typeof value === 'string') return pythonString(value);
|
|
91
|
+
if (Array.isArray(value)) return renderArray(value);
|
|
92
|
+
if (typeof value === 'object') return renderDict(value as Record<string, unknown>);
|
|
93
|
+
return 'None';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pythonString(s: string): string {
|
|
97
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderArray(items: unknown[]): string {
|
|
101
|
+
if (items.length === 0) return '[]';
|
|
102
|
+
const rendered = items.map((v) => renderValue(v));
|
|
103
|
+
const oneline = `[${rendered.join(', ')}]`;
|
|
104
|
+
if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
|
|
105
|
+
const lines: string[] = ['['];
|
|
106
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
107
|
+
const trailing = i < rendered.length - 1 ? ',' : '';
|
|
108
|
+
lines.push(`${INDENT}${indentContinuationLines(rendered[i]!, INDENT)}${trailing}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(']');
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderDict(obj: Record<string, unknown>): string {
|
|
115
|
+
const entries = Object.entries(obj);
|
|
116
|
+
if (entries.length === 0) return '{}';
|
|
117
|
+
const rendered = entries.map(([k, v]) => ({ key: k, value: renderValue(v) }));
|
|
118
|
+
const oneline = `{${rendered.map((e) => `"${e.key}": ${e.value}`).join(', ')}}`;
|
|
119
|
+
if (oneline.length <= 80 && rendered.every((e) => !e.value.includes('\n'))) return oneline;
|
|
120
|
+
|
|
121
|
+
const lines: string[] = ['{'];
|
|
122
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
123
|
+
const e = rendered[i]!;
|
|
124
|
+
const trailing = i < rendered.length - 1 ? ',' : '';
|
|
125
|
+
lines.push(`${INDENT}"${e.key}": ${indentContinuationLines(e.value, INDENT)}${trailing}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push('}');
|
|
128
|
+
return lines.join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function indentContinuationLines(s: string, indent: string): string {
|
|
132
|
+
if (!s.includes('\n')) return s;
|
|
133
|
+
const lines = s.split('\n');
|
|
134
|
+
return lines.map((line, i) => (i === 0 ? line : `${indent}${line}`)).join('\n');
|
|
135
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { EmitterContext, SnippetArg, SnippetEmitter } from '@workos/oagen';
|
|
2
|
+
import { collectSnippetArgs, collectWrapperArgs, toSnakeCase } from '@workos/oagen';
|
|
3
|
+
import { buildExportedClassNameSet, fieldName, resolveServiceTarget, safeParamName } from '../ruby/naming.js';
|
|
4
|
+
|
|
5
|
+
const INDENT = ' ';
|
|
6
|
+
|
|
7
|
+
export const rubySnippetEmitter: SnippetEmitter = {
|
|
8
|
+
language: 'ruby',
|
|
9
|
+
fileExtension: 'rb',
|
|
10
|
+
|
|
11
|
+
renderOperation(resolved, ctx, examples) {
|
|
12
|
+
if (resolved.urlBuilder) return null;
|
|
13
|
+
|
|
14
|
+
const accessor = serviceAccessor(resolved.mountOn, ctx);
|
|
15
|
+
|
|
16
|
+
if (resolved.wrappers && resolved.wrappers.length > 0) {
|
|
17
|
+
const wrapper = resolved.wrappers[0]!;
|
|
18
|
+
const args = collectWrapperArgs(wrapper, ctx, examples);
|
|
19
|
+
return renderCall(accessor, wrapper.name, toRubyArgs(args, new Set()));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { args, collisionNames } = collectSnippetArgs(resolved, ctx, examples);
|
|
23
|
+
return renderCall(accessor, resolved.methodName, toRubyArgs(args, collisionNames));
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface RubyArg {
|
|
28
|
+
keyword: string;
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toRubyArgs(args: SnippetArg[], collisionNames: Set<string>): RubyArg[] {
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const out: RubyArg[] = [];
|
|
35
|
+
for (const a of args) {
|
|
36
|
+
const keyword = rubyKeyword(a, collisionNames);
|
|
37
|
+
if (seen.has(keyword)) continue;
|
|
38
|
+
seen.add(keyword);
|
|
39
|
+
out.push({ keyword, value: renderValue(a.value) });
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rubyKeyword(arg: SnippetArg, collisions: Set<string>): string {
|
|
45
|
+
if (arg.source === 'body') {
|
|
46
|
+
const base = fieldName(arg.wireName);
|
|
47
|
+
return collisions.has(arg.wireName) ? `body_${base}` : base;
|
|
48
|
+
}
|
|
49
|
+
// path / query
|
|
50
|
+
return safeParamName(arg.wireName);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderCall(accessor: string, method: string, args: RubyArg[]): string {
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
lines.push('require "workos"');
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push('WorkOS.configure do |config|');
|
|
58
|
+
lines.push(`${INDENT}config.api_key = "sk_example_123456789"`);
|
|
59
|
+
lines.push('end');
|
|
60
|
+
lines.push('');
|
|
61
|
+
|
|
62
|
+
const target = `WorkOS.client.${accessor}.${method}`;
|
|
63
|
+
if (args.length === 0) {
|
|
64
|
+
lines.push(target);
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (args.length === 1 && !args[0]!.value.includes('\n')) {
|
|
69
|
+
const a = args[0]!;
|
|
70
|
+
lines.push(`${target}(${a.keyword}: ${a.value})`);
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
lines.push(`${target}(`);
|
|
75
|
+
for (let i = 0; i < args.length; i++) {
|
|
76
|
+
const a = args[i]!;
|
|
77
|
+
const trailing = i < args.length - 1 ? ',' : '';
|
|
78
|
+
const valueIndented = indentContinuationLines(a.value, INDENT);
|
|
79
|
+
lines.push(`${INDENT}${a.keyword}: ${valueIndented}${trailing}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(')');
|
|
82
|
+
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function serviceAccessor(mountOn: string, ctx: EmitterContext): string {
|
|
87
|
+
// The accessor uses the raw mount target (no `Service` suffix) to match
|
|
88
|
+
// the `client.organization_membership` style documented in workos-ruby.
|
|
89
|
+
void resolveServiceTarget(mountOn, buildExportedClassNameSet(ctx));
|
|
90
|
+
return toSnakeCase(mountOn);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Ruby literal rendering
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function renderValue(value: unknown): string {
|
|
98
|
+
if (value === null || value === undefined) return 'nil';
|
|
99
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
100
|
+
if (typeof value === 'number') return String(value);
|
|
101
|
+
if (typeof value === 'string') return rubyString(value);
|
|
102
|
+
if (Array.isArray(value)) return renderArray(value);
|
|
103
|
+
if (typeof value === 'object') return renderHash(value as Record<string, unknown>);
|
|
104
|
+
return 'nil';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function rubyString(s: string): string {
|
|
108
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderArray(items: unknown[]): string {
|
|
112
|
+
if (items.length === 0) return '[]';
|
|
113
|
+
const rendered = items.map((v) => renderValue(v));
|
|
114
|
+
const oneline = `[${rendered.join(', ')}]`;
|
|
115
|
+
if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
|
|
116
|
+
const lines: string[] = ['['];
|
|
117
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
118
|
+
const trailing = i < rendered.length - 1 ? ',' : '';
|
|
119
|
+
lines.push(`${INDENT}${indentContinuationLines(rendered[i]!, INDENT)}${trailing}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push(']');
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderHash(obj: Record<string, unknown>): string {
|
|
126
|
+
const entries = Object.entries(obj);
|
|
127
|
+
if (entries.length === 0) return '{}';
|
|
128
|
+
const rendered = entries.map(([k, v]) => ({ key: k, value: renderValue(v) }));
|
|
129
|
+
const oneLineParts = rendered.map((e) => `${formatInlineKey(e.key)} ${e.value}`);
|
|
130
|
+
const oneline = `{ ${oneLineParts.join(', ')} }`;
|
|
131
|
+
if (oneline.length <= 80 && rendered.every((e) => !e.value.includes('\n'))) return oneline;
|
|
132
|
+
|
|
133
|
+
const lines: string[] = ['{'];
|
|
134
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
135
|
+
const e = rendered[i]!;
|
|
136
|
+
const trailing = i < rendered.length - 1 ? ',' : '';
|
|
137
|
+
lines.push(`${INDENT}${formatInlineKey(e.key)} ${indentContinuationLines(e.value, INDENT)}${trailing}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push('}');
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatInlineKey(name: string): string {
|
|
144
|
+
if (/^[a-z_][a-zA-Z0-9_]*$/.test(name)) return `${name}:`;
|
|
145
|
+
return `"${name.replace(/"/g, '\\"')}" =>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function indentContinuationLines(s: string, indent: string): string {
|
|
149
|
+
if (!s.includes('\n')) return s;
|
|
150
|
+
const lines = s.split('\n');
|
|
151
|
+
return lines.map((line, i) => (i === 0 ? line : `${indent}${line}`)).join('\n');
|
|
152
|
+
}
|