@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
package/src/go/enums.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Enum, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
|
|
2
|
+
import { walkTypeRef } from '@workos/oagen';
|
|
3
|
+
import { className } from './naming.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate Go typed string enum constants from IR Enum definitions.
|
|
7
|
+
*
|
|
8
|
+
* Each enum becomes a named string type + const block:
|
|
9
|
+
* type Status string
|
|
10
|
+
* const (
|
|
11
|
+
* StatusActive Status = "active"
|
|
12
|
+
* StatusInactive Status = "inactive"
|
|
13
|
+
* )
|
|
14
|
+
*/
|
|
15
|
+
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
16
|
+
if (enums.length === 0) return [];
|
|
17
|
+
|
|
18
|
+
const aliasOf = collectEnumAliasOf(enums);
|
|
19
|
+
const files: GeneratedFile[] = [];
|
|
20
|
+
|
|
21
|
+
// Group all enums into a single file per SDK
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
lines.push(`package ${ctx.namespace}`);
|
|
24
|
+
lines.push('');
|
|
25
|
+
|
|
26
|
+
for (const enumDef of enums) {
|
|
27
|
+
// If this enum is an alias, emit a simple type alias
|
|
28
|
+
const canonicalName = aliasOf.get(enumDef.name);
|
|
29
|
+
if (canonicalName) {
|
|
30
|
+
const aliasType = className(enumDef.name);
|
|
31
|
+
const canonicalType = className(canonicalName);
|
|
32
|
+
lines.push(`// ${aliasType} is an alias for ${canonicalType}.`);
|
|
33
|
+
lines.push(`type ${aliasType} = ${canonicalType}`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const typeName = className(enumDef.name);
|
|
39
|
+
|
|
40
|
+
if (enumDef.values.length === 0) {
|
|
41
|
+
const humanized = humanize(enumDef.name);
|
|
42
|
+
lines.push(`// ${typeName} represents ${humanized} values.`);
|
|
43
|
+
lines.push(`type ${typeName} = string`);
|
|
44
|
+
lines.push('');
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Deduplicate values
|
|
49
|
+
const seenValues = new Set<string>();
|
|
50
|
+
const uniqueValues: typeof enumDef.values = [];
|
|
51
|
+
for (const v of enumDef.values) {
|
|
52
|
+
const vs = String(v.value);
|
|
53
|
+
if (!seenValues.has(vs)) {
|
|
54
|
+
seenValues.add(vs);
|
|
55
|
+
uniqueValues.push(v);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const humanized = humanize(enumDef.name);
|
|
60
|
+
lines.push(`// ${typeName} represents ${humanized} values.`);
|
|
61
|
+
lines.push(`type ${typeName} string`);
|
|
62
|
+
lines.push('');
|
|
63
|
+
lines.push('const (');
|
|
64
|
+
|
|
65
|
+
const usedNames = new Set<string>();
|
|
66
|
+
for (const v of uniqueValues) {
|
|
67
|
+
let constSuffix = className(String(v.value));
|
|
68
|
+
// Avoid collision with the type itself
|
|
69
|
+
if (usedNames.has(`${typeName}${constSuffix}`)) {
|
|
70
|
+
let suffix = 2;
|
|
71
|
+
while (usedNames.has(`${typeName}${constSuffix}${suffix}`)) suffix++;
|
|
72
|
+
constSuffix = `${constSuffix}${suffix}`;
|
|
73
|
+
}
|
|
74
|
+
const constName = `${typeName}${constSuffix}`;
|
|
75
|
+
usedNames.add(constName);
|
|
76
|
+
const valueStr = typeof v.value === 'string' ? `"${v.value}"` : String(v.value);
|
|
77
|
+
if (v.description) {
|
|
78
|
+
lines.push(`\t// ${constName} is ${v.description}.`);
|
|
79
|
+
}
|
|
80
|
+
if (v.deprecated) {
|
|
81
|
+
if (v.description) lines.push(`\t//`);
|
|
82
|
+
lines.push(`\t// Deprecated: this value is deprecated.`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(`\t${constName} ${typeName} = ${valueStr}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push(')');
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
files.push({
|
|
91
|
+
path: 'enums.go',
|
|
92
|
+
content: lines.join('\n'),
|
|
93
|
+
overwriteExisting: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Known acronyms to preserve as single tokens during humanization. */
|
|
100
|
+
const HUMANIZE_ACRONYMS: [RegExp, string][] = [
|
|
101
|
+
[/OAuth/g, 'OAUTH_ACRN'],
|
|
102
|
+
[/URN/g, 'URN_ACRN'],
|
|
103
|
+
[/IETF/g, 'IETF_ACRN'],
|
|
104
|
+
[/API/g, 'API_ACRN'],
|
|
105
|
+
[/SSO/g, 'SSO_ACRN'],
|
|
106
|
+
[/PKCE/g, 'PKCE_ACRN'],
|
|
107
|
+
[/JWT/g, 'JWT_ACRN'],
|
|
108
|
+
[/MFA/g, 'MFA_ACRN'],
|
|
109
|
+
[/TOTP/g, 'TOTP_ACRN'],
|
|
110
|
+
[/SAML/g, 'SAML_ACRN'],
|
|
111
|
+
[/SCIM/g, 'SCIM_ACRN'],
|
|
112
|
+
[/OIDC/g, 'OIDC_ACRN'],
|
|
113
|
+
[/CORS/g, 'CORS_ACRN'],
|
|
114
|
+
[/RBAC/g, 'RBAC_ACRN'],
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const HUMANIZE_RESTORE: [RegExp, string][] = [
|
|
118
|
+
[/oauth_acrn/g, 'OAuth'],
|
|
119
|
+
[/urn_acrn/g, 'URN'],
|
|
120
|
+
[/ietf_acrn/g, 'IETF'],
|
|
121
|
+
[/api_acrn/g, 'API'],
|
|
122
|
+
[/sso_acrn/g, 'SSO'],
|
|
123
|
+
[/pkce_acrn/g, 'PKCE'],
|
|
124
|
+
[/jwt_acrn/g, 'JWT'],
|
|
125
|
+
[/mfa_acrn/g, 'MFA'],
|
|
126
|
+
[/totp_acrn/g, 'TOTP'],
|
|
127
|
+
[/saml_acrn/g, 'SAML'],
|
|
128
|
+
[/scim_acrn/g, 'SCIM'],
|
|
129
|
+
[/oidc_acrn/g, 'OIDC'],
|
|
130
|
+
[/cors_acrn/g, 'CORS'],
|
|
131
|
+
[/rbac_acrn/g, 'RBAC'],
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
function humanize(name: string): string {
|
|
135
|
+
// Replace known acronyms with placeholders before splitting
|
|
136
|
+
let s = name;
|
|
137
|
+
for (const [pattern, replacement] of HUMANIZE_ACRONYMS) {
|
|
138
|
+
s = s.replace(pattern, replacement);
|
|
139
|
+
}
|
|
140
|
+
let result = s.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
141
|
+
result = result.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
142
|
+
result = result.toLowerCase();
|
|
143
|
+
for (const [pattern, replacement] of HUMANIZE_RESTORE) {
|
|
144
|
+
result = result.replace(pattern, replacement);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectEnumAliasOf(enums: Enum[]): Map<string, string> {
|
|
150
|
+
const hashGroups = new Map<string, string[]>();
|
|
151
|
+
for (const enumDef of enums) {
|
|
152
|
+
const hash = [...enumDef.values]
|
|
153
|
+
.map((v) => String(v.value))
|
|
154
|
+
.sort()
|
|
155
|
+
.join('|');
|
|
156
|
+
if (!hashGroups.has(hash)) hashGroups.set(hash, []);
|
|
157
|
+
hashGroups.get(hash)!.push(enumDef.name);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const aliasOf = new Map<string, string>();
|
|
161
|
+
for (const [, names] of hashGroups) {
|
|
162
|
+
if (names.length <= 1) continue;
|
|
163
|
+
const sorted = [...names].sort();
|
|
164
|
+
const canonical = sorted[0];
|
|
165
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
166
|
+
aliasOf.set(sorted[i], canonical);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return aliasOf;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function assignEnumsToServices(enums: Enum[], services: Service[]): Map<string, string> {
|
|
173
|
+
const enumToService = new Map<string, string>();
|
|
174
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
175
|
+
|
|
176
|
+
for (const service of services) {
|
|
177
|
+
for (const op of service.operations) {
|
|
178
|
+
const refs = new Set<string>();
|
|
179
|
+
const collect = (ref: any) => {
|
|
180
|
+
walkTypeRef(ref, { enum: (r: any) => refs.add(r.name) });
|
|
181
|
+
};
|
|
182
|
+
if (op.requestBody) collect(op.requestBody);
|
|
183
|
+
collect(op.response);
|
|
184
|
+
for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams, ...(op.cookieParams ?? [])]) {
|
|
185
|
+
collect(p.type);
|
|
186
|
+
}
|
|
187
|
+
for (const name of refs) {
|
|
188
|
+
if (enumNames.has(name) && !enumToService.has(name)) {
|
|
189
|
+
enumToService.set(name, service.name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return enumToService;
|
|
196
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Model, TypeRef, Enum } from '@workos/oagen';
|
|
2
|
+
import { fileName, fieldName } from './naming.js';
|
|
3
|
+
import { isListMetadataModel, isListWrapperModel } from './models.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prefix mapping for generating realistic ID fixture values.
|
|
7
|
+
*/
|
|
8
|
+
export const ID_PREFIXES: Record<string, string> = {
|
|
9
|
+
Connection: 'conn_',
|
|
10
|
+
Organization: 'org_',
|
|
11
|
+
OrganizationMembership: 'om_',
|
|
12
|
+
User: 'user_',
|
|
13
|
+
Directory: 'directory_',
|
|
14
|
+
DirectoryGroup: 'dir_grp_',
|
|
15
|
+
DirectoryUser: 'dir_usr_',
|
|
16
|
+
Invitation: 'inv_',
|
|
17
|
+
Session: 'session_',
|
|
18
|
+
AuthenticationFactor: 'auth_factor_',
|
|
19
|
+
EmailVerification: 'email_verification_',
|
|
20
|
+
MagicAuth: 'magic_auth_',
|
|
21
|
+
PasswordReset: 'password_reset_',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate JSON fixture files for test data.
|
|
26
|
+
*/
|
|
27
|
+
export function generateFixtures(spec: {
|
|
28
|
+
models: Model[];
|
|
29
|
+
enums: Enum[];
|
|
30
|
+
services: any[];
|
|
31
|
+
}): { path: string; content: string }[] {
|
|
32
|
+
if (spec.models.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const modelMap = new Map(spec.models.map((m) => [m.name, m]));
|
|
35
|
+
const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
|
|
36
|
+
const files: { path: string; content: string }[] = [];
|
|
37
|
+
|
|
38
|
+
for (const model of spec.models) {
|
|
39
|
+
if (isListMetadataModel(model)) continue;
|
|
40
|
+
if (isListWrapperModel(model)) continue;
|
|
41
|
+
|
|
42
|
+
const fixture = model.fields.length === 0 ? {} : generateModelFixture(model, modelMap, enumMap);
|
|
43
|
+
|
|
44
|
+
files.push({
|
|
45
|
+
path: `testdata/${fileName(model.name)}.json`,
|
|
46
|
+
content: JSON.stringify(fixture, null, 2),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate list fixtures for paginated responses
|
|
51
|
+
for (const service of spec.services) {
|
|
52
|
+
for (const op of service.operations) {
|
|
53
|
+
if (op.pagination) {
|
|
54
|
+
let itemModel = op.pagination.itemType.kind === 'model' ? modelMap.get(op.pagination.itemType.name) : null;
|
|
55
|
+
if (itemModel) {
|
|
56
|
+
const unwrapped = unwrapListModel(itemModel, modelMap);
|
|
57
|
+
if (unwrapped) itemModel = unwrapped;
|
|
58
|
+
if (itemModel.fields.length === 0) continue;
|
|
59
|
+
const fixture = generateModelFixture(itemModel, modelMap, enumMap);
|
|
60
|
+
const listFixture = {
|
|
61
|
+
data: [fixture],
|
|
62
|
+
list_metadata: {
|
|
63
|
+
before: null,
|
|
64
|
+
after: null,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
files.push({
|
|
68
|
+
path: `testdata/list_${fileName(itemModel.name)}.json`,
|
|
69
|
+
content: JSON.stringify(listFixture, null, 2),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Deduplicate fixtures with identical content.
|
|
77
|
+
// When multiple fixtures have the same content, emit one shared file and
|
|
78
|
+
// rewrite the others as references to the shared path.
|
|
79
|
+
const contentGroups = new Map<string, string[]>();
|
|
80
|
+
for (const f of files) {
|
|
81
|
+
if (!contentGroups.has(f.content)) contentGroups.set(f.content, []);
|
|
82
|
+
contentGroups.get(f.content)!.push(f.path);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pathRewrites = new Map<string, string>();
|
|
86
|
+
for (const [_content, paths] of contentGroups) {
|
|
87
|
+
if (paths.length < 3) continue; // only dedup when 3+ files are identical
|
|
88
|
+
// Use the shortest path as the canonical shared fixture
|
|
89
|
+
const sorted = [...paths].sort((a, b) => a.length - b.length);
|
|
90
|
+
const canonical = sorted[0];
|
|
91
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
92
|
+
pathRewrites.set(sorted[i], canonical);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Remove duplicate files (they'll reference the canonical)
|
|
97
|
+
const deduped = files.filter((f) => !pathRewrites.has(f.path));
|
|
98
|
+
|
|
99
|
+
return deduped;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function unwrapListModel(model: Model, modelMap: Map<string, Model>): Model | null {
|
|
103
|
+
const dataField = model.fields.find((f) => f.name === 'data');
|
|
104
|
+
const hasListMetadata = model.fields.some((f) => f.name === 'list_metadata' || f.name === 'listMetadata');
|
|
105
|
+
if (dataField && hasListMetadata && dataField.type.kind === 'array') {
|
|
106
|
+
const itemType = dataField.type.items;
|
|
107
|
+
if (itemType.kind === 'model') {
|
|
108
|
+
return modelMap.get(itemType.name) ?? null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function generateModelFixture(
|
|
115
|
+
model: Model,
|
|
116
|
+
modelMap: Map<string, Model>,
|
|
117
|
+
enumMap: Map<string, Enum>,
|
|
118
|
+
): Record<string, any> {
|
|
119
|
+
const fixture: Record<string, any> = {};
|
|
120
|
+
|
|
121
|
+
const seenFieldNames = new Set<string>();
|
|
122
|
+
const deduplicatedFields = model.fields.filter((f) => {
|
|
123
|
+
const goName = fieldName(f.name);
|
|
124
|
+
if (seenFieldNames.has(goName)) return false;
|
|
125
|
+
seenFieldNames.add(goName);
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
for (const field of deduplicatedFields) {
|
|
130
|
+
const wireName = field.name;
|
|
131
|
+
if (field.example !== undefined) {
|
|
132
|
+
fixture[wireName] = field.example;
|
|
133
|
+
} else {
|
|
134
|
+
fixture[wireName] = generateFieldValue(field.type, field.name, model.name, modelMap, enumMap);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return fixture;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function generateFieldValue(
|
|
142
|
+
ref: TypeRef,
|
|
143
|
+
fName: string,
|
|
144
|
+
modelName: string,
|
|
145
|
+
modelMap: Map<string, Model>,
|
|
146
|
+
enumMap: Map<string, Enum>,
|
|
147
|
+
): any {
|
|
148
|
+
switch (ref.kind) {
|
|
149
|
+
case 'primitive':
|
|
150
|
+
return generatePrimitiveValue(ref.type, ref.format, fName, modelName);
|
|
151
|
+
case 'literal':
|
|
152
|
+
return ref.value;
|
|
153
|
+
case 'enum': {
|
|
154
|
+
const e = enumMap.get(ref.name);
|
|
155
|
+
return e?.values[0]?.value ?? 'unknown';
|
|
156
|
+
}
|
|
157
|
+
case 'model': {
|
|
158
|
+
const nested = modelMap.get(ref.name);
|
|
159
|
+
if (nested) return generateModelFixture(nested, modelMap, enumMap);
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
case 'array': {
|
|
163
|
+
if (ref.items.kind === 'enum') {
|
|
164
|
+
const e = enumMap.get(ref.items.name);
|
|
165
|
+
if (e && e.values.length > 0) {
|
|
166
|
+
return e.values.map((v) => v.value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const item = generateFieldValue(ref.items, fName, modelName, modelMap, enumMap);
|
|
170
|
+
return [item];
|
|
171
|
+
}
|
|
172
|
+
case 'nullable':
|
|
173
|
+
return generateFieldValue(ref.inner, fName, modelName, modelMap, enumMap);
|
|
174
|
+
case 'union':
|
|
175
|
+
if (ref.variants.length > 0) {
|
|
176
|
+
return generateFieldValue(ref.variants[0], fName, modelName, modelMap, enumMap);
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
case 'map':
|
|
180
|
+
return {
|
|
181
|
+
key: generateFieldValue(ref.valueType, 'value', modelName, modelMap, enumMap),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function generatePrimitiveValue(type: string, format: string | undefined, name: string, modelName: string): any {
|
|
187
|
+
switch (type) {
|
|
188
|
+
case 'string':
|
|
189
|
+
if (format === 'date-time') return '2023-01-01T00:00:00.000Z';
|
|
190
|
+
if (format === 'date') return '2023-01-01';
|
|
191
|
+
if (format === 'uuid') return '00000000-0000-0000-0000-000000000000';
|
|
192
|
+
if (name === 'id') {
|
|
193
|
+
const prefix = ID_PREFIXES[modelName] ?? '';
|
|
194
|
+
return `${prefix}01234`;
|
|
195
|
+
}
|
|
196
|
+
if (name.includes('id')) return `${name}_01234`;
|
|
197
|
+
if (name.includes('email')) return 'test@example.com';
|
|
198
|
+
if (name.includes('url') || name.includes('uri')) return 'https://example.com';
|
|
199
|
+
if (name.includes('name')) return 'Test';
|
|
200
|
+
return `test_${name}`;
|
|
201
|
+
case 'integer':
|
|
202
|
+
return 1;
|
|
203
|
+
case 'number':
|
|
204
|
+
return 1.0;
|
|
205
|
+
case 'boolean':
|
|
206
|
+
return true;
|
|
207
|
+
case 'unknown':
|
|
208
|
+
return {};
|
|
209
|
+
default:
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/go/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Emitter,
|
|
3
|
+
EmitterContext,
|
|
4
|
+
FormatCommand,
|
|
5
|
+
GeneratedFile,
|
|
6
|
+
ApiSpec,
|
|
7
|
+
Model,
|
|
8
|
+
Enum,
|
|
9
|
+
Service,
|
|
10
|
+
} from '@workos/oagen';
|
|
11
|
+
|
|
12
|
+
import { generateModels } from './models.js';
|
|
13
|
+
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
14
|
+
import { generateEnums } from './enums.js';
|
|
15
|
+
import { generateResources } from './resources.js';
|
|
16
|
+
import { generateClient } from './client.js';
|
|
17
|
+
import { generateTests } from './tests.js';
|
|
18
|
+
import { generateManifest } from './manifest.js';
|
|
19
|
+
|
|
20
|
+
/** Ensure every generated file's content ends with a trailing newline. */
|
|
21
|
+
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
22
|
+
for (const f of files) {
|
|
23
|
+
if (f.content && !f.content.endsWith('\n')) {
|
|
24
|
+
f.content += '\n';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return files;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const goEmitter: Emitter = {
|
|
31
|
+
language: 'go',
|
|
32
|
+
|
|
33
|
+
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
34
|
+
// Enrich models by flattening oneOf/allOf+oneOf variant fields from the raw spec
|
|
35
|
+
const enriched = enrichModelsFromSpec(models);
|
|
36
|
+
return ensureTrailingNewlines(generateModels(enriched, ctx));
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
40
|
+
// Merge synthetic enums produced during model enrichment (inline oneOf
|
|
41
|
+
// definitions) so they get proper type definitions in enums.go.
|
|
42
|
+
const syntheticEnums = getSyntheticEnums();
|
|
43
|
+
return ensureTrailingNewlines(generateEnums([...enums, ...syntheticEnums], ctx));
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
47
|
+
return ensureTrailingNewlines(generateResources(services, ctx));
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
51
|
+
return ensureTrailingNewlines(generateClient(spec, ctx));
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
generateErrors(): GeneratedFile[] {
|
|
55
|
+
return [];
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
generateTypeSignatures(): GeneratedFile[] {
|
|
59
|
+
// Go uses inline type annotations -- no separate type signature files needed
|
|
60
|
+
return [];
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
64
|
+
return ensureTrailingNewlines(generateTests(spec, ctx));
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
68
|
+
return ensureTrailingNewlines(generateManifest(spec, ctx));
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
fileHeader(): string {
|
|
72
|
+
return '// Code generated by oagen. DO NOT EDIT.';
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
formatCommand(targetDir: string): FormatCommand | null {
|
|
76
|
+
// Pass targetDir as the first path so gofmt formats the entire directory
|
|
77
|
+
// (including hand-maintained files), not just the generated file list.
|
|
78
|
+
return {
|
|
79
|
+
cmd: 'gofmt',
|
|
80
|
+
args: ['-w', targetDir],
|
|
81
|
+
batchSize: 999999,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ApiSpec, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { resolveMethodName } from './naming.js';
|
|
3
|
+
import { buildServiceAccessPaths } from './client.js';
|
|
4
|
+
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate smoke test manifest mapping HTTP operations to SDK methods.
|
|
8
|
+
*/
|
|
9
|
+
export function generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
10
|
+
const manifest: Record<string, { sdkMethod: string; service: string }> = {};
|
|
11
|
+
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
12
|
+
|
|
13
|
+
for (const service of spec.services) {
|
|
14
|
+
let propName = accessPaths.get(service.name);
|
|
15
|
+
if (!propName) {
|
|
16
|
+
const mountTarget = getMountTarget(service, ctx);
|
|
17
|
+
propName = accessPaths.get(mountTarget);
|
|
18
|
+
}
|
|
19
|
+
if (!propName) {
|
|
20
|
+
throw new Error(`Missing public client access path for service ${service.name}`);
|
|
21
|
+
}
|
|
22
|
+
for (const op of service.operations) {
|
|
23
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
24
|
+
const method = resolveMethodName(op, service, ctx);
|
|
25
|
+
manifest[httpKey] = { sdkMethod: method, service: propName };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
path: 'smoke-manifest.json',
|
|
32
|
+
content: JSON.stringify(manifest, null, 2),
|
|
33
|
+
integrateTarget: false,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|