@workos/oagen-emitters 0.2.0 → 0.3.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.
Files changed (110) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.oxfmtrc.json +8 -1
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +129 -0
  6. package/dist/index.d.mts +10 -1
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +11943 -2728
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +298 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +17 -3
  18. package/smoke/sdk-elixir.ts +17 -3
  19. package/smoke/sdk-go.ts +137 -46
  20. package/smoke/sdk-kotlin.ts +23 -4
  21. package/smoke/sdk-node.ts +15 -3
  22. package/smoke/sdk-php.ts +28 -26
  23. package/smoke/sdk-python.ts +5 -2
  24. package/smoke/sdk-ruby.ts +17 -3
  25. package/smoke/sdk-rust.ts +16 -3
  26. package/src/go/client.ts +141 -0
  27. package/src/go/enums.ts +196 -0
  28. package/src/go/fixtures.ts +212 -0
  29. package/src/go/index.ts +81 -0
  30. package/src/go/manifest.ts +36 -0
  31. package/src/go/models.ts +254 -0
  32. package/src/go/naming.ts +191 -0
  33. package/src/go/resources.ts +827 -0
  34. package/src/go/tests.ts +751 -0
  35. package/src/go/type-map.ts +82 -0
  36. package/src/go/wrappers.ts +261 -0
  37. package/src/index.ts +3 -0
  38. package/src/node/client.ts +167 -122
  39. package/src/node/enums.ts +13 -4
  40. package/src/node/errors.ts +42 -233
  41. package/src/node/field-plan.ts +726 -0
  42. package/src/node/fixtures.ts +15 -5
  43. package/src/node/index.ts +65 -16
  44. package/src/node/models.ts +264 -96
  45. package/src/node/naming.ts +52 -25
  46. package/src/node/resources.ts +621 -172
  47. package/src/node/sdk-errors.ts +41 -0
  48. package/src/node/tests.ts +71 -27
  49. package/src/node/type-map.ts +4 -2
  50. package/src/node/utils.ts +56 -64
  51. package/src/node/wrappers.ts +151 -0
  52. package/src/php/client.ts +171 -0
  53. package/src/php/enums.ts +67 -0
  54. package/src/php/errors.ts +9 -0
  55. package/src/php/fixtures.ts +181 -0
  56. package/src/php/index.ts +96 -0
  57. package/src/php/manifest.ts +36 -0
  58. package/src/php/models.ts +310 -0
  59. package/src/php/naming.ts +298 -0
  60. package/src/php/resources.ts +561 -0
  61. package/src/php/tests.ts +533 -0
  62. package/src/php/type-map.ts +90 -0
  63. package/src/php/utils.ts +18 -0
  64. package/src/php/wrappers.ts +151 -0
  65. package/src/python/client.ts +337 -0
  66. package/src/python/enums.ts +313 -0
  67. package/src/python/fixtures.ts +196 -0
  68. package/src/python/index.ts +95 -0
  69. package/src/python/manifest.ts +38 -0
  70. package/src/python/models.ts +688 -0
  71. package/src/python/naming.ts +209 -0
  72. package/src/python/resources.ts +1322 -0
  73. package/src/python/tests.ts +1335 -0
  74. package/src/python/type-map.ts +93 -0
  75. package/src/python/wrappers.ts +191 -0
  76. package/src/shared/model-utils.ts +255 -0
  77. package/src/shared/naming-utils.ts +107 -0
  78. package/src/shared/non-spec-services.ts +54 -0
  79. package/src/shared/resolved-ops.ts +109 -0
  80. package/src/shared/wrapper-utils.ts +59 -0
  81. package/test/go/client.test.ts +92 -0
  82. package/test/go/enums.test.ts +132 -0
  83. package/test/go/errors.test.ts +9 -0
  84. package/test/go/models.test.ts +265 -0
  85. package/test/go/resources.test.ts +408 -0
  86. package/test/go/tests.test.ts +143 -0
  87. package/test/node/client.test.ts +199 -94
  88. package/test/node/enums.test.ts +75 -3
  89. package/test/node/errors.test.ts +2 -41
  90. package/test/node/models.test.ts +109 -20
  91. package/test/node/naming.test.ts +37 -4
  92. package/test/node/resources.test.ts +662 -30
  93. package/test/node/serializers.test.ts +36 -7
  94. package/test/node/type-map.test.ts +11 -0
  95. package/test/php/client.test.ts +94 -0
  96. package/test/php/enums.test.ts +173 -0
  97. package/test/php/errors.test.ts +9 -0
  98. package/test/php/models.test.ts +497 -0
  99. package/test/php/resources.test.ts +644 -0
  100. package/test/php/tests.test.ts +118 -0
  101. package/test/python/client.test.ts +200 -0
  102. package/test/python/enums.test.ts +228 -0
  103. package/test/python/errors.test.ts +16 -0
  104. package/test/python/manifest.test.ts +74 -0
  105. package/test/python/models.test.ts +716 -0
  106. package/test/python/resources.test.ts +617 -0
  107. package/test/python/tests.test.ts +202 -0
  108. package/src/node/common.ts +0 -273
  109. package/src/node/config.ts +0 -71
  110. package/src/node/serializers.ts +0 -744
@@ -0,0 +1,141 @@
1
+ import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
+ import { toPascalCase, toSnakeCase } from '@workos/oagen';
3
+ // naming utilities used indirectly via resolveResourceClassName
4
+ import { resolveResourceClassName } from './resources.js';
5
+ import { unexportedName } from './naming.js';
6
+ import { getMountTarget } from '../shared/resolved-ops.js';
7
+
8
+ /**
9
+ * Generate the Go client file with service accessors.
10
+ * Produces: workos.go (Client struct + constructor + service accessors).
11
+ * Static files (client.go, pagination.go, errors.go, go.mod, options.go)
12
+ * are hand-maintained in the target SDK with @oagen-ignore-file.
13
+ */
14
+ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
15
+ return [generateWorkOSFile(spec, ctx)];
16
+ }
17
+
18
+ /**
19
+ * Deduplicate services by mount target.
20
+ */
21
+ function deduplicateByMount(services: Service[], ctx: EmitterContext): Service[] {
22
+ const byTarget = new Map<string, Service>();
23
+ for (const s of services) {
24
+ const target = getMountTarget(s, ctx);
25
+ const existing = byTarget.get(target);
26
+ if (!existing || toPascalCase(s.name) === target) {
27
+ byTarget.set(target, s);
28
+ }
29
+ }
30
+ return [...byTarget.values()];
31
+ }
32
+
33
+ /**
34
+ * Build map of service name -> accessor property name.
35
+ */
36
+ export function buildServiceAccessPaths(services: Service[], ctx: EmitterContext): Map<string, string> {
37
+ const topLevel = deduplicateByMount(services, ctx);
38
+ const paths = new Map<string, string>();
39
+
40
+ for (const service of topLevel) {
41
+ const resolvedName = resolveResourceClassName(service, ctx);
42
+ const prop = toSnakeCase(resolvedName);
43
+ paths.set(service.name, prop);
44
+ }
45
+
46
+ // Also map mount targets
47
+ for (const service of services) {
48
+ const target = getMountTarget(service, ctx);
49
+ if (!paths.has(target)) {
50
+ const existing = paths.get(service.name);
51
+ if (existing) paths.set(target, existing);
52
+ }
53
+ }
54
+
55
+ return paths;
56
+ }
57
+
58
+ function generateWorkOSFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
59
+ const topLevel = deduplicateByMount(spec.services, ctx);
60
+ const lines: string[] = [];
61
+
62
+ lines.push(`package ${ctx.namespace}`);
63
+ lines.push('');
64
+ lines.push('import "net/http"');
65
+ lines.push('');
66
+
67
+ // Client struct
68
+ lines.push('// Client is the WorkOS API client.');
69
+ lines.push('type Client struct {');
70
+ lines.push('\tapiKey string');
71
+ lines.push('\tclientID string');
72
+ lines.push('\tbaseURL string');
73
+ lines.push('\thttpClient *http.Client');
74
+ lines.push('\tmaxRetries int');
75
+ lines.push('');
76
+ // Service fields
77
+ for (const service of topLevel) {
78
+ const resolvedName = resolveResourceClassName(service, ctx);
79
+ const fieldNameStr = unexportedName(resolvedName);
80
+ const serviceTypeName = serviceType(resolvedName);
81
+ lines.push(`\t${fieldNameStr} *${serviceTypeName}`);
82
+ }
83
+ lines.push('}');
84
+ lines.push('');
85
+
86
+ // NewClient constructor
87
+ lines.push('// NewClient creates a new WorkOS API client.');
88
+ lines.push('func NewClient(apiKey string, opts ...ClientOption) *Client {');
89
+ lines.push('\tc := &Client{');
90
+ lines.push('\t\tapiKey: apiKey,');
91
+ lines.push('\t\tbaseURL: defaultBaseURL,');
92
+ lines.push('\t\thttpClient: &http.Client{Timeout: defaultTimeout},');
93
+ lines.push('\t\tmaxRetries: defaultMaxRetries,');
94
+ lines.push('\t}');
95
+ lines.push('\tfor _, opt := range opts {');
96
+ lines.push('\t\topt(c)');
97
+ lines.push('\t}');
98
+ // Initialize services
99
+ for (const service of topLevel) {
100
+ const resolvedName = resolveResourceClassName(service, ctx);
101
+ const fieldNameStr = unexportedName(resolvedName);
102
+ const serviceTypeName = serviceType(resolvedName);
103
+ lines.push(`\tc.${fieldNameStr} = &${serviceTypeName}{client: c}`);
104
+ }
105
+ lines.push('\treturn c');
106
+ lines.push('}');
107
+ lines.push('');
108
+
109
+ // Service accessor methods
110
+ for (const service of topLevel) {
111
+ const resolvedName = resolveResourceClassName(service, ctx);
112
+ const accessorName = resolvedName;
113
+ const fieldNameStr = unexportedName(resolvedName);
114
+ const serviceTypeName = serviceType(resolvedName);
115
+ lines.push(`// ${accessorName} returns the ${resolvedName} service.`);
116
+ lines.push(`func (c *Client) ${accessorName}() *${serviceTypeName} {`);
117
+ lines.push(`\treturn c.${fieldNameStr}`);
118
+ lines.push('}');
119
+ lines.push('');
120
+ }
121
+
122
+ return {
123
+ path: `${ctx.namespace}.go`,
124
+ content: lines.join('\n'),
125
+ overwriteExisting: true,
126
+ };
127
+ }
128
+
129
+ function singularizePascal(name: string): string {
130
+ if (name.endsWith('ies')) {
131
+ return `${name.slice(0, -3)}y`;
132
+ }
133
+ if (name.endsWith('s') && !name.endsWith('ss')) {
134
+ return name.slice(0, -1);
135
+ }
136
+ return name;
137
+ }
138
+
139
+ function serviceType(name: string): string {
140
+ return `${unexportedName(singularizePascal(name))}Service`;
141
+ }
@@ -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
+ }
@@ -0,0 +1,81 @@
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 } 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
+ return ensureTrailingNewlines(generateEnums(enums, ctx));
41
+ },
42
+
43
+ generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
44
+ return ensureTrailingNewlines(generateResources(services, ctx));
45
+ },
46
+
47
+ generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
48
+ return ensureTrailingNewlines(generateClient(spec, ctx));
49
+ },
50
+
51
+ generateErrors(): GeneratedFile[] {
52
+ return [];
53
+ },
54
+
55
+ generateTypeSignatures(): GeneratedFile[] {
56
+ // Go uses inline type annotations -- no separate type signature files needed
57
+ return [];
58
+ },
59
+
60
+ generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
61
+ return ensureTrailingNewlines(generateTests(spec, ctx));
62
+ },
63
+
64
+ generateManifest(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
65
+ return ensureTrailingNewlines(generateManifest(spec, ctx));
66
+ },
67
+
68
+ fileHeader(): string {
69
+ return '// Code generated by oagen. DO NOT EDIT.';
70
+ },
71
+
72
+ formatCommand(targetDir: string): FormatCommand | null {
73
+ // Pass targetDir as the first path so gofmt formats the entire directory
74
+ // (including hand-maintained files), not just the generated file list.
75
+ return {
76
+ cmd: 'gofmt',
77
+ args: ['-w', targetDir],
78
+ batchSize: 999999,
79
+ };
80
+ },
81
+ };
@@ -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
+ }