@workos/oagen-emitters 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +13 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14549 -3385
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +328 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-dotnet.ts +45 -12
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/src/dotnet/client.ts +89 -0
- package/src/dotnet/enums.ts +323 -0
- package/src/dotnet/fixtures.ts +236 -0
- package/src/dotnet/index.ts +246 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +344 -0
- package/src/dotnet/naming.ts +330 -0
- package/src/dotnet/resources.ts +622 -0
- package/src/dotnet/tests.ts +693 -0
- package/src/dotnet/type-map.ts +201 -0
- package/src/dotnet/wrappers.ts +186 -0
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +84 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +179 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +4 -0
- package/src/kotlin/client.ts +53 -0
- package/src/kotlin/enums.ts +162 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +395 -0
- package/src/kotlin/naming.ts +223 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +667 -0
- package/src/kotlin/tests.ts +1019 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +128 -115
- package/src/node/enums.ts +9 -0
- package/src/node/errors.ts +37 -232
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +9 -1
- package/src/node/index.ts +3 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +527 -397
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +69 -19
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +13 -71
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +179 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +279 -0
- package/src/php/resources.ts +636 -0
- package/src/php/tests.ts +609 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +152 -0
- package/src/python/client.ts +345 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +189 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +472 -0
- package/src/shared/naming-utils.ts +154 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +70 -0
- package/test/dotnet/client.test.ts +121 -0
- package/test/dotnet/enums.test.ts +193 -0
- package/test/dotnet/errors.test.ts +9 -0
- package/test/dotnet/manifest.test.ts +82 -0
- package/test/dotnet/models.test.ts +260 -0
- package/test/dotnet/resources.test.ts +255 -0
- package/test/dotnet/tests.test.ts +202 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +92 -12
- package/test/node/enums.test.ts +2 -0
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +2 -0
- package/test/node/naming.test.ts +23 -0
- package/test/node/resources.test.ts +315 -84
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +95 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +682 -0
- package/test/php/tests.test.ts +185 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -746
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { TypeRef, PrimitiveType, UnionType } from '@workos/oagen';
|
|
2
|
+
import { mapTypeRef as irMapTypeRef } from '@workos/oagen';
|
|
3
|
+
import { className } from './naming.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map an IR TypeRef to a Go type string.
|
|
7
|
+
*/
|
|
8
|
+
export function mapTypeRef(ref: TypeRef, asPointer = false): string {
|
|
9
|
+
const base = irMapTypeRef<string>(ref, {
|
|
10
|
+
primitive: mapPrimitive,
|
|
11
|
+
array: (_ref, items) => `[]${items}`,
|
|
12
|
+
model: (r) => `*${className(r.name)}`,
|
|
13
|
+
enum: (r) => className(r.name),
|
|
14
|
+
union: (_r, variants) => joinUnionVariants(_r, variants),
|
|
15
|
+
nullable: (_ref, inner) => {
|
|
16
|
+
// If inner is already a pointer type (model), don't double-pointer
|
|
17
|
+
if (inner.startsWith('*')) return inner;
|
|
18
|
+
return `*${inner}`;
|
|
19
|
+
},
|
|
20
|
+
literal: (r) => {
|
|
21
|
+
if (r.value === null) return 'interface{}';
|
|
22
|
+
if (typeof r.value === 'string') return 'string';
|
|
23
|
+
if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'int' : 'float64';
|
|
24
|
+
if (typeof r.value === 'boolean') return 'bool';
|
|
25
|
+
return 'interface{}';
|
|
26
|
+
},
|
|
27
|
+
map: (_ref, value) => `map[string]${value}`,
|
|
28
|
+
});
|
|
29
|
+
if (asPointer && !base.startsWith('*') && !base.startsWith('[]') && !base.startsWith('map[')) {
|
|
30
|
+
return `*${base}`;
|
|
31
|
+
}
|
|
32
|
+
return base;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Map an IR TypeRef to a Go type string without pointer wrapping for models.
|
|
37
|
+
* Used for response type references where we don't want a double pointer.
|
|
38
|
+
*/
|
|
39
|
+
export function mapTypeRefValue(ref: TypeRef): string {
|
|
40
|
+
return irMapTypeRef<string>(ref, {
|
|
41
|
+
primitive: mapPrimitive,
|
|
42
|
+
array: (_ref, items) => `[]${items}`,
|
|
43
|
+
model: (r) => className(r.name),
|
|
44
|
+
enum: (r) => className(r.name),
|
|
45
|
+
union: (_r, variants) => joinUnionVariants(_r, variants),
|
|
46
|
+
nullable: (_ref, inner) => `*${inner}`,
|
|
47
|
+
literal: (r) => {
|
|
48
|
+
if (r.value === null) return 'interface{}';
|
|
49
|
+
if (typeof r.value === 'string') return 'string';
|
|
50
|
+
if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'int' : 'float64';
|
|
51
|
+
if (typeof r.value === 'boolean') return 'bool';
|
|
52
|
+
return 'interface{}';
|
|
53
|
+
},
|
|
54
|
+
map: (_ref, value) => `map[string]${value}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mapPrimitive(ref: PrimitiveType): string {
|
|
59
|
+
if (ref.format === 'binary') return '[]byte';
|
|
60
|
+
switch (ref.type) {
|
|
61
|
+
case 'string':
|
|
62
|
+
return 'string';
|
|
63
|
+
case 'integer':
|
|
64
|
+
return 'int';
|
|
65
|
+
case 'number':
|
|
66
|
+
return 'float64';
|
|
67
|
+
case 'boolean':
|
|
68
|
+
return 'bool';
|
|
69
|
+
case 'unknown':
|
|
70
|
+
return 'interface{}';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function joinUnionVariants(_ref: UnionType, variants: string[]): string {
|
|
75
|
+
if (_ref.compositionKind === 'allOf') {
|
|
76
|
+
return variants[0] ?? 'interface{}';
|
|
77
|
+
}
|
|
78
|
+
const unique = [...new Set(variants)];
|
|
79
|
+
if (unique.length === 1) return unique[0];
|
|
80
|
+
// Go doesn't have union types; use interface{}
|
|
81
|
+
return 'interface{}';
|
|
82
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
|
|
2
|
+
import {
|
|
3
|
+
className as goClassName,
|
|
4
|
+
fieldName as goFieldName,
|
|
5
|
+
methodName as goMethodName,
|
|
6
|
+
unexportedName,
|
|
7
|
+
} from './naming.js';
|
|
8
|
+
import { sortPathParamsByTemplateOrder } from './resources.js';
|
|
9
|
+
import { resolveWrapperParams, formatWrapperDescription, type ResolvedWrapperParam } from '../shared/wrapper-utils.js';
|
|
10
|
+
import { lowerFirstForDoc, fieldDocComment } from '../shared/naming-utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate Go wrapper method lines for union split operations.
|
|
14
|
+
*
|
|
15
|
+
* Each wrapper is a typed convenience method that:
|
|
16
|
+
* - Accepts only the exposed params (not the full union body)
|
|
17
|
+
* - Injects constant defaults (e.g., grant_type)
|
|
18
|
+
* - Reads inferred fields from client config (e.g., client_id)
|
|
19
|
+
* - Delegates to the HTTP client with the constructed body
|
|
20
|
+
*/
|
|
21
|
+
export function generateWrapperMethods(
|
|
22
|
+
serviceType: string,
|
|
23
|
+
resolvedOp: ResolvedOperation,
|
|
24
|
+
ctx: EmitterContext,
|
|
25
|
+
): string[] {
|
|
26
|
+
if (!resolvedOp.wrappers || resolvedOp.wrappers.length === 0) return [];
|
|
27
|
+
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (const wrapper of resolvedOp.wrappers) {
|
|
31
|
+
const wrapperParams = resolveWrapperParams(wrapper, ctx);
|
|
32
|
+
lines.push('');
|
|
33
|
+
emitWrapperParamsStruct(lines, wrapper, wrapperParams);
|
|
34
|
+
lines.push('');
|
|
35
|
+
emitWrapperMethod(lines, serviceType, resolvedOp, wrapper, wrapperParams);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function emitWrapperParamsStruct(
|
|
42
|
+
lines: string[],
|
|
43
|
+
wrapper: ResolvedWrapper,
|
|
44
|
+
wrapperParams: ResolvedWrapperParam[],
|
|
45
|
+
): void {
|
|
46
|
+
const structName = `${goMethodName(wrapper.name)}Params`;
|
|
47
|
+
|
|
48
|
+
lines.push(`// ${structName} contains the parameters for ${goMethodName(wrapper.name)}.`);
|
|
49
|
+
lines.push(`type ${structName} struct {`);
|
|
50
|
+
|
|
51
|
+
for (const { paramName, field, isOptional } of wrapperParams) {
|
|
52
|
+
const goField = goFieldName(paramName);
|
|
53
|
+
const goType = field ? resolveSimpleGoType(field.type) : 'string';
|
|
54
|
+
|
|
55
|
+
if (field?.description) {
|
|
56
|
+
const fdLines = field.description.split('\n').filter((l: string) => l.trim());
|
|
57
|
+
lines.push(`\t// ${fieldDocComment(goField, fdLines[0])}`);
|
|
58
|
+
for (let i = 1; i < fdLines.length; i++) {
|
|
59
|
+
lines.push(`\t// ${fdLines[i].trim()}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (isOptional) {
|
|
63
|
+
const optType = goType.startsWith('*') || goType.startsWith('[]') ? goType : `*${goType}`;
|
|
64
|
+
lines.push(`\t${goField} ${optType} \`json:"${paramName},omitempty"\``);
|
|
65
|
+
} else {
|
|
66
|
+
lines.push(`\t${goField} ${goType} \`json:"${paramName}"\``);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lines.push('}');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function emitWrapperMethod(
|
|
74
|
+
lines: string[],
|
|
75
|
+
serviceType: string,
|
|
76
|
+
resolvedOp: ResolvedOperation,
|
|
77
|
+
wrapper: ResolvedWrapper,
|
|
78
|
+
wrapperParams: ResolvedWrapperParam[],
|
|
79
|
+
): void {
|
|
80
|
+
const op = resolvedOp.operation;
|
|
81
|
+
const method = goMethodName(wrapper.name);
|
|
82
|
+
const paramsStruct = `${method}Params`;
|
|
83
|
+
|
|
84
|
+
// Return type
|
|
85
|
+
const responseType = wrapper.responseModelName ? goClassName(wrapper.responseModelName) : null;
|
|
86
|
+
|
|
87
|
+
// GoDoc
|
|
88
|
+
lines.push(`// ${method} ${formatWrapperDescription(wrapper.name)}.`);
|
|
89
|
+
|
|
90
|
+
// Signature
|
|
91
|
+
const sigParams: string[] = ['ctx context.Context'];
|
|
92
|
+
|
|
93
|
+
// Path params as positional args (sorted by template order)
|
|
94
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
95
|
+
sigParams.push(`${lowerFirstSafe(goFieldName(p.name))} ${resolveSimpleGoType(p.type)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
sigParams.push(`params *${paramsStruct}`);
|
|
99
|
+
sigParams.push('opts ...RequestOption');
|
|
100
|
+
|
|
101
|
+
if (responseType) {
|
|
102
|
+
lines.push(`func (s *${serviceType}) ${method}(${sigParams.join(', ')}) (*${responseType}, error) {`);
|
|
103
|
+
} else {
|
|
104
|
+
lines.push(`func (s *${serviceType}) ${method}(${sigParams.join(', ')}) error {`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build body map with defaults + exposed params
|
|
108
|
+
lines.push('\tbody := map[string]interface{}{');
|
|
109
|
+
|
|
110
|
+
// Constant defaults (e.g., grant_type)
|
|
111
|
+
for (const [key, value] of Object.entries(wrapper.defaults)) {
|
|
112
|
+
lines.push(`\t\t"${key}": ${goLiteral(value)},`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Required exposed params
|
|
116
|
+
for (const { paramName, isOptional } of wrapperParams) {
|
|
117
|
+
if (isOptional) continue;
|
|
118
|
+
const goField = goFieldName(paramName);
|
|
119
|
+
lines.push(`\t\t"${paramName}": params.${goField},`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push('\t}');
|
|
123
|
+
|
|
124
|
+
// Inferred fields from client config
|
|
125
|
+
for (const field of wrapper.inferFromClient) {
|
|
126
|
+
const expr = clientFieldExpression(field);
|
|
127
|
+
lines.push(`\tif ${expr} != "" {`);
|
|
128
|
+
lines.push(`\t\tbody["${field}"] = ${expr}`);
|
|
129
|
+
lines.push('\t}');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Optional exposed params
|
|
133
|
+
for (const { paramName, isOptional } of wrapperParams) {
|
|
134
|
+
if (!isOptional) continue;
|
|
135
|
+
const goField = goFieldName(paramName);
|
|
136
|
+
lines.push(`\tif params.${goField} != nil {`);
|
|
137
|
+
lines.push(`\t\tbody["${paramName}"] = *params.${goField}`);
|
|
138
|
+
lines.push('\t}');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build path expression
|
|
142
|
+
let pathExpr: string;
|
|
143
|
+
if (op.pathParams.length > 0) {
|
|
144
|
+
let fmtStr = op.path;
|
|
145
|
+
const fmtArgs: string[] = [];
|
|
146
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
147
|
+
fmtStr = fmtStr.replace(`{${p.name}}`, '%s');
|
|
148
|
+
fmtArgs.push(lowerFirstSafe(goFieldName(p.name)));
|
|
149
|
+
}
|
|
150
|
+
pathExpr = `fmt.Sprintf("${fmtStr}", ${fmtArgs.join(', ')})`;
|
|
151
|
+
} else {
|
|
152
|
+
pathExpr = `"${op.path}"`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Make the request
|
|
156
|
+
if (responseType) {
|
|
157
|
+
lines.push(`\tvar result ${responseType}`);
|
|
158
|
+
lines.push(
|
|
159
|
+
`\t_, err := s.client.request(ctx, "${op.httpMethod.toUpperCase()}", ${pathExpr}, nil, body, &result, opts)`,
|
|
160
|
+
);
|
|
161
|
+
lines.push('\tif err != nil {');
|
|
162
|
+
lines.push('\t\treturn nil, err');
|
|
163
|
+
lines.push('\t}');
|
|
164
|
+
lines.push('\treturn &result, nil');
|
|
165
|
+
} else {
|
|
166
|
+
lines.push(
|
|
167
|
+
`\t_, err := s.client.request(ctx, "${op.httpMethod.toUpperCase()}", ${pathExpr}, nil, body, nil, opts)`,
|
|
168
|
+
);
|
|
169
|
+
lines.push('\treturn err');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.push('}');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Convert a value to a Go literal. */
|
|
176
|
+
function goLiteral(value: string | number | boolean): string {
|
|
177
|
+
if (typeof value === 'string') return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
178
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
179
|
+
return String(value);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Get the Go expression for reading a client config field. */
|
|
183
|
+
function clientFieldExpression(field: string): string {
|
|
184
|
+
switch (field) {
|
|
185
|
+
case 'client_id':
|
|
186
|
+
return 's.client.clientID';
|
|
187
|
+
case 'client_secret':
|
|
188
|
+
return 's.client.apiKey';
|
|
189
|
+
default:
|
|
190
|
+
return `s.client.${lowerFirstSafe(goFieldName(field))}`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Resolve a TypeRef to a simple Go type string. */
|
|
195
|
+
function resolveSimpleGoType(ref: any): string {
|
|
196
|
+
if (ref.kind === 'primitive') {
|
|
197
|
+
switch (ref.type) {
|
|
198
|
+
case 'string':
|
|
199
|
+
return 'string';
|
|
200
|
+
case 'integer':
|
|
201
|
+
return 'int';
|
|
202
|
+
case 'number':
|
|
203
|
+
return 'float64';
|
|
204
|
+
case 'boolean':
|
|
205
|
+
return 'bool';
|
|
206
|
+
default:
|
|
207
|
+
return 'interface{}';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (ref.kind === 'nullable') return `*${resolveSimpleGoType(ref.inner)}`;
|
|
211
|
+
if (ref.kind === 'array') return `[]${resolveSimpleGoType(ref.items)}`;
|
|
212
|
+
if (ref.kind === 'model') return `*${goClassName(ref.name)}`;
|
|
213
|
+
if (ref.kind === 'enum') return goClassName(ref.name);
|
|
214
|
+
if (ref.kind === 'union') {
|
|
215
|
+
// For oneOf with a single non-null variant, use that variant's type
|
|
216
|
+
const nonNull = ref.variants.filter((v: any) => v.kind !== 'literal' || v.value !== null);
|
|
217
|
+
if (nonNull.length === 1) return resolveSimpleGoType(nonNull[0]);
|
|
218
|
+
return 'interface{}';
|
|
219
|
+
}
|
|
220
|
+
return 'interface{}';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Go reserved words set. */
|
|
224
|
+
const GO_RESERVED = new Set([
|
|
225
|
+
'break',
|
|
226
|
+
'case',
|
|
227
|
+
'chan',
|
|
228
|
+
'const',
|
|
229
|
+
'continue',
|
|
230
|
+
'default',
|
|
231
|
+
'defer',
|
|
232
|
+
'else',
|
|
233
|
+
'fallthrough',
|
|
234
|
+
'for',
|
|
235
|
+
'func',
|
|
236
|
+
'go',
|
|
237
|
+
'goto',
|
|
238
|
+
'if',
|
|
239
|
+
'import',
|
|
240
|
+
'interface',
|
|
241
|
+
'map',
|
|
242
|
+
'package',
|
|
243
|
+
'range',
|
|
244
|
+
'return',
|
|
245
|
+
'select',
|
|
246
|
+
'struct',
|
|
247
|
+
'switch',
|
|
248
|
+
'type',
|
|
249
|
+
'var',
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
function lowerFirstSafe(s: string): string {
|
|
253
|
+
if (!s) return s;
|
|
254
|
+
const result = unexportedName(s);
|
|
255
|
+
if (GO_RESERVED.has(result)) return `${result}Param`;
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function _lowerFirstField(s: string): string {
|
|
260
|
+
return lowerFirstForDoc(s);
|
|
261
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
|
+
import { apiClassName, packageSegment, servicePropertyName } from './naming.js';
|
|
3
|
+
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
4
|
+
|
|
5
|
+
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate service accessor properties for the hand-maintained `WorkOS` class.
|
|
9
|
+
*
|
|
10
|
+
* Each accessor is a `val` property with a custom getter that delegates to
|
|
11
|
+
* `WorkOS.service(...)` for lazy, cached construction. The generated file
|
|
12
|
+
* contains a `WorkOS` class stub with only these properties — the oagen
|
|
13
|
+
* merger deep-merges them into the existing hand-written `WorkOS.kt`.
|
|
14
|
+
*
|
|
15
|
+
* Accessors use fully-qualified type names so the merger doesn't need to
|
|
16
|
+
* inject imports into the hand-written file.
|
|
17
|
+
*/
|
|
18
|
+
export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
19
|
+
const targets = deduplicateByMount(spec.services, ctx);
|
|
20
|
+
if (targets.length === 0) return [];
|
|
21
|
+
|
|
22
|
+
const accessorLines: string[] = [];
|
|
23
|
+
for (const mount of targets) {
|
|
24
|
+
const apiCls = apiClassName(mount);
|
|
25
|
+
const fqn = `com.workos.${packageSegment(mount)}.${apiCls}`;
|
|
26
|
+
const prop = servicePropertyName(mount);
|
|
27
|
+
accessorLines.push('');
|
|
28
|
+
accessorLines.push(` /** Lazily-constructed [${apiCls}] accessor for this [WorkOS] client. */`);
|
|
29
|
+
accessorLines.push(` val ${prop}: ${fqn}`);
|
|
30
|
+
accessorLines.push(` get() = service(${fqn}::class) { ${fqn}(this) }`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
lines.push('package com.workos');
|
|
35
|
+
lines.push('');
|
|
36
|
+
lines.push('open class WorkOS {');
|
|
37
|
+
for (const line of accessorLines) lines.push(line);
|
|
38
|
+
lines.push('}');
|
|
39
|
+
lines.push('');
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
path: `${KOTLIN_SRC_PREFIX}com/workos/WorkOS.kt`,
|
|
44
|
+
content: lines.join('\n'),
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function deduplicateByMount(services: Service[], ctx: EmitterContext): string[] {
|
|
50
|
+
const targets = new Set<string>();
|
|
51
|
+
for (const s of services) targets.add(getMountTarget(s, ctx));
|
|
52
|
+
return [...targets].sort();
|
|
53
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { className, ktStringLiteral } from './naming.js';
|
|
3
|
+
|
|
4
|
+
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
5
|
+
const ENUMS_PACKAGE = 'com.workos.types';
|
|
6
|
+
const ENUMS_DIR = 'com/workos/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mapping from an IR enum name to its canonical enum name. When two enums
|
|
10
|
+
* share identical sorted wire values the shorter-named one is canonical and
|
|
11
|
+
* the others become `typealias` files. Downstream consumers (type-map,
|
|
12
|
+
* resources) use this map to resolve references to the canonical class.
|
|
13
|
+
*/
|
|
14
|
+
export const enumCanonicalMap = new Map<string, string>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate Kotlin `enum class` types from the IR enums. Each enum is emitted
|
|
18
|
+
* to its own file under `com.workos.types`, annotated with Jackson
|
|
19
|
+
* `@JsonValue` on the wire value. An `Unknown` sentinel is always the first
|
|
20
|
+
* constant so that responses with new variants still deserialize instead of
|
|
21
|
+
* throwing.
|
|
22
|
+
*
|
|
23
|
+
* Enums with identical sets of wire values are deduplicated: the one with the
|
|
24
|
+
* shortest PascalCase name becomes canonical and the rest emit `typealias`
|
|
25
|
+
* files pointing at the canonical class.
|
|
26
|
+
*/
|
|
27
|
+
export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFile[] {
|
|
28
|
+
if (enums.length === 0) return [];
|
|
29
|
+
|
|
30
|
+
// Reset the canonical map on every generation run (guards against re-entry).
|
|
31
|
+
enumCanonicalMap.clear();
|
|
32
|
+
|
|
33
|
+
// --- Dedup: group enums by a hash of their sorted wire values. ---
|
|
34
|
+
const hashGroups = new Map<string, Enum[]>();
|
|
35
|
+
for (const enumDef of enums) {
|
|
36
|
+
if (enumDef.values.length === 0) continue;
|
|
37
|
+
const hash = enumWireHash(enumDef);
|
|
38
|
+
if (!hashGroups.has(hash)) hashGroups.set(hash, []);
|
|
39
|
+
hashGroups.get(hash)!.push(enumDef);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Within each group, pick the shortest className as canonical.
|
|
43
|
+
const aliasOf = new Map<string, string>(); // enum name → canonical enum name
|
|
44
|
+
for (const [, group] of hashGroups) {
|
|
45
|
+
if (group.length <= 1) continue;
|
|
46
|
+
const sorted = [...group].sort(
|
|
47
|
+
(a, b) =>
|
|
48
|
+
className(a.name).length - className(b.name).length || className(a.name).localeCompare(className(b.name)),
|
|
49
|
+
);
|
|
50
|
+
const canonical = sorted[0];
|
|
51
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
52
|
+
aliasOf.set(sorted[i].name, canonical.name);
|
|
53
|
+
enumCanonicalMap.set(sorted[i].name, canonical.name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files: GeneratedFile[] = [];
|
|
58
|
+
|
|
59
|
+
for (const enumDef of enums) {
|
|
60
|
+
if (enumDef.values.length === 0) continue;
|
|
61
|
+
|
|
62
|
+
const typeName = className(enumDef.name);
|
|
63
|
+
|
|
64
|
+
// Non-canonical enum: emit a typealias instead of a full enum class.
|
|
65
|
+
const canonicalName = aliasOf.get(enumDef.name);
|
|
66
|
+
if (canonicalName) {
|
|
67
|
+
const canonicalType = className(canonicalName);
|
|
68
|
+
const aliasLine = `typealias ${typeName} = ${canonicalType}`;
|
|
69
|
+
// ktlint enforces a 140-char max line length. When the typealias
|
|
70
|
+
// exceeds that, add a @file:Suppress to avoid an unfixable violation.
|
|
71
|
+
const suppressLine = aliasLine.length > 140 ? `@file:Suppress("ktlint:standard:max-line-length")\n\n` : '';
|
|
72
|
+
const aliasContent = [`${suppressLine}package ${ENUMS_PACKAGE}`, '', aliasLine, ''].join('\n');
|
|
73
|
+
files.push({
|
|
74
|
+
path: `${KOTLIN_SRC_PREFIX}${ENUMS_DIR}/${typeName}.kt`,
|
|
75
|
+
content: aliasContent,
|
|
76
|
+
overwriteExisting: true,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
lines.push(`package ${ENUMS_PACKAGE}`);
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('import com.fasterxml.jackson.annotation.JsonEnumDefaultValue');
|
|
85
|
+
lines.push('import com.fasterxml.jackson.annotation.JsonValue');
|
|
86
|
+
lines.push('');
|
|
87
|
+
// Replace the tautological "Foo enum." docstring with a slightly more
|
|
88
|
+
// informative summary. `Unknown` is emitted as the forward-compatibility
|
|
89
|
+
// sentinel for values the server introduces after this SDK was built.
|
|
90
|
+
lines.push(`/** Enumeration of valid ${typeName} values returned or accepted by the API. */`);
|
|
91
|
+
lines.push(`enum class ${typeName}(`);
|
|
92
|
+
lines.push(' @JsonValue val value: String');
|
|
93
|
+
lines.push(') {');
|
|
94
|
+
// `@JsonEnumDefaultValue` makes Jackson's
|
|
95
|
+
// READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE feature map unrecognized
|
|
96
|
+
// wire values onto `Unknown` instead of throwing — required for forward
|
|
97
|
+
// compatibility when the API introduces new variants.
|
|
98
|
+
lines.push(' @JsonEnumDefaultValue');
|
|
99
|
+
lines.push(` Unknown(${ktStringLiteral('unknown')}),`);
|
|
100
|
+
|
|
101
|
+
const seenNames = new Set<string>(['Unknown']);
|
|
102
|
+
const seenWire = new Set<string>(['unknown']);
|
|
103
|
+
const members: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const v of enumDef.values) {
|
|
106
|
+
const wire = String(v.value);
|
|
107
|
+
if (seenWire.has(wire)) continue;
|
|
108
|
+
seenWire.add(wire);
|
|
109
|
+
|
|
110
|
+
let memberName = className(wire);
|
|
111
|
+
if (!memberName || /^[0-9]/.test(memberName)) memberName = `Value${memberName || wire}`;
|
|
112
|
+
if (memberName === typeName || seenNames.has(memberName)) {
|
|
113
|
+
let suffix = 2;
|
|
114
|
+
while (seenNames.has(`${memberName}${suffix}`)) suffix += 1;
|
|
115
|
+
memberName = `${memberName}${suffix}`;
|
|
116
|
+
}
|
|
117
|
+
seenNames.add(memberName);
|
|
118
|
+
|
|
119
|
+
if (v.description?.trim()) {
|
|
120
|
+
members.push(` /** ${escapeKdoc(v.description.split('\n')[0].trim())} */`);
|
|
121
|
+
}
|
|
122
|
+
if (v.deprecated) {
|
|
123
|
+
members.push(' @Deprecated("Deprecated enum value")');
|
|
124
|
+
}
|
|
125
|
+
members.push(` ${memberName}(${ktStringLiteral(wire)})`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < members.length; i++) {
|
|
129
|
+
const isLast = i === members.length - 1;
|
|
130
|
+
const line = members[i];
|
|
131
|
+
const trimmedStart = line.trimStart();
|
|
132
|
+
if (trimmedStart.startsWith('/**') || trimmedStart.startsWith('@')) {
|
|
133
|
+
lines.push(line);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
lines.push(isLast ? line : `${line},`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lines.push('}');
|
|
140
|
+
lines.push('');
|
|
141
|
+
|
|
142
|
+
files.push({
|
|
143
|
+
path: `${KOTLIN_SRC_PREFIX}${ENUMS_DIR}/${typeName}.kt`,
|
|
144
|
+
content: lines.join('\n'),
|
|
145
|
+
overwriteExisting: true,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Hash an enum by its sorted wire values so identical enums collide. */
|
|
153
|
+
function enumWireHash(enumDef: Enum): string {
|
|
154
|
+
return [...enumDef.values]
|
|
155
|
+
.map((v) => String(v.value))
|
|
156
|
+
.sort()
|
|
157
|
+
.join('|');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function escapeKdoc(s: string): string {
|
|
161
|
+
return s.replace(/\*\//g, '*\u200b/');
|
|
162
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { generateModels } from './models.js';
|
|
15
|
+
import { generateEnums } from './enums.js';
|
|
16
|
+
import { generateResources } from './resources.js';
|
|
17
|
+
import { generateClient } from './client.js';
|
|
18
|
+
import { generateTests } from './tests.js';
|
|
19
|
+
import { generateManifest } from './manifest.js';
|
|
20
|
+
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
21
|
+
|
|
22
|
+
/** Ensure every generated file ends with a trailing newline. */
|
|
23
|
+
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
24
|
+
for (const f of files) {
|
|
25
|
+
if (f.content && !f.content.endsWith('\n')) {
|
|
26
|
+
f.content += '\n';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return files;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const kotlinEmitter: Emitter = {
|
|
33
|
+
language: 'kotlin',
|
|
34
|
+
|
|
35
|
+
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
36
|
+
const enriched = enrichModelsFromSpec(models);
|
|
37
|
+
return ensureTrailingNewlines(generateModels(enriched, ctx));
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
41
|
+
// Merge synthetic enums produced during model enrichment (inline oneOf
|
|
42
|
+
// definitions) so they get proper enum class files.
|
|
43
|
+
const syntheticEnums = getSyntheticEnums();
|
|
44
|
+
return ensureTrailingNewlines(generateEnums([...enums, ...syntheticEnums], ctx));
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
48
|
+
return ensureTrailingNewlines(generateResources(services, ctx));
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
52
|
+
return ensureTrailingNewlines(generateClient(spec, ctx));
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
generateErrors(_ctx: EmitterContext): GeneratedFile[] {
|
|
56
|
+
// Exception hierarchy is hand-maintained in workos-kotlin (see Phase 2).
|
|
57
|
+
return [];
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
generateTypeSignatures(): GeneratedFile[] {
|
|
61
|
+
return [];
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
65
|
+
// Pass enriched models so round-trip tests see the full field set
|
|
66
|
+
// (including optional oneOf-enriched fields) and can filter accurately.
|
|
67
|
+
const enrichedModels = enrichModelsFromSpec(spec.models);
|
|
68
|
+
const enrichedSpec: ApiSpec = { ...spec, models: enrichedModels };
|
|
69
|
+
return ensureTrailingNewlines(generateTests(enrichedSpec, { ...ctx, spec: enrichedSpec }));
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
73
|
+
return ensureTrailingNewlines(generateManifest(spec, ctx));
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
fileHeader(): string {
|
|
77
|
+
return '// This file is auto-generated by oagen. Do not edit.';
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
formatCommand(targetDir: string): FormatCommand | null {
|
|
81
|
+
// ktlint enforces a 140-char max line length that cannot be auto-corrected
|
|
82
|
+
// by the Gradle plugin alone, but `./gradlew ktlintFormat` fixes most
|
|
83
|
+
// violations (trailing whitespace, import ordering, etc.) across all
|
|
84
|
+
// generated files. The file list appended by oagen is ignored — Gradle
|
|
85
|
+
// reformats the whole source set.
|
|
86
|
+
if (!fs.existsSync(path.join(targetDir, 'gradlew'))) return null;
|
|
87
|
+
return {
|
|
88
|
+
cmd: 'bash',
|
|
89
|
+
args: ['-c', `cd ${JSON.stringify(targetDir)} && ./gradlew ktlintFormat --quiet 2>/dev/null; true`, '--'],
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
};
|