@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.
Files changed (136) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +129 -0
  5. package/dist/index.d.mts +13 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +14549 -3385
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/sdk-architecture/dotnet.md +336 -0
  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 +328 -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 +45 -12
  18. package/smoke/sdk-go.ts +116 -42
  19. package/smoke/sdk-php.ts +28 -26
  20. package/smoke/sdk-python.ts +5 -2
  21. package/src/dotnet/client.ts +89 -0
  22. package/src/dotnet/enums.ts +323 -0
  23. package/src/dotnet/fixtures.ts +236 -0
  24. package/src/dotnet/index.ts +246 -0
  25. package/src/dotnet/manifest.ts +36 -0
  26. package/src/dotnet/models.ts +344 -0
  27. package/src/dotnet/naming.ts +330 -0
  28. package/src/dotnet/resources.ts +622 -0
  29. package/src/dotnet/tests.ts +693 -0
  30. package/src/dotnet/type-map.ts +201 -0
  31. package/src/dotnet/wrappers.ts +186 -0
  32. package/src/go/client.ts +141 -0
  33. package/src/go/enums.ts +196 -0
  34. package/src/go/fixtures.ts +212 -0
  35. package/src/go/index.ts +84 -0
  36. package/src/go/manifest.ts +36 -0
  37. package/src/go/models.ts +254 -0
  38. package/src/go/naming.ts +179 -0
  39. package/src/go/resources.ts +827 -0
  40. package/src/go/tests.ts +751 -0
  41. package/src/go/type-map.ts +82 -0
  42. package/src/go/wrappers.ts +261 -0
  43. package/src/index.ts +4 -0
  44. package/src/kotlin/client.ts +53 -0
  45. package/src/kotlin/enums.ts +162 -0
  46. package/src/kotlin/index.ts +92 -0
  47. package/src/kotlin/manifest.ts +55 -0
  48. package/src/kotlin/models.ts +395 -0
  49. package/src/kotlin/naming.ts +223 -0
  50. package/src/kotlin/overrides.ts +25 -0
  51. package/src/kotlin/resources.ts +667 -0
  52. package/src/kotlin/tests.ts +1019 -0
  53. package/src/kotlin/type-map.ts +123 -0
  54. package/src/kotlin/wrappers.ts +168 -0
  55. package/src/node/client.ts +128 -115
  56. package/src/node/enums.ts +9 -0
  57. package/src/node/errors.ts +37 -232
  58. package/src/node/field-plan.ts +726 -0
  59. package/src/node/fixtures.ts +9 -1
  60. package/src/node/index.ts +3 -9
  61. package/src/node/models.ts +178 -21
  62. package/src/node/naming.ts +49 -111
  63. package/src/node/resources.ts +527 -397
  64. package/src/node/sdk-errors.ts +41 -0
  65. package/src/node/tests.ts +69 -19
  66. package/src/node/type-map.ts +4 -2
  67. package/src/node/utils.ts +13 -71
  68. package/src/node/wrappers.ts +151 -0
  69. package/src/php/client.ts +179 -0
  70. package/src/php/enums.ts +67 -0
  71. package/src/php/errors.ts +9 -0
  72. package/src/php/fixtures.ts +181 -0
  73. package/src/php/index.ts +96 -0
  74. package/src/php/manifest.ts +36 -0
  75. package/src/php/models.ts +310 -0
  76. package/src/php/naming.ts +279 -0
  77. package/src/php/resources.ts +636 -0
  78. package/src/php/tests.ts +609 -0
  79. package/src/php/type-map.ts +90 -0
  80. package/src/php/utils.ts +18 -0
  81. package/src/php/wrappers.ts +152 -0
  82. package/src/python/client.ts +345 -0
  83. package/src/python/enums.ts +313 -0
  84. package/src/python/fixtures.ts +196 -0
  85. package/src/python/index.ts +95 -0
  86. package/src/python/manifest.ts +38 -0
  87. package/src/python/models.ts +688 -0
  88. package/src/python/naming.ts +189 -0
  89. package/src/python/resources.ts +1322 -0
  90. package/src/python/tests.ts +1335 -0
  91. package/src/python/type-map.ts +93 -0
  92. package/src/python/wrappers.ts +191 -0
  93. package/src/shared/model-utils.ts +472 -0
  94. package/src/shared/naming-utils.ts +154 -0
  95. package/src/shared/non-spec-services.ts +54 -0
  96. package/src/shared/resolved-ops.ts +109 -0
  97. package/src/shared/wrapper-utils.ts +70 -0
  98. package/test/dotnet/client.test.ts +121 -0
  99. package/test/dotnet/enums.test.ts +193 -0
  100. package/test/dotnet/errors.test.ts +9 -0
  101. package/test/dotnet/manifest.test.ts +82 -0
  102. package/test/dotnet/models.test.ts +260 -0
  103. package/test/dotnet/resources.test.ts +255 -0
  104. package/test/dotnet/tests.test.ts +202 -0
  105. package/test/go/client.test.ts +92 -0
  106. package/test/go/enums.test.ts +132 -0
  107. package/test/go/errors.test.ts +9 -0
  108. package/test/go/models.test.ts +265 -0
  109. package/test/go/resources.test.ts +408 -0
  110. package/test/go/tests.test.ts +143 -0
  111. package/test/kotlin/models.test.ts +135 -0
  112. package/test/kotlin/tests.test.ts +176 -0
  113. package/test/node/client.test.ts +92 -12
  114. package/test/node/enums.test.ts +2 -0
  115. package/test/node/errors.test.ts +2 -41
  116. package/test/node/models.test.ts +2 -0
  117. package/test/node/naming.test.ts +23 -0
  118. package/test/node/resources.test.ts +315 -84
  119. package/test/node/serializers.test.ts +3 -1
  120. package/test/node/type-map.test.ts +11 -0
  121. package/test/php/client.test.ts +95 -0
  122. package/test/php/enums.test.ts +173 -0
  123. package/test/php/errors.test.ts +9 -0
  124. package/test/php/models.test.ts +497 -0
  125. package/test/php/resources.test.ts +682 -0
  126. package/test/php/tests.test.ts +185 -0
  127. package/test/python/client.test.ts +200 -0
  128. package/test/python/enums.test.ts +228 -0
  129. package/test/python/errors.test.ts +16 -0
  130. package/test/python/manifest.test.ts +74 -0
  131. package/test/python/models.test.ts +716 -0
  132. package/test/python/resources.test.ts +617 -0
  133. package/test/python/tests.test.ts +202 -0
  134. package/src/node/common.ts +0 -273
  135. package/src/node/config.ts +0 -71
  136. package/src/node/serializers.ts +0 -746
@@ -0,0 +1,201 @@
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
+ /** Known C# value types that need `?` for nullable. */
6
+ const VALUE_TYPES = new Set(['int', 'long', 'double', 'bool', 'float', 'decimal', 'byte', 'short', 'DateTimeOffset']);
7
+
8
+ /**
9
+ * Module-level alias map for structurally-identical enums. Populated by
10
+ * `setEnumAliases` from the current spec's enum list; consulted by
11
+ * `mapTypeRef` so that every reference to a duplicate enum resolves to the
12
+ * canonical name. C# has no first-class type alias for enums, so dedup must
13
+ * happen at reference-rewrite time rather than via a runtime alias.
14
+ */
15
+ const enumAliases = new Map<string, string>();
16
+
17
+ /**
18
+ * Names of enums that resolve to a single wire value and should therefore be
19
+ * mapped to C# `string` at reference sites (the owning property emits a
20
+ * const initializer). Populated by `setSingleValueEnumNames`.
21
+ */
22
+ const singleValueEnumNames = new Set<string>();
23
+
24
+ /** Replace the current enum-alias map. Safe to call more than once. */
25
+ export function setEnumAliases(aliases: Map<string, string>): void {
26
+ enumAliases.clear();
27
+ for (const [k, v] of aliases) enumAliases.set(k, v);
28
+ }
29
+
30
+ /** Replace the set of enum names that are single-value discriminator stand-ins. */
31
+ export function setSingleValueEnumNames(names: Iterable<string>): void {
32
+ singleValueEnumNames.clear();
33
+ for (const n of names) singleValueEnumNames.add(n);
34
+ }
35
+
36
+ /** Resolve an enum reference name to its canonical form. */
37
+ export function resolveEnumTypeName(name: string): string {
38
+ return enumAliases.get(name) ?? name;
39
+ }
40
+
41
+ /**
42
+ * Map an IR TypeRef to a C# type string.
43
+ */
44
+ export function mapTypeRef(ref: TypeRef): string {
45
+ return irMapTypeRef<string>(ref, {
46
+ primitive: mapPrimitive,
47
+ array: (_ref, items) => `List<${items}>`,
48
+ model: (r) => className(r.name),
49
+ enum: (r) => {
50
+ // Single-value enums (discriminator consts in disguise) map to `string`
51
+ // so the caller can't misuse a public one-member enum type. The
52
+ // owning property emits a const initializer separately.
53
+ if ((r.values?.length ?? 0) === 1) return 'string';
54
+ if (singleValueEnumNames.has(r.name)) return 'string';
55
+ return className(resolveEnumTypeName(r.name));
56
+ },
57
+ union: (_r, variants) => joinUnionVariants(_r, variants),
58
+ nullable: (_ref, inner) => {
59
+ // With <Nullable>enable</Nullable>, all nullable types need `?`
60
+ if (inner.endsWith('?')) return inner; // already nullable (e.g., nested nullable)
61
+ return `${inner}?`;
62
+ },
63
+ literal: (r) => {
64
+ if (r.value === null) return 'object';
65
+ if (typeof r.value === 'string') return 'string';
66
+ if (typeof r.value === 'number') return Number.isInteger(r.value) ? 'int' : 'double';
67
+ if (typeof r.value === 'boolean') return 'bool';
68
+ return 'object';
69
+ },
70
+ map: (_ref, value) => `Dictionary<string, ${value}>`,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Map an IR TypeRef to a C# type string, making optional fields nullable.
76
+ * For value types, appends `?`. For reference types, returns as-is.
77
+ */
78
+ export function mapTypeRefOptional(ref: TypeRef): string {
79
+ const baseType = mapTypeRef(ref);
80
+ if (isValueType(baseType)) return `${baseType}?`;
81
+ return baseType;
82
+ }
83
+
84
+ /**
85
+ * Check if a C# type is a value type (needs ? for nullable).
86
+ */
87
+ export function isValueType(csType: string): boolean {
88
+ // Strip trailing ? if present
89
+ const bare = csType.endsWith('?') ? csType.slice(0, -1) : csType;
90
+ if (VALUE_TYPES.has(bare)) return true;
91
+ // Enums are value types, but we can't detect them purely from the type string.
92
+ // The caller should handle enum nullability explicitly when needed.
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Check if an IR TypeRef maps to a C# value type.
98
+ */
99
+ export function isValueTypeRef(ref: TypeRef): boolean {
100
+ if (ref.kind === 'enum') return true;
101
+ if (ref.kind === 'primitive') {
102
+ // DateTimeOffset is a value type (struct)
103
+ if (ref.format === 'date-time') return true;
104
+ switch (ref.type) {
105
+ case 'integer':
106
+ case 'number':
107
+ case 'boolean':
108
+ return true;
109
+ default:
110
+ return false;
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Whether a TypeRef directly names an enum (no nullable wrapper).
118
+ * Used to detect required enum request fields that must not silently serialize
119
+ * their default Unknown sentinel.
120
+ */
121
+ export function isEnumRef(ref: TypeRef): boolean {
122
+ return ref.kind === 'enum';
123
+ }
124
+
125
+ /**
126
+ * Emit JSON attributes for a request-side property. When `isRequiredEnum` is
127
+ * true, configure both serializers to skip the field when its value equals the
128
+ * enum default (0 = Unknown sentinel). This converts "unset required enum"
129
+ * from a silent `"unknown"` wire value into a clean omission so the API
130
+ * returns a clear `missing required field` error instead of a confusing 422.
131
+ */
132
+ export function emitJsonPropertyAttributes(wireName: string, options: { isRequiredEnum?: boolean } = {}): string[] {
133
+ if (options.isRequiredEnum) {
134
+ return [
135
+ ` [JsonProperty("${wireName}", DefaultValueHandling = DefaultValueHandling.Ignore)]`,
136
+ ` [STJS.JsonPropertyName("${wireName}")]`,
137
+ ` [STJS.JsonIgnore(Condition = STJS.JsonIgnoreCondition.WhenWritingDefault)]`,
138
+ ];
139
+ }
140
+ return [` [JsonProperty("${wireName}")]`, ` [STJS.JsonPropertyName("${wireName}")]`];
141
+ }
142
+
143
+ function mapPrimitive(ref: PrimitiveType): string {
144
+ if (ref.format === 'binary') return 'byte[]';
145
+ if (ref.format === 'int32') return 'int';
146
+ if (ref.format === 'int64') return 'long';
147
+ if (ref.format === 'date-time') return 'DateTimeOffset';
148
+ switch (ref.type) {
149
+ case 'string':
150
+ return 'string';
151
+ case 'integer':
152
+ return 'long';
153
+ case 'number':
154
+ return 'double';
155
+ case 'boolean':
156
+ return 'bool';
157
+ case 'unknown':
158
+ return 'object';
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Track discriminated unions for downstream model generation.
164
+ * Key = generated base type name, Value = discriminator info.
165
+ */
166
+ export const discriminatedUnions = new Map<
167
+ string,
168
+ { property: string; mapping: Record<string, string>; variantTypes: string[] }
169
+ >();
170
+
171
+ function joinUnionVariants(_ref: UnionType, variants: string[]): string {
172
+ if (_ref.compositionKind === 'allOf') {
173
+ return variants[0] ?? 'object';
174
+ }
175
+ const unique = [...new Set(variants)];
176
+ if (unique.length === 1) return unique[0];
177
+
178
+ // Discriminated union: register for converter generation and return first variant as base
179
+ if (_ref.discriminator && _ref.discriminator.mapping) {
180
+ const baseName = unique[0];
181
+ discriminatedUnions.set(baseName, {
182
+ property: _ref.discriminator.property,
183
+ mapping: _ref.discriminator.mapping,
184
+ variantTypes: unique,
185
+ });
186
+ // Use object with JsonConverter for discriminated unions since
187
+ // AnyOf<> doesn't support discriminator-based deserialization
188
+ return 'object';
189
+ }
190
+
191
+ if (unique.length >= 2 && unique.length <= 3) return `AnyOf<${unique.join(', ')}>`;
192
+ // AnyOf only supports arity 2 and 3. Higher-arity unions collapse to
193
+ // `object`, losing type information. Warn so the author knows the spec
194
+ // outgrew the runtime support instead of silently degrading.
195
+ if (unique.length >= 4) {
196
+ console.warn(
197
+ `[oagen:dotnet] Union with ${unique.length} variants exceeds AnyOf<T1,T2,T3> arity; falling back to object. Variants: ${unique.join(', ')}`,
198
+ );
199
+ }
200
+ return 'object';
201
+ }
@@ -0,0 +1,186 @@
1
+ import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
2
+ import {
3
+ className as csClassName,
4
+ fieldName as csFieldName,
5
+ methodName as csMethodName,
6
+ localName,
7
+ csLiteral,
8
+ clientFieldExpression,
9
+ httpMethodHelperName,
10
+ escapeXml,
11
+ emitXmlDoc,
12
+ humanize,
13
+ } from './naming.js';
14
+ import { sortPathParamsByTemplateOrder } from './resources.js';
15
+ import { resolveWrapperParams, formatWrapperDescription, type ResolvedWrapperParam } from '../shared/wrapper-utils.js';
16
+ import { mapTypeRef, isValueTypeRef, isEnumRef, emitJsonPropertyAttributes } from './type-map.js';
17
+
18
+ /**
19
+ * Generate C# wrapper method lines for union split operations.
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
+ emitWrapperMethod(lines, resolvedOp, wrapper, wrapperParams, ctx);
34
+ }
35
+
36
+ return lines;
37
+ }
38
+
39
+ function emitWrapperMethod(
40
+ lines: string[],
41
+ resolvedOp: ResolvedOperation,
42
+ wrapper: ResolvedWrapper,
43
+ _wrapperParams: ResolvedWrapperParam[],
44
+ _ctx: EmitterContext,
45
+ ): void {
46
+ const op = resolvedOp.operation;
47
+ const method = csMethodName(wrapper.name);
48
+ const optionsClass = `${method}Options`;
49
+ const responseType = wrapper.responseModelName ? csClassName(wrapper.responseModelName) : null;
50
+
51
+ // XML doc
52
+ lines.push(` /// <summary>${formatWrapperDescription(wrapper.name)}.</summary>`);
53
+ for (const p of sortPathParamsByTemplateOrder(op)) {
54
+ const paramDesc = p.description ? escapeXml(p.description) : `The ${humanize(p.name)}.`;
55
+ lines.push(` /// <param name="${localName(p.name)}">${paramDesc}</param>`);
56
+ }
57
+ lines.push(` /// <param name="options">Request options.</param>`);
58
+ lines.push(` /// <param name="requestOptions">Per-request configuration overrides.</param>`);
59
+ lines.push(` /// <param name="cancellationToken">Cancellation token.</param>`);
60
+ if (responseType) {
61
+ lines.push(` /// <returns>The <see cref="${responseType}"/> result.</returns>`);
62
+ }
63
+
64
+ // Signature
65
+ const sigParams: string[] = [];
66
+ for (const p of sortPathParamsByTemplateOrder(op)) {
67
+ sigParams.push(`string ${localName(p.name)}`);
68
+ }
69
+ sigParams.push(`${optionsClass} options`);
70
+ sigParams.push('RequestOptions? requestOptions = null');
71
+ sigParams.push('CancellationToken cancellationToken = default');
72
+
73
+ const returnType = responseType ? `Task<${responseType}>` : 'Task';
74
+ lines.push(` public async ${returnType} ${method}(${sigParams.join(', ')})`);
75
+ lines.push(' {');
76
+
77
+ // Set defaults on options
78
+ for (const [key, value] of Object.entries(wrapper.defaults)) {
79
+ lines.push(` options.${csFieldName(key)} = ${csLiteral(value)};`);
80
+ }
81
+
82
+ // Set inferred fields from client. ClientId is required: fail loudly via RequireClientId()
83
+ // so that callers who forgot to configure it get a clear error instead of a 422 from the API.
84
+ for (const field of wrapper.inferFromClient) {
85
+ if (field === 'client_id') {
86
+ lines.push(` options.${csFieldName(field)} = this.Client.RequireClientId();`);
87
+ } else {
88
+ lines.push(
89
+ ` options.${csFieldName(field)} = this.Client.${clientFieldExpression(field)} ?? string.Empty;`,
90
+ );
91
+ }
92
+ }
93
+
94
+ // Build path
95
+ let pathExpr: string;
96
+ if (op.pathParams.length > 0) {
97
+ let interpolated = op.path;
98
+ for (const p of sortPathParamsByTemplateOrder(op)) {
99
+ interpolated = interpolated.replace(`{${p.name}}`, `{${localName(p.name)}}`);
100
+ }
101
+ pathExpr = `$"${interpolated}"`;
102
+ } else {
103
+ pathExpr = `"${op.path}"`;
104
+ }
105
+
106
+ // Use the Service base-class helper so wrappers read as one-liners.
107
+ const helper = httpMethodHelperName(op.httpMethod);
108
+ if (responseType) {
109
+ lines.push(
110
+ ` return await this.${helper}<${responseType}>(${pathExpr}, options, requestOptions, cancellationToken);`,
111
+ );
112
+ } else if (helper === 'DeleteAsync') {
113
+ lines.push(` await this.${helper}(${pathExpr}, options, requestOptions, cancellationToken);`);
114
+ } else {
115
+ lines.push(` await this.${helper}<object>(${pathExpr}, options, requestOptions, cancellationToken);`);
116
+ }
117
+
118
+ lines.push(' }');
119
+ }
120
+
121
+ // NOTE: T26 (wrapper DRY) — the AuthenticateWith* wrappers share a small
122
+ // SendAuthenticateAsync helper at runtime to avoid 8x copies of the same
123
+ // MakeAPIRequest call. The helper itself lives in UserManagementService.cs as
124
+ // a hand-maintained method (it can't easily be expressed as a generic because
125
+ // the eight options classes don't share an interface). Keeping each generated
126
+ // wrapper's body short is the practical part of the DRY win.
127
+
128
+ /**
129
+ * Generate wrapper options classes. Called from resources.ts options generation.
130
+ */
131
+ export function generateWrapperOptionsClasses(resolvedOp: ResolvedOperation, ctx: EmitterContext): string[] {
132
+ if (!resolvedOp.wrappers || resolvedOp.wrappers.length === 0) return [];
133
+
134
+ const lines: string[] = [];
135
+
136
+ for (const wrapper of resolvedOp.wrappers) {
137
+ const wrapperParams = resolveWrapperParams(wrapper, ctx);
138
+ const optionsClass = `${csMethodName(wrapper.name)}Options`;
139
+
140
+ lines.push('');
141
+ lines.push(` public class ${optionsClass} : BaseOptions`);
142
+ lines.push(' {');
143
+
144
+ // Exposed params
145
+ for (const { paramName, field, isOptional } of wrapperParams) {
146
+ const csField = csFieldName(paramName);
147
+ const csType = field ? resolveSimpleCsType(field.type, isOptional) : isOptional ? 'string?' : 'string';
148
+ const needsDefault = !isOptional && !csType.endsWith('?') && !(field && isValueTypeRef(field.type));
149
+ const initializer = needsDefault ? ' = default!;' : '';
150
+
151
+ const isRequiredEnum = !isOptional && !!field && isEnumRef(field.type);
152
+ lines.push(...emitXmlDoc(field?.description, ' '));
153
+ lines.push(...emitJsonPropertyAttributes(paramName, { isRequiredEnum }));
154
+ lines.push(` public ${csType} ${csField} { get; set; }${initializer}`);
155
+ lines.push('');
156
+ }
157
+
158
+ // Hidden fields (defaults + inferred)
159
+ for (const key of Object.keys(wrapper.defaults)) {
160
+ const csField = csFieldName(key);
161
+ lines.push(` [JsonProperty("${key}")]`);
162
+ lines.push(` [STJS.JsonPropertyName("${key}")]`);
163
+ lines.push(` internal string ${csField} { get; set; } = default!;`);
164
+ lines.push('');
165
+ }
166
+ for (const key of wrapper.inferFromClient) {
167
+ const csField = csFieldName(key);
168
+ // Skip if already added as a default
169
+ if (Object.keys(wrapper.defaults).includes(key)) continue;
170
+ lines.push(` [JsonProperty("${key}")]`);
171
+ lines.push(` [STJS.JsonPropertyName("${key}")]`);
172
+ lines.push(` internal string ${csField} { get; set; } = default!;`);
173
+ lines.push('');
174
+ }
175
+
176
+ lines.push(' }');
177
+ }
178
+
179
+ return lines;
180
+ }
181
+
182
+ function resolveSimpleCsType(ref: any, isOptional: boolean): string {
183
+ const base = mapTypeRef(ref);
184
+ if (isOptional && !base.endsWith('?')) return `${base}?`;
185
+ return base;
186
+ }
@@ -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
+ }