@workos/oagen-emitters 0.15.2 → 0.16.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +12 -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-DuB1UozS.mjs} +56 -4
- package/dist/plugin-DuB1UozS.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +4 -4
- package/src/dotnet/naming.ts +1 -1
- package/src/go/naming.ts +1 -1
- package/src/index.ts +15 -0
- package/src/node/tests.ts +29 -3
- 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/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,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
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { EmitterContext, ExampleBuilder, ResolvedOperation, SnippetArg, SnippetEmitter } from '@workos/oagen';
|
|
2
|
+
import { collectSnippetArgs, collectWrapperArgs } from '@workos/oagen';
|
|
3
|
+
import { fieldName, methodName, moduleName, resourceAccessorName, typeName } from '../rust/naming.js';
|
|
4
|
+
|
|
5
|
+
const INDENT = ' ';
|
|
6
|
+
|
|
7
|
+
export const rustSnippetEmitter: SnippetEmitter = {
|
|
8
|
+
language: 'rust',
|
|
9
|
+
fileExtension: 'rs',
|
|
10
|
+
|
|
11
|
+
renderOperation(resolved, ctx, examples) {
|
|
12
|
+
if (resolved.urlBuilder) return null;
|
|
13
|
+
|
|
14
|
+
const method = methodName(
|
|
15
|
+
resolved.wrappers && resolved.wrappers.length > 0 ? resolved.wrappers[0]!.name : resolved.methodName,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return renderCall(resolved, ctx, examples, method);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function renderCall(
|
|
23
|
+
resolved: ResolvedOperation,
|
|
24
|
+
ctx: EmitterContext,
|
|
25
|
+
examples: ExampleBuilder,
|
|
26
|
+
method: string,
|
|
27
|
+
): string {
|
|
28
|
+
const accessor = resourceAccessorName(resolved.mountOn);
|
|
29
|
+
const modulePath = moduleName(resolved.mountOn);
|
|
30
|
+
const paramsStructName = `${typeName(
|
|
31
|
+
resolved.wrappers && resolved.wrappers.length > 0 ? resolved.wrappers[0]!.name : resolved.methodName,
|
|
32
|
+
)}Params`;
|
|
33
|
+
|
|
34
|
+
let args: SnippetArg[];
|
|
35
|
+
let pathArgs: SnippetArg[];
|
|
36
|
+
let structArgs: SnippetArg[];
|
|
37
|
+
|
|
38
|
+
if (resolved.wrappers && resolved.wrappers.length > 0) {
|
|
39
|
+
args = collectWrapperArgs(resolved.wrappers[0]!, ctx, examples);
|
|
40
|
+
pathArgs = [];
|
|
41
|
+
structArgs = args;
|
|
42
|
+
} else {
|
|
43
|
+
const collected = collectSnippetArgs(resolved, ctx, examples);
|
|
44
|
+
args = collected.args;
|
|
45
|
+
pathArgs = args.filter((a) => a.source === 'path');
|
|
46
|
+
structArgs = args.filter((a) => a.source !== 'path');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const imports: string[] = ['use workos::Client;'];
|
|
50
|
+
if (structArgs.length > 0) {
|
|
51
|
+
imports.push(`use workos::${modulePath}::${paramsStructName};`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
lines.push(...imports);
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push('#[tokio::main]');
|
|
58
|
+
lines.push('async fn main() -> Result<(), workos::Error> {');
|
|
59
|
+
lines.push(`${INDENT}let client = Client::builder()`);
|
|
60
|
+
lines.push(`${INDENT}${INDENT}.api_key("sk_example_123456789")`);
|
|
61
|
+
lines.push(`${INDENT}${INDENT}.client_id("client_123456789")`);
|
|
62
|
+
lines.push(`${INDENT}${INDENT}.build();`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
|
|
65
|
+
const callParts: string[] = [];
|
|
66
|
+
for (const p of pathArgs) {
|
|
67
|
+
const v = p.value;
|
|
68
|
+
if (typeof v === 'string') callParts.push(rustString(v));
|
|
69
|
+
else callParts.push(renderValue(v));
|
|
70
|
+
}
|
|
71
|
+
if (structArgs.length > 0) {
|
|
72
|
+
callParts.push(renderStructLiteral(paramsStructName, structArgs));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const callLines: string[] = [`${INDENT}let _result = client`, `${INDENT}${INDENT}.${accessor}()`];
|
|
76
|
+
if (callParts.length === 0) {
|
|
77
|
+
callLines.push(`${INDENT}${INDENT}.${method}()`);
|
|
78
|
+
} else if (callParts.length === 1 && !callParts[0]!.includes('\n')) {
|
|
79
|
+
callLines.push(`${INDENT}${INDENT}.${method}(${callParts[0]})`);
|
|
80
|
+
} else {
|
|
81
|
+
callLines.push(`${INDENT}${INDENT}.${method}(`);
|
|
82
|
+
for (let i = 0; i < callParts.length; i++) {
|
|
83
|
+
const trailing = i < callParts.length - 1 ? ',' : '';
|
|
84
|
+
const indented = indentContinuationLines(callParts[i]!, `${INDENT}${INDENT}${INDENT}`);
|
|
85
|
+
callLines.push(`${INDENT}${INDENT}${INDENT}${indented}${trailing}`);
|
|
86
|
+
}
|
|
87
|
+
callLines.push(`${INDENT}${INDENT})`);
|
|
88
|
+
}
|
|
89
|
+
callLines.push(`${INDENT}${INDENT}.await?;`);
|
|
90
|
+
lines.push(...callLines);
|
|
91
|
+
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`${INDENT}Ok(())`);
|
|
94
|
+
lines.push('}');
|
|
95
|
+
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderStructLiteral(structName: string, args: SnippetArg[]): string {
|
|
100
|
+
const lines: string[] = [`${structName} {`];
|
|
101
|
+
for (const a of args) {
|
|
102
|
+
const field = fieldName(a.wireName);
|
|
103
|
+
const value = renderStructValue(a.value);
|
|
104
|
+
lines.push(`${INDENT}${field}: ${indentContinuationLines(value, INDENT)},`);
|
|
105
|
+
}
|
|
106
|
+
lines.push(`${INDENT}..Default::default()`);
|
|
107
|
+
lines.push('}');
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Rust literal rendering
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/** Top-level value rendering (no `.into()` wrap — used for path params etc.). */
|
|
116
|
+
function renderValue(value: unknown): string {
|
|
117
|
+
if (value === null || value === undefined) return 'None';
|
|
118
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
119
|
+
if (typeof value === 'number') return String(value);
|
|
120
|
+
if (typeof value === 'string') return rustString(value);
|
|
121
|
+
if (Array.isArray(value)) return renderVec(value);
|
|
122
|
+
if (typeof value === 'object') return renderInlineObject(value as Record<string, unknown>);
|
|
123
|
+
return 'None';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Struct-field rendering: wraps strings with `.into()` so `String` fields accept &str. */
|
|
127
|
+
function renderStructValue(value: unknown): string {
|
|
128
|
+
if (typeof value === 'string') return `${rustString(value)}.into()`;
|
|
129
|
+
if (Array.isArray(value)) return renderVecStructValues(value);
|
|
130
|
+
return renderValue(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function rustString(s: string): string {
|
|
134
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderVec(items: unknown[]): string {
|
|
138
|
+
if (items.length === 0) return 'vec![]';
|
|
139
|
+
const rendered = items.map((v) => renderValue(v));
|
|
140
|
+
const oneline = `vec![${rendered.join(', ')}]`;
|
|
141
|
+
if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
|
|
142
|
+
const lines: string[] = ['vec!['];
|
|
143
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
144
|
+
const trailing = i < rendered.length - 1 ? ',' : ',';
|
|
145
|
+
lines.push(`${INDENT}${indentContinuationLines(rendered[i]!, INDENT)}${trailing}`);
|
|
146
|
+
}
|
|
147
|
+
lines.push(']');
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderVecStructValues(items: unknown[]): string {
|
|
152
|
+
if (items.length === 0) return 'vec![]';
|
|
153
|
+
const rendered = items.map((v) => renderStructValue(v));
|
|
154
|
+
const oneline = `vec![${rendered.join(', ')}]`;
|
|
155
|
+
if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
|
|
156
|
+
const lines: string[] = ['vec!['];
|
|
157
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
158
|
+
const trailing = i < rendered.length - 1 ? ',' : ',';
|
|
159
|
+
lines.push(`${INDENT}${indentContinuationLines(rendered[i]!, INDENT)}${trailing}`);
|
|
160
|
+
}
|
|
161
|
+
lines.push(']');
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Render a plain object as a generic `serde_json::json!({...})` literal. The
|
|
166
|
+
* snippet doesn't know the exact Rust struct corresponding to each nested
|
|
167
|
+
* object, so we fall back to a serde-friendly literal a developer can swap
|
|
168
|
+
* for the concrete struct (e.g. `OrganizationDomainData { ... }`). */
|
|
169
|
+
function renderInlineObject(obj: Record<string, unknown>): string {
|
|
170
|
+
const entries = Object.entries(obj);
|
|
171
|
+
if (entries.length === 0) return 'serde_json::json!({})';
|
|
172
|
+
const rendered = entries.map(([k, v]) => ({ key: k, value: renderValue(v) }));
|
|
173
|
+
const oneline = `serde_json::json!({ ${rendered.map((e) => `"${e.key}": ${e.value}`).join(', ')} })`;
|
|
174
|
+
if (oneline.length <= 80 && rendered.every((e) => !e.value.includes('\n'))) return oneline;
|
|
175
|
+
const lines: string[] = ['serde_json::json!({'];
|
|
176
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
177
|
+
const e = rendered[i]!;
|
|
178
|
+
const trailing = i < rendered.length - 1 ? ',' : ',';
|
|
179
|
+
lines.push(`${INDENT}"${e.key}": ${indentContinuationLines(e.value, INDENT)}${trailing}`);
|
|
180
|
+
}
|
|
181
|
+
lines.push('})');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function indentContinuationLines(s: string, indent: string): string {
|
|
186
|
+
if (!s.includes('\n')) return s;
|
|
187
|
+
const lines = s.split('\n');
|
|
188
|
+
return lines.map((line, i) => (i === 0 ? line : `${indent}${line}`)).join('\n');
|
|
189
|
+
}
|