@workos/oagen-emitters 0.6.5 → 0.6.7
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 +17 -0
- package/dist/index.mjs +1 -1
- package/dist/{plugin-BV_wDWDO.mjs → plugin-Bk0xWTQC.mjs} +58 -17
- package/dist/plugin-Bk0xWTQC.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +1 -1
- package/src/dotnet/index.ts +17 -4
- package/src/dotnet/models.ts +24 -0
- package/src/go/fixtures.ts +6 -7
- package/src/go/models.ts +18 -1
- package/src/go/tests.ts +15 -4
- package/src/python/client.ts +7 -5
- package/test/dotnet/models.test.ts +77 -0
- package/dist/plugin-BV_wDWDO.mjs.map +0 -1
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as workosEmittersPlugin } from "./plugin-
|
|
1
|
+
import { t as workosEmittersPlugin } from "./plugin-Bk0xWTQC.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
package/src/dotnet/index.ts
CHANGED
|
@@ -103,7 +103,14 @@ export const dotnetEmitter: Emitter = {
|
|
|
103
103
|
return m;
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
// Build a map of base model name → discriminator wire-property name so the
|
|
107
|
+
// model emitter can mark the discriminator field as internal-set.
|
|
108
|
+
const discriminatorProperties = new Map<string, string>();
|
|
109
|
+
for (const [baseName, disc] of modelDiscriminators) {
|
|
110
|
+
discriminatorProperties.set(baseName, disc.property);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const discCtx: DiscriminatorContext = { discriminatorBases, variantToBase, discriminatorProperties };
|
|
107
114
|
const files = generateModels(dotnetModels, c, discCtx);
|
|
108
115
|
|
|
109
116
|
// Generate discriminator converters for oneOf unions with discriminator
|
|
@@ -161,8 +168,10 @@ export const dotnetEmitter: Emitter = {
|
|
|
161
168
|
|
|
162
169
|
// Generate converters for discriminated base models (model-level
|
|
163
170
|
// discriminators detected by enrichModelsFromSpec, e.g. EventSchema).
|
|
164
|
-
//
|
|
165
|
-
// attribute applied to the base class.
|
|
171
|
+
// ReadJson uses Populate (not Deserialize) to avoid infinite recursion
|
|
172
|
+
// with the [JsonConverter] attribute applied to the base class.
|
|
173
|
+
// CanWrite is false so serialization uses the default path and never
|
|
174
|
+
// re-enters WriteJson.
|
|
166
175
|
for (const [baseName, disc] of modelDiscriminators) {
|
|
167
176
|
const baseClass = modelClassName(baseName);
|
|
168
177
|
const converterName = `${baseClass}DiscriminatorConverter`;
|
|
@@ -179,6 +188,8 @@ export const dotnetEmitter: Emitter = {
|
|
|
179
188
|
lines.push(` /// </summary>`);
|
|
180
189
|
lines.push(` public class ${converterName} : Newtonsoft.Json.JsonConverter`);
|
|
181
190
|
lines.push(' {');
|
|
191
|
+
lines.push(' public override bool CanWrite => false;');
|
|
192
|
+
lines.push('');
|
|
182
193
|
lines.push(
|
|
183
194
|
` public override bool CanConvert(Type objectType) => typeof(${baseClass}).IsAssignableFrom(objectType);`,
|
|
184
195
|
);
|
|
@@ -208,7 +219,9 @@ export const dotnetEmitter: Emitter = {
|
|
|
208
219
|
' public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer)',
|
|
209
220
|
);
|
|
210
221
|
lines.push(' {');
|
|
211
|
-
lines.push(
|
|
222
|
+
lines.push(
|
|
223
|
+
' throw new NotImplementedException("Serialization is handled by the default serializer.");',
|
|
224
|
+
);
|
|
212
225
|
lines.push(' }');
|
|
213
226
|
lines.push(' }');
|
|
214
227
|
lines.push('}');
|
package/src/dotnet/models.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface DiscriminatorContext {
|
|
|
31
31
|
discriminatorBases: Set<string>;
|
|
32
32
|
/** Maps variant model name → base model name. */
|
|
33
33
|
variantToBase: Map<string, string>;
|
|
34
|
+
/** Maps base model name → wire name of the discriminator property. */
|
|
35
|
+
discriminatorProperties?: Map<string, string>;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -161,6 +163,12 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
161
163
|
let initializer = '';
|
|
162
164
|
let setterModifier = '';
|
|
163
165
|
|
|
166
|
+
// On a discriminated union base, the discriminator property (e.g. "event")
|
|
167
|
+
// should be non-public-settable even though it lacks a single const value
|
|
168
|
+
// (each variant has a different value). Consumers must never mutate it.
|
|
169
|
+
const discProp = isDiscBase ? discCtx?.discriminatorProperties?.get(model.name) : undefined;
|
|
170
|
+
const isDiscriminatorField = discProp !== undefined && field.name === discProp;
|
|
171
|
+
|
|
164
172
|
if (constInit !== null && !isOptional) {
|
|
165
173
|
// Discriminator-style single-value enum/literal: emit with a const
|
|
166
174
|
// initializer and a non-public setter so callers can't drift the
|
|
@@ -170,6 +178,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
170
178
|
csType = baseType;
|
|
171
179
|
initializer = ` = ${constInit};`;
|
|
172
180
|
setterModifier = 'internal ';
|
|
181
|
+
} else if (isDiscriminatorField) {
|
|
182
|
+
// Discriminator property on the base class: varies per variant but
|
|
183
|
+
// should still be non-public-settable so consumers can't change it.
|
|
184
|
+
csType = baseType;
|
|
185
|
+
if (!isAlreadyNullable && !isValueTypeRef(field.type)) {
|
|
186
|
+
initializer = ' = default!;';
|
|
187
|
+
}
|
|
188
|
+
setterModifier = 'internal ';
|
|
173
189
|
} else if (isOptional) {
|
|
174
190
|
if (isAlreadyNullable) {
|
|
175
191
|
csType = baseType;
|
|
@@ -219,6 +235,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
219
235
|
lines.push(` /// <paramref name="key"/> coerced to <typeparamref name="T"/>, or the default`);
|
|
220
236
|
lines.push(` /// value when the key is missing or the value is not convertible.`);
|
|
221
237
|
lines.push(` /// </summary>`);
|
|
238
|
+
if (isDiscBase) {
|
|
239
|
+
lines.push(` /// <remarks>`);
|
|
240
|
+
lines.push(` /// Variant subclasses provide strongly-typed <c>${dict.csName}</c> properties that`);
|
|
241
|
+
lines.push(` /// shadow this dictionary. This accessor is intended for forward-compatible handling`);
|
|
242
|
+
lines.push(` /// of types not yet known to this SDK version. For recognized types, cast to the`);
|
|
243
|
+
lines.push(` /// specific subclass and access its typed <c>${dict.csName}</c> property directly.`);
|
|
244
|
+
lines.push(` /// </remarks>`);
|
|
245
|
+
}
|
|
222
246
|
lines.push(` /// <typeparam name="T">Expected value type.</typeparam>`);
|
|
223
247
|
lines.push(` /// <param name="key">The key to look up.</param>`);
|
|
224
248
|
lines.push(` public T? Get${dict.csName}Attribute<T>(string key)`);
|
package/src/go/fixtures.ts
CHANGED
|
@@ -24,12 +24,11 @@ export const ID_PREFIXES: Record<string, string> = {
|
|
|
24
24
|
/**
|
|
25
25
|
* Generate JSON fixture files for test data.
|
|
26
26
|
*/
|
|
27
|
-
export function generateFixtures(spec: {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (spec.models.length === 0) return [];
|
|
27
|
+
export function generateFixtures(spec: { models: Model[]; enums: Enum[]; services: any[] }): {
|
|
28
|
+
files: { path: string; content: string }[];
|
|
29
|
+
pathRewrites: Map<string, string>;
|
|
30
|
+
} {
|
|
31
|
+
if (spec.models.length === 0) return { files: [], pathRewrites: new Map() };
|
|
33
32
|
|
|
34
33
|
const modelMap = new Map(spec.models.map((m) => [m.name, m]));
|
|
35
34
|
const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
|
|
@@ -96,7 +95,7 @@ export function generateFixtures(spec: {
|
|
|
96
95
|
// Remove duplicate files (they'll reference the canonical)
|
|
97
96
|
const deduped = files.filter((f) => !pathRewrites.has(f.path));
|
|
98
97
|
|
|
99
|
-
return deduped;
|
|
98
|
+
return { files: deduped, pathRewrites };
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
function unwrapListModel(model: Model, modelMap: Map<string, Model>): Model | null {
|
package/src/go/models.ts
CHANGED
|
@@ -166,10 +166,27 @@ function makeOptional(goType: string): string {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function structuralHash(model: Model): string {
|
|
169
|
-
|
|
169
|
+
const fieldHash = model.fields
|
|
170
170
|
.map((f) => `${f.name}:${JSON.stringify(f.type)}:${f.required}`)
|
|
171
171
|
.sort()
|
|
172
172
|
.join('|');
|
|
173
|
+
// Include entity domain for CRUD-prefixed models to prevent cross-domain
|
|
174
|
+
// aliasing (e.g. UpdateGroup vs UpdateAuthorizationPermission have identical
|
|
175
|
+
// fields but belong to different API domains and should stay separate types).
|
|
176
|
+
const domain = crudEntityDomain(model.name);
|
|
177
|
+
return domain ? `${domain}::${fieldHash}` : fieldHash;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const CRUD_PREFIXES = ['Create', 'Update', 'Delete', 'Get', 'List'];
|
|
181
|
+
|
|
182
|
+
/** Strip CRUD verb prefix to get the entity name, or null if no prefix matches. */
|
|
183
|
+
function crudEntityDomain(name: string): string | null {
|
|
184
|
+
for (const prefix of CRUD_PREFIXES) {
|
|
185
|
+
if (name.startsWith(prefix) && name.length > prefix.length) {
|
|
186
|
+
return name.slice(prefix.length);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
/** Known acronyms to preserve as single tokens during humanization. */
|
package/src/go/tests.ts
CHANGED
|
@@ -72,7 +72,7 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
// Generate fixture JSON files
|
|
75
|
-
const fixtures = generateFixtures(spec);
|
|
75
|
+
const { files: fixtures, pathRewrites: fixtureRewrites } = generateFixtures(spec);
|
|
76
76
|
for (const fixture of fixtures) {
|
|
77
77
|
files.push({
|
|
78
78
|
path: fixture.path,
|
|
@@ -97,7 +97,7 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
97
97
|
for (const { name: mountName, operations } of testEntries) {
|
|
98
98
|
if (operations.length === 0) continue;
|
|
99
99
|
const mergedService: Service = { name: mountName, operations };
|
|
100
|
-
const testFile = generateServiceTest(mergedService, spec, ctx, accessPaths);
|
|
100
|
+
const testFile = generateServiceTest(mergedService, spec, ctx, accessPaths, fixtureRewrites);
|
|
101
101
|
if (testFile) files.push(testFile);
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -109,6 +109,7 @@ function generateServiceTest(
|
|
|
109
109
|
spec: ApiSpec,
|
|
110
110
|
ctx: EmitterContext,
|
|
111
111
|
_accessPaths: Map<string, string>,
|
|
112
|
+
fixtureRewrites: Map<string, string>,
|
|
112
113
|
): GeneratedFile | null {
|
|
113
114
|
if (service.operations.length === 0) return null;
|
|
114
115
|
|
|
@@ -213,6 +214,10 @@ function generateServiceTest(
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
fixturePath = `testdata/list_${fileName(resolved.name)}.json`;
|
|
217
|
+
// If this fixture was deduplicated, use the canonical path
|
|
218
|
+
if (fixtureRewrites.has(fixturePath)) {
|
|
219
|
+
fixturePath = fixtureRewrites.get(fixturePath)!;
|
|
220
|
+
}
|
|
216
221
|
}
|
|
217
222
|
}
|
|
218
223
|
|
|
@@ -302,7 +307,10 @@ function generateServiceTest(
|
|
|
302
307
|
// Success test
|
|
303
308
|
const respModel = plan.responseModelName;
|
|
304
309
|
const isArrayResponse = !isPaginated && op.response?.kind === 'array';
|
|
305
|
-
|
|
310
|
+
let fixturePath = `testdata/${fileName(respModel)}.json`;
|
|
311
|
+
if (fixtureRewrites.has(fixturePath)) {
|
|
312
|
+
fixturePath = fixtureRewrites.get(fixturePath)!;
|
|
313
|
+
}
|
|
306
314
|
const expectedPath = buildExpectedPath(op);
|
|
307
315
|
|
|
308
316
|
const httpMethodUpper = op.httpMethod.toUpperCase();
|
|
@@ -406,7 +414,10 @@ function generateServiceTest(
|
|
|
406
414
|
const wrapperParamsStruct = paramsStructName(resolvedName, wrapperMethod);
|
|
407
415
|
const responseType = wrapper.responseModelName;
|
|
408
416
|
const testName = `Test${accessorName}_${wrapperMethod}`;
|
|
409
|
-
|
|
417
|
+
let fixturePath = responseType ? `testdata/${fileName(responseType)}.json` : null;
|
|
418
|
+
if (fixturePath && fixtureRewrites.has(fixturePath)) {
|
|
419
|
+
fixturePath = fixtureRewrites.get(fixturePath)!;
|
|
420
|
+
}
|
|
410
421
|
|
|
411
422
|
const wrapperCallArgs: string[] = ['context.Background()'];
|
|
412
423
|
for (const p of sortPathParamsByTemplateOrder(op)) {
|
package/src/python/client.ts
CHANGED
|
@@ -163,15 +163,17 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
163
163
|
lines.push(importLine);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
// Non-spec service imports —
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
// Non-spec service imports — emitted as plain imports (not wrapped in
|
|
167
|
+
// ignore markers). The overwriteWithPreservedRegions() machinery in oagen
|
|
168
|
+
// relocates top-level ignore blocks to EOF because they have no containing
|
|
169
|
+
// class, stripping the markers from the imports while spliceExtraImports()
|
|
170
|
+
// preserves the bare import lines. Emitting them directly avoids the
|
|
171
|
+
// displacement entirely; spliceExtraImports() will preserve any additional
|
|
172
|
+
// hand-added imports from the existing file on subsequent regenerations.
|
|
170
173
|
for (const s of NON_SPEC_SERVICES) {
|
|
171
174
|
const w = PYTHON_NON_SPEC_WIRING[s.id];
|
|
172
175
|
if (w) lines.push(w.importLine);
|
|
173
176
|
}
|
|
174
|
-
lines.push('# @oagen-ignore-end');
|
|
175
177
|
lines.push('');
|
|
176
178
|
lines.push('');
|
|
177
179
|
|
|
@@ -255,4 +255,81 @@ describe('dotnet/models', () => {
|
|
|
255
255
|
expect(orgFile).toBeDefined();
|
|
256
256
|
expect(orgFile.content).toContain('List<OrganizationDomain>');
|
|
257
257
|
});
|
|
258
|
+
|
|
259
|
+
it('emits internal set on discriminator property of base class', () => {
|
|
260
|
+
const models: Model[] = [
|
|
261
|
+
{
|
|
262
|
+
name: 'EventSchema',
|
|
263
|
+
fields: [
|
|
264
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
265
|
+
{ name: 'event', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
266
|
+
{
|
|
267
|
+
name: 'data',
|
|
268
|
+
type: { kind: 'map', valueType: { kind: 'primitive', type: 'unknown' } },
|
|
269
|
+
required: true,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'UserCreated',
|
|
275
|
+
fields: [
|
|
276
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
277
|
+
{ name: 'event', type: { kind: 'literal', value: 'user.created' }, required: true },
|
|
278
|
+
{ name: 'data', type: { kind: 'model', name: 'UserCreatedData' }, required: true },
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'UserCreatedData',
|
|
283
|
+
fields: [{ name: 'user_id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
primeEnumAliases([]);
|
|
288
|
+
const discCtx = {
|
|
289
|
+
discriminatorBases: new Set(['EventSchema']),
|
|
290
|
+
variantToBase: new Map([['UserCreated', 'EventSchema']]),
|
|
291
|
+
discriminatorProperties: new Map([['EventSchema', 'event']]),
|
|
292
|
+
};
|
|
293
|
+
const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } }, discCtx);
|
|
294
|
+
|
|
295
|
+
const baseFile = files.find((f) => f.path.includes('EventSchema.cs'))!;
|
|
296
|
+
expect(baseFile).toBeDefined();
|
|
297
|
+
|
|
298
|
+
// The discriminator property "event" should have internal set
|
|
299
|
+
expect(baseFile.content).toContain('Event { get; internal set; }');
|
|
300
|
+
// Non-discriminator required fields should NOT have internal set
|
|
301
|
+
expect(baseFile.content).toContain('Id { get; set; }');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('adds remarks to dictionary accessors on discriminator base class', () => {
|
|
305
|
+
const models: Model[] = [
|
|
306
|
+
{
|
|
307
|
+
name: 'EventSchema',
|
|
308
|
+
fields: [
|
|
309
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
310
|
+
{ name: 'event', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
311
|
+
{
|
|
312
|
+
name: 'data',
|
|
313
|
+
type: { kind: 'map', valueType: { kind: 'primitive', type: 'unknown' } },
|
|
314
|
+
required: true,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
primeEnumAliases([]);
|
|
321
|
+
const discCtx = {
|
|
322
|
+
discriminatorBases: new Set(['EventSchema']),
|
|
323
|
+
variantToBase: new Map<string, string>(),
|
|
324
|
+
discriminatorProperties: new Map([['EventSchema', 'event']]),
|
|
325
|
+
};
|
|
326
|
+
const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } }, discCtx);
|
|
327
|
+
|
|
328
|
+
const baseFile = files.find((f) => f.path.includes('EventSchema.cs'))!;
|
|
329
|
+
expect(baseFile).toBeDefined();
|
|
330
|
+
|
|
331
|
+
// Dictionary accessors on discriminator bases should have a remarks note
|
|
332
|
+
expect(baseFile.content).toContain('/// <remarks>');
|
|
333
|
+
expect(baseFile.content).toContain('forward-compatible');
|
|
334
|
+
});
|
|
258
335
|
});
|