@workos/oagen-emitters 0.3.0 → 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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3288 -791
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- package/oagen.config.ts +42 -12
- package/package.json +2 -2
- package/smoke/sdk-dotnet.ts +45 -12
- 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/index.ts +5 -2
- package/src/go/naming.ts +5 -17
- package/src/index.ts +1 -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 +50 -0
- package/src/node/index.ts +1 -0
- package/src/node/resources.ts +164 -44
- package/src/node/tests.ts +37 -7
- package/src/php/client.ts +11 -3
- package/src/php/naming.ts +2 -21
- package/src/php/resources.ts +81 -6
- package/src/php/tests.ts +93 -17
- package/src/php/wrappers.ts +1 -0
- package/src/python/client.ts +37 -29
- package/src/python/enums.ts +7 -7
- package/src/python/models.ts +1 -1
- package/src/python/naming.ts +2 -22
- package/src/shared/model-utils.ts +232 -15
- package/src/shared/naming-utils.ts +47 -0
- package/src/shared/wrapper-utils.ts +12 -1
- 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/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +74 -0
- package/test/node/resources.test.ts +216 -15
- package/test/php/client.test.ts +2 -1
- package/test/php/resources.test.ts +38 -0
- package/test/php/tests.test.ts +67 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Model, Field, TypeRef } from '@workos/oagen';
|
|
1
|
+
import type { Model, Field, TypeRef, Enum } from '@workos/oagen';
|
|
2
2
|
import { readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
// @ts-ignore -- js-yaml has no type declarations in this project
|
|
@@ -103,13 +103,80 @@ function lookupRawSchema(name: string): Record<string, any> | null {
|
|
|
103
103
|
return spec?.components?.schemas?.[name] ?? null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Synthetic model / enum collection
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Accumulator for synthetic models and enums generated from inline
|
|
112
|
+
* definitions encountered during oneOf flattening.
|
|
113
|
+
*/
|
|
114
|
+
interface SyntheticCollector {
|
|
115
|
+
models: Model[];
|
|
116
|
+
enums: Array<{ name: string; values: Array<{ value: string; description?: string }> }>;
|
|
117
|
+
/** Track names already used to avoid duplicates. */
|
|
118
|
+
usedNames: Set<string>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function createCollector(): SyntheticCollector {
|
|
122
|
+
return { models: [], enums: [], usedNames: new Set() };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Singularize a snake_case name for use as an array-item model name.
|
|
127
|
+
* `redirect_uris` -> `redirect_uri`, `scopes` -> `scope`.
|
|
128
|
+
*/
|
|
129
|
+
function singularizeSnake(name: string): string {
|
|
130
|
+
if (name.endsWith('ies') && name.length > 3) {
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// rawSchemaToTypeRef -- with synthetic model/enum generation
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Convert a raw OpenAPI type+format to an IR TypeRef.
|
|
145
|
+
*
|
|
146
|
+
* When `parentModelName` and `fieldName` are provided, inline objects and
|
|
147
|
+
* enums generate synthetic models/enums instead of degrading to `unknown`
|
|
148
|
+
* or `string`.
|
|
149
|
+
*/
|
|
150
|
+
function rawSchemaToTypeRef(
|
|
151
|
+
schema: Record<string, any>,
|
|
152
|
+
parentModelName?: string,
|
|
153
|
+
fName?: string,
|
|
154
|
+
collector?: SyntheticCollector,
|
|
155
|
+
): TypeRef {
|
|
108
156
|
if (schema.const !== undefined) {
|
|
109
157
|
return { kind: 'literal', value: schema.const };
|
|
110
158
|
}
|
|
159
|
+
if (schema.enum && collector && parentModelName && fName) {
|
|
160
|
+
// Generate a synthetic enum
|
|
161
|
+
const syntheticName = `${parentModelName}_${fName}`;
|
|
162
|
+
if (!collector.usedNames.has(syntheticName)) {
|
|
163
|
+
collector.usedNames.add(syntheticName);
|
|
164
|
+
collector.enums.push({
|
|
165
|
+
name: syntheticName,
|
|
166
|
+
values: (schema.enum as string[]).map((v: string) => ({
|
|
167
|
+
value: v,
|
|
168
|
+
description: undefined,
|
|
169
|
+
})),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
kind: 'enum',
|
|
174
|
+
name: syntheticName,
|
|
175
|
+
values: schema.enum as string[],
|
|
176
|
+
} as TypeRef;
|
|
177
|
+
}
|
|
111
178
|
if (schema.enum) {
|
|
112
|
-
// Simple string enum -- represent as primitive string
|
|
179
|
+
// Simple string enum -- represent as primitive string (no collector)
|
|
113
180
|
return { kind: 'primitive', type: 'string' } as TypeRef;
|
|
114
181
|
}
|
|
115
182
|
if (schema.$ref) {
|
|
@@ -127,11 +194,39 @@ function rawSchemaToTypeRef(schema: Record<string, any>): TypeRef {
|
|
|
127
194
|
}
|
|
128
195
|
|
|
129
196
|
let ref: TypeRef;
|
|
130
|
-
if (baseType === 'object' && schema.properties) {
|
|
131
|
-
// Inline object --
|
|
197
|
+
if (baseType === 'object' && schema.properties && collector && parentModelName && fName) {
|
|
198
|
+
// Inline object -- generate a synthetic model
|
|
199
|
+
const syntheticName = `${parentModelName}_${fName}`;
|
|
200
|
+
if (!collector.usedNames.has(syntheticName)) {
|
|
201
|
+
collector.usedNames.add(syntheticName);
|
|
202
|
+
const fields: Field[] = [];
|
|
203
|
+
const requiredSet = new Set<string>(schema.required ?? []);
|
|
204
|
+
for (const [propName, propSchema] of Object.entries(schema.properties) as [string, Record<string, any>][]) {
|
|
205
|
+
fields.push({
|
|
206
|
+
name: propName,
|
|
207
|
+
type: rawSchemaToTypeRef(propSchema, syntheticName, propName, collector),
|
|
208
|
+
required: requiredSet.has(propName),
|
|
209
|
+
description: propSchema.description,
|
|
210
|
+
deprecated: propSchema.deprecated,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
collector.models.push({
|
|
214
|
+
name: syntheticName,
|
|
215
|
+
fields,
|
|
216
|
+
description: schema.description,
|
|
217
|
+
} as Model);
|
|
218
|
+
}
|
|
219
|
+
ref = { kind: 'model', name: syntheticName } as TypeRef;
|
|
220
|
+
} else if (baseType === 'object' && schema.properties) {
|
|
221
|
+
// Inline object -- treat as unknown (no collector)
|
|
132
222
|
ref = { kind: 'primitive', type: 'unknown' } as TypeRef;
|
|
133
223
|
} else if (baseType === 'array' && schema.items) {
|
|
134
|
-
|
|
224
|
+
// For array items that are inline objects, use the singular field name
|
|
225
|
+
const itemFieldName = fName ? singularizeSnake(fName) : undefined;
|
|
226
|
+
ref = {
|
|
227
|
+
kind: 'array',
|
|
228
|
+
items: rawSchemaToTypeRef(schema.items, parentModelName, itemFieldName, collector),
|
|
229
|
+
} as TypeRef;
|
|
135
230
|
} else if (baseType === 'boolean') {
|
|
136
231
|
ref = { kind: 'primitive', type: 'boolean' } as TypeRef;
|
|
137
232
|
} else if (baseType === 'integer' || baseType === 'number') {
|
|
@@ -151,13 +246,17 @@ function rawSchemaToTypeRef(schema: Record<string, any>): TypeRef {
|
|
|
151
246
|
* All fields are returned as optional (not required) since they come from
|
|
152
247
|
* oneOf variants where only one variant is active at a time.
|
|
153
248
|
*/
|
|
154
|
-
function extractFieldsFromRawSchema(
|
|
249
|
+
function extractFieldsFromRawSchema(
|
|
250
|
+
schema: Record<string, any>,
|
|
251
|
+
parentModelName?: string,
|
|
252
|
+
collector?: SyntheticCollector,
|
|
253
|
+
): Field[] {
|
|
155
254
|
const fields: Field[] = [];
|
|
156
255
|
const props = schema.properties ?? {};
|
|
157
256
|
for (const [name, propSchema] of Object.entries(props) as [string, Record<string, any>][]) {
|
|
158
257
|
fields.push({
|
|
159
258
|
name,
|
|
160
|
-
type: rawSchemaToTypeRef(propSchema),
|
|
259
|
+
type: rawSchemaToTypeRef(propSchema, parentModelName, name, collector),
|
|
161
260
|
required: false, // All oneOf variant fields are optional
|
|
162
261
|
description: propSchema.description,
|
|
163
262
|
deprecated: propSchema.deprecated,
|
|
@@ -170,14 +269,18 @@ function extractFieldsFromRawSchema(schema: Record<string, any>): Field[] {
|
|
|
170
269
|
* Recursively collect all fields from a oneOf schema, flattening nested
|
|
171
270
|
* allOf+oneOf compositions. All fields are marked optional.
|
|
172
271
|
*/
|
|
173
|
-
function collectOneOfFields(
|
|
272
|
+
function collectOneOfFields(
|
|
273
|
+
schema: Record<string, any>,
|
|
274
|
+
parentModelName?: string,
|
|
275
|
+
collector?: SyntheticCollector,
|
|
276
|
+
): Field[] {
|
|
174
277
|
const allFields: Field[] = [];
|
|
175
278
|
const seenFieldNames = new Set<string>();
|
|
176
279
|
|
|
177
280
|
function walkSchema(s: Record<string, any>): void {
|
|
178
281
|
// Direct properties
|
|
179
282
|
if (s.properties) {
|
|
180
|
-
for (const f of extractFieldsFromRawSchema(s)) {
|
|
283
|
+
for (const f of extractFieldsFromRawSchema(s, parentModelName, collector)) {
|
|
181
284
|
if (!seenFieldNames.has(f.name)) {
|
|
182
285
|
seenFieldNames.add(f.name);
|
|
183
286
|
allFields.push(f);
|
|
@@ -208,6 +311,83 @@ function collectOneOfFields(schema: Record<string, any>): Field[] {
|
|
|
208
311
|
return allFields;
|
|
209
312
|
}
|
|
210
313
|
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Array-item type upgrade
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if a TypeRef is `unknown` (the degraded type for inline objects).
|
|
320
|
+
*/
|
|
321
|
+
function isUnknownType(ref: TypeRef): boolean {
|
|
322
|
+
return ref.kind === 'primitive' && (ref as any).type === 'unknown';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* If a field is an `array<unknown>` (or `nullable<array<unknown>>`) and the
|
|
327
|
+
* raw spec defines inline object/enum items, replace the item type with a
|
|
328
|
+
* synthetic model/enum. Returns the original field unchanged when no upgrade
|
|
329
|
+
* is needed.
|
|
330
|
+
*/
|
|
331
|
+
function upgradeArrayItemType(
|
|
332
|
+
field: Field,
|
|
333
|
+
rawSchema: Record<string, any>,
|
|
334
|
+
parentModelName: string,
|
|
335
|
+
collector: SyntheticCollector,
|
|
336
|
+
): Field {
|
|
337
|
+
// Unwrap nullable to find the array
|
|
338
|
+
let arrayRef: TypeRef | null = null;
|
|
339
|
+
let isNullableWrapper = false;
|
|
340
|
+
if (field.type.kind === 'array') {
|
|
341
|
+
arrayRef = field.type;
|
|
342
|
+
} else if (field.type.kind === 'nullable' && field.type.inner.kind === 'array') {
|
|
343
|
+
arrayRef = field.type.inner;
|
|
344
|
+
isNullableWrapper = true;
|
|
345
|
+
}
|
|
346
|
+
if (!arrayRef || arrayRef.kind !== 'array') return field;
|
|
347
|
+
if (!isUnknownType(arrayRef.items)) return field;
|
|
348
|
+
|
|
349
|
+
// Look up the raw spec for this field
|
|
350
|
+
const rawProp = rawSchema.properties?.[field.name];
|
|
351
|
+
if (!rawProp) return field;
|
|
352
|
+
|
|
353
|
+
// Handle the case where the raw property is inside a nullable type array
|
|
354
|
+
let rawArraySchema = rawProp;
|
|
355
|
+
if (Array.isArray(rawProp.type)) {
|
|
356
|
+
const nonNull = rawProp.type.filter((t: string) => t !== 'null');
|
|
357
|
+
if (nonNull[0] === 'array') {
|
|
358
|
+
rawArraySchema = rawProp;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (rawArraySchema.type !== 'array' && !(Array.isArray(rawArraySchema.type) && rawArraySchema.type.includes('array')))
|
|
362
|
+
return field;
|
|
363
|
+
if (!rawArraySchema.items) return field;
|
|
364
|
+
|
|
365
|
+
// Generate a proper TypeRef from the raw items schema
|
|
366
|
+
const itemFieldName = singularizeSnake(field.name);
|
|
367
|
+
const newItemRef = rawSchemaToTypeRef(rawArraySchema.items, parentModelName, itemFieldName, collector);
|
|
368
|
+
if (isUnknownType(newItemRef)) return field;
|
|
369
|
+
|
|
370
|
+
const newArrayRef: TypeRef = { kind: 'array', items: newItemRef } as TypeRef;
|
|
371
|
+
const newType: TypeRef = isNullableWrapper ? ({ kind: 'nullable', inner: newArrayRef } as TypeRef) : newArrayRef;
|
|
372
|
+
|
|
373
|
+
return { ...field, type: newType };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Module-level store for synthetic enums produced during enrichment.
|
|
378
|
+
// Consumed by `getSyntheticEnums()` after `enrichModelsFromSpec` runs.
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
let _lastSyntheticEnums: Enum[] = [];
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Return the synthetic enums generated during the last call to
|
|
384
|
+
* `enrichModelsFromSpec`. Call this after enrichment to merge them into the
|
|
385
|
+
* enum generation phase.
|
|
386
|
+
*/
|
|
387
|
+
export function getSyntheticEnums(): Enum[] {
|
|
388
|
+
return _lastSyntheticEnums;
|
|
389
|
+
}
|
|
390
|
+
|
|
211
391
|
/**
|
|
212
392
|
* Enrich IR models by flattening oneOf/allOf+oneOf variant fields from the raw spec.
|
|
213
393
|
*
|
|
@@ -217,13 +397,24 @@ function collectOneOfFields(schema: Record<string, any>): Field[] {
|
|
|
217
397
|
* For models whose raw spec schema has allOf containing a oneOf:
|
|
218
398
|
* - Collect the missing variant fields and add them as optional.
|
|
219
399
|
*
|
|
400
|
+
* Inline objects and enums in oneOf branches are promoted to synthetic
|
|
401
|
+
* models/enums instead of degrading to `object` / `string`.
|
|
402
|
+
*
|
|
220
403
|
* Returns a new array of enriched models (original models are not mutated).
|
|
404
|
+
* Synthetic enums are stored internally; retrieve them via `getSyntheticEnums()`.
|
|
221
405
|
*/
|
|
222
406
|
export function enrichModelsFromSpec(models: Model[]): Model[] {
|
|
223
407
|
const spec = loadRawSpec();
|
|
224
|
-
if (!spec)
|
|
408
|
+
if (!spec) {
|
|
409
|
+
_lastSyntheticEnums = [];
|
|
410
|
+
return models;
|
|
411
|
+
}
|
|
225
412
|
|
|
226
|
-
|
|
413
|
+
const collector = createCollector();
|
|
414
|
+
// Avoid name collisions with existing models
|
|
415
|
+
for (const m of models) collector.usedNames.add(m.name);
|
|
416
|
+
|
|
417
|
+
const enriched = models.map((model) => {
|
|
227
418
|
const rawSchema = lookupRawSchema(model.name);
|
|
228
419
|
if (!rawSchema) return model;
|
|
229
420
|
|
|
@@ -237,8 +428,9 @@ export function enrichModelsFromSpec(models: Model[]): Model[] {
|
|
|
237
428
|
rawSchema.allOf?.some((s: any) => s.discriminator || s.oneOf?.some((v: any) => v.discriminator));
|
|
238
429
|
if (hasDiscriminator) return model;
|
|
239
430
|
|
|
240
|
-
// Collect all variant fields from the raw schema
|
|
241
|
-
|
|
431
|
+
// Collect all variant fields from the raw schema, generating synthetic
|
|
432
|
+
// models/enums for inline definitions along the way.
|
|
433
|
+
const variantFields = collectOneOfFields(rawSchema, model.name, collector);
|
|
242
434
|
if (variantFields.length === 0) return model;
|
|
243
435
|
|
|
244
436
|
// Merge variant fields into the existing model, skipping duplicates
|
|
@@ -252,4 +444,29 @@ export function enrichModelsFromSpec(models: Model[]): Model[] {
|
|
|
252
444
|
fields: [...model.fields, ...newFields],
|
|
253
445
|
};
|
|
254
446
|
});
|
|
447
|
+
|
|
448
|
+
// Second pass: fix array fields whose items degraded to `unknown` in the
|
|
449
|
+
// IR but are actually inline objects or enums in the raw spec.
|
|
450
|
+
const enriched2 = enriched.map((model) => {
|
|
451
|
+
const rawSchema = lookupRawSchema(model.name);
|
|
452
|
+
if (!rawSchema?.properties) return model;
|
|
453
|
+
|
|
454
|
+
let modified = false;
|
|
455
|
+
const newFields = model.fields.map((field) => {
|
|
456
|
+
const upgraded = upgradeArrayItemType(field, rawSchema, model.name, collector);
|
|
457
|
+
if (upgraded !== field) modified = true;
|
|
458
|
+
return upgraded;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return modified ? { ...model, fields: newFields } : model;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Convert synthetic enum collector entries to proper Enum objects
|
|
465
|
+
_lastSyntheticEnums = collector.enums.map((e) => ({
|
|
466
|
+
name: e.name,
|
|
467
|
+
values: e.values.map((v) => ({ value: v.value, description: v.description })),
|
|
468
|
+
})) as Enum[];
|
|
469
|
+
|
|
470
|
+
// Append synthetic models to the output
|
|
471
|
+
return [...enriched2, ...collector.models];
|
|
255
472
|
}
|
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Shared acronym fixes
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base set of acronym corrections applied after PascalCase conversion.
|
|
7
|
+
* These are domain-specific terms that `toPascalCase` produces as e.g.
|
|
8
|
+
* "Sso" but should be rendered as "SSO" in every SDK language.
|
|
9
|
+
*
|
|
10
|
+
* Language emitters can extend this with additional entries (e.g. Go adds
|
|
11
|
+
* API, URL, HTTP, JSON, etc. per Go naming conventions).
|
|
12
|
+
*/
|
|
13
|
+
export const BASE_ACRONYM_FIXES: readonly [RegExp, string][] = [
|
|
14
|
+
[/Workos/g, 'WorkOS'],
|
|
15
|
+
[/Sso/g, 'SSO'],
|
|
16
|
+
[/Mfa/g, 'MFA'],
|
|
17
|
+
[/Jwt/g, 'JWT'],
|
|
18
|
+
[/Cors/g, 'CORS'],
|
|
19
|
+
[/Saml/g, 'SAML'],
|
|
20
|
+
[/Scim/g, 'SCIM'],
|
|
21
|
+
[/Rbac/g, 'RBAC'],
|
|
22
|
+
[/Oauth/g, 'OAuth'],
|
|
23
|
+
[/Oidc/g, 'OIDC'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Apply acronym corrections to a PascalCase string.
|
|
28
|
+
* Uses the base set by default; pass extra entries for language-specific
|
|
29
|
+
* conventions (e.g. Go's `[/Api(?=[A-Z]|$)/g, 'API']`).
|
|
30
|
+
*/
|
|
31
|
+
export function applyAcronymFixes(s: string, extra?: readonly [RegExp, string][]): string {
|
|
32
|
+
let result = s;
|
|
33
|
+
for (const [pattern, replacement] of BASE_ACRONYM_FIXES) {
|
|
34
|
+
result = result.replace(pattern, replacement);
|
|
35
|
+
}
|
|
36
|
+
if (extra) {
|
|
37
|
+
for (const [pattern, replacement] of extra) {
|
|
38
|
+
result = result.replace(pattern, replacement);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// URN stripping
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
1
48
|
/** Strip URN OAuth grant-type prefixes from discriminator-derived schema names. */
|
|
2
49
|
export function stripUrnPrefix(name: string): string {
|
|
3
50
|
// Handles both OAuth and Oauth casing from different PascalCase implementations
|
|
@@ -42,7 +42,18 @@ export function resolveWrapperParams(wrapper: ResolvedWrapper, ctx: EmitterConte
|
|
|
42
42
|
return wrapper.exposedParams.map((paramName) => {
|
|
43
43
|
const field =
|
|
44
44
|
variantFields.find((f) => f.name === paramName || toSnakeCase(f.name) === toSnakeCase(paramName)) ?? null;
|
|
45
|
-
|
|
45
|
+
// Default to required: a wrapper exists to make a specific call shape work,
|
|
46
|
+
// and exposedParams is the contract for that shape. Mark optional only when
|
|
47
|
+
// (a) the wrapper hint explicitly says so, or (b) the IR variant model
|
|
48
|
+
// resolves and reports the field as not required.
|
|
49
|
+
let isOptional: boolean;
|
|
50
|
+
if (optionalSet.has(paramName)) {
|
|
51
|
+
isOptional = true;
|
|
52
|
+
} else if (field) {
|
|
53
|
+
isOptional = !field.required;
|
|
54
|
+
} else {
|
|
55
|
+
isOptional = false;
|
|
56
|
+
}
|
|
46
57
|
return { paramName, field, isOptional };
|
|
47
58
|
});
|
|
48
59
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateClient } from '../../src/dotnet/client.js';
|
|
3
|
+
import type { EmitterContext, ApiSpec, Service, Model } from '@workos/oagen';
|
|
4
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
5
|
+
|
|
6
|
+
const models: Model[] = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Organization',
|
|
9
|
+
fields: [
|
|
10
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
11
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const services: Service[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'Organizations',
|
|
19
|
+
operations: [
|
|
20
|
+
{
|
|
21
|
+
name: 'listOrganizations',
|
|
22
|
+
httpMethod: 'get',
|
|
23
|
+
path: '/organizations',
|
|
24
|
+
pathParams: [],
|
|
25
|
+
queryParams: [],
|
|
26
|
+
headerParams: [],
|
|
27
|
+
response: { kind: 'model', name: 'Organization' },
|
|
28
|
+
errors: [],
|
|
29
|
+
injectIdempotencyKey: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const spec: ApiSpec = {
|
|
36
|
+
name: 'TestAPI',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
baseUrl: 'https://api.workos.com',
|
|
39
|
+
services,
|
|
40
|
+
models,
|
|
41
|
+
enums: [],
|
|
42
|
+
sdk: defaultSdkBehavior(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ctx: EmitterContext = {
|
|
46
|
+
namespace: 'workos',
|
|
47
|
+
namespacePascal: 'WorkOS',
|
|
48
|
+
spec,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe('dotnet/client', () => {
|
|
52
|
+
it('generates only WorkOSClient.Generated.cs', () => {
|
|
53
|
+
const files = generateClient(spec, ctx);
|
|
54
|
+
expect(files).toHaveLength(1);
|
|
55
|
+
expect(files[0].path).toBe('Client/WorkOSClient.Generated.cs');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('generates partial class with service accessors', () => {
|
|
59
|
+
const files = generateClient(spec, ctx);
|
|
60
|
+
const content = files[0].content;
|
|
61
|
+
|
|
62
|
+
expect(content).toContain('namespace WorkOS');
|
|
63
|
+
expect(content).toContain('public partial class WorkOSClient');
|
|
64
|
+
// Lazy-initialized service property
|
|
65
|
+
expect(content).toContain('OrganizationsService');
|
|
66
|
+
expect(content).toContain('??= new OrganizationsService(this)');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not contain static HTTP infrastructure', () => {
|
|
70
|
+
const files = generateClient(spec, ctx);
|
|
71
|
+
const content = files[0].content;
|
|
72
|
+
|
|
73
|
+
// These belong in the hand-maintained WorkOSClient.cs
|
|
74
|
+
expect(content).not.toContain('HttpClient');
|
|
75
|
+
expect(content).not.toContain('ApiKey');
|
|
76
|
+
expect(content).not.toContain('SendAsync');
|
|
77
|
+
expect(content).not.toContain('RequestAsync');
|
|
78
|
+
expect(content).not.toContain('ApiBaseURL');
|
|
79
|
+
expect(content).not.toContain('AuthenticationError');
|
|
80
|
+
expect(content).not.toContain('RateLimitExceededError');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('deduplicates services by mount target', () => {
|
|
84
|
+
const multiSpec: ApiSpec = {
|
|
85
|
+
...spec,
|
|
86
|
+
services: [
|
|
87
|
+
...services,
|
|
88
|
+
{
|
|
89
|
+
name: 'OrganizationsApiKeys',
|
|
90
|
+
operations: [
|
|
91
|
+
{
|
|
92
|
+
name: 'listOrganizationApiKeys',
|
|
93
|
+
httpMethod: 'get',
|
|
94
|
+
path: '/organizations/api_keys',
|
|
95
|
+
pathParams: [],
|
|
96
|
+
queryParams: [],
|
|
97
|
+
headerParams: [],
|
|
98
|
+
response: { kind: 'model', name: 'Organization' },
|
|
99
|
+
errors: [],
|
|
100
|
+
injectIdempotencyKey: false,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const files = generateClient(multiSpec, { ...ctx, spec: multiSpec });
|
|
108
|
+
const content = files[0].content;
|
|
109
|
+
|
|
110
|
+
// Both services should appear since they have different mount targets
|
|
111
|
+
expect(content).toContain('OrganizationsService');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('generates XML doc comments on service properties', () => {
|
|
115
|
+
const files = generateClient(spec, ctx);
|
|
116
|
+
const content = files[0].content;
|
|
117
|
+
|
|
118
|
+
expect(content).toContain('/// <summary>');
|
|
119
|
+
expect(content).toContain('OrganizationsService');
|
|
120
|
+
});
|
|
121
|
+
});
|