@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,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
|
+
}
|
package/src/go/client.ts
ADDED
|
@@ -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
|
+
}
|