@workos/oagen-emitters 0.6.3 → 0.6.4
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 +9 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-DgjQSh2G.mjs → plugin-CZoeqixh.mjs} +131 -10
- package/dist/plugin-CZoeqixh.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +1 -1
- package/src/dotnet/index.ts +92 -5
- package/src/dotnet/models.ts +62 -3
- package/src/kotlin/index.ts +16 -1
- package/src/php/models.ts +13 -0
- package/src/python/models.ts +16 -1
- package/src/ruby/client.ts +2 -0
- package/dist/plugin-DgjQSh2G.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-CZoeqixh.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
package/src/dotnet/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
|
|
14
|
-
import { generateModels, primeModelAliases } from './models.js';
|
|
14
|
+
import { generateModels, primeModelAliases, type DiscriminatorContext } from './models.js';
|
|
15
15
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
16
16
|
import { generateEnums, primeEnumAliases } from './enums.js';
|
|
17
17
|
import { generateResources } from './resources.js';
|
|
@@ -21,6 +21,7 @@ import { buildOperationsMap } from './manifest.js';
|
|
|
21
21
|
import { generateWrapperOptionsClasses } from './wrappers.js';
|
|
22
22
|
import { groupByMount } from '../shared/resolved-ops.js';
|
|
23
23
|
import { discriminatedUnions } from './type-map.js';
|
|
24
|
+
import { modelClassName } from './naming.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Fix the namespace for C#. The CLI passes `--namespace workos` which gives
|
|
@@ -77,9 +78,36 @@ export const dotnetEmitter: Emitter = {
|
|
|
77
78
|
if (synEnumsForModels.length > 0) {
|
|
78
79
|
primeEnumAliases([...c.spec.enums, ...synEnumsForModels]);
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
|
|
82
|
+
// Restore fields on discriminated base models. enrichModelsFromSpec clears
|
|
83
|
+
// fields for dispatcher-capable languages; C# uses inheritance instead:
|
|
84
|
+
// the base class keeps its common fields and variant classes extend it.
|
|
85
|
+
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
86
|
+
const discriminatorBases = new Set<string>();
|
|
87
|
+
const variantToBase = new Map<string, string>();
|
|
88
|
+
const modelDiscriminators = new Map<string, { property: string; mapping: Record<string, string> }>();
|
|
89
|
+
|
|
90
|
+
const dotnetModels = enriched.map((m) => {
|
|
91
|
+
const disc = (m as any).discriminator;
|
|
92
|
+
if (disc && m.fields.length === 0) {
|
|
93
|
+
const original = originalByName.get(m.name);
|
|
94
|
+
if (original && original.fields.length > 0) {
|
|
95
|
+
discriminatorBases.add(m.name);
|
|
96
|
+
modelDiscriminators.set(m.name, disc);
|
|
97
|
+
for (const variantName of Object.values(disc.mapping) as string[]) {
|
|
98
|
+
variantToBase.set(variantName, m.name);
|
|
99
|
+
}
|
|
100
|
+
return { ...m, fields: original.fields };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return m;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const discCtx: DiscriminatorContext = { discriminatorBases, variantToBase };
|
|
107
|
+
const files = generateModels(dotnetModels, c, discCtx);
|
|
81
108
|
|
|
82
109
|
// Generate discriminator converters for oneOf unions with discriminator
|
|
110
|
+
// (union-type references, e.g. a field typed as oneOf with discriminator)
|
|
83
111
|
if (discriminatedUnions.size > 0) {
|
|
84
112
|
for (const [baseName, disc] of discriminatedUnions) {
|
|
85
113
|
const converterName = `${baseName}DiscriminatorConverter`;
|
|
@@ -87,8 +115,6 @@ export const dotnetEmitter: Emitter = {
|
|
|
87
115
|
lines.push(`namespace ${c.namespacePascal}`);
|
|
88
116
|
lines.push('{');
|
|
89
117
|
lines.push(' using System;');
|
|
90
|
-
lines.push(' using System.Text.Json;');
|
|
91
|
-
lines.push(' using System.Text.Json.Serialization;');
|
|
92
118
|
lines.push(' using Newtonsoft.Json;');
|
|
93
119
|
lines.push(' using Newtonsoft.Json.Linq;');
|
|
94
120
|
lines.push('');
|
|
@@ -109,7 +135,7 @@ export const dotnetEmitter: Emitter = {
|
|
|
109
135
|
lines.push(' switch (discriminatorValue)');
|
|
110
136
|
lines.push(' {');
|
|
111
137
|
for (const [value, modelName] of Object.entries(disc.mapping)) {
|
|
112
|
-
const csName = modelName
|
|
138
|
+
const csName = modelClassName(modelName);
|
|
113
139
|
lines.push(` case "${value}": return jObject.ToObject<${csName}>(serializer);`);
|
|
114
140
|
}
|
|
115
141
|
lines.push(' default: return jObject.ToObject<object>(serializer);');
|
|
@@ -133,6 +159,67 @@ export const dotnetEmitter: Emitter = {
|
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
|
|
162
|
+
// Generate converters for discriminated base models (model-level
|
|
163
|
+
// discriminators detected by enrichModelsFromSpec, e.g. EventSchema).
|
|
164
|
+
// Uses Populate to avoid infinite recursion with the [JsonConverter]
|
|
165
|
+
// attribute applied to the base class.
|
|
166
|
+
for (const [baseName, disc] of modelDiscriminators) {
|
|
167
|
+
const baseClass = modelClassName(baseName);
|
|
168
|
+
const converterName = `${baseClass}DiscriminatorConverter`;
|
|
169
|
+
const lines: string[] = [];
|
|
170
|
+
lines.push(`namespace ${c.namespacePascal}`);
|
|
171
|
+
lines.push('{');
|
|
172
|
+
lines.push(' using System;');
|
|
173
|
+
lines.push(' using Newtonsoft.Json;');
|
|
174
|
+
lines.push(' using Newtonsoft.Json.Linq;');
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push(` /// <summary>`);
|
|
177
|
+
lines.push(` /// JSON converter that deserializes <see cref="${baseClass}"/> into the`);
|
|
178
|
+
lines.push(` /// correct variant subclass based on the "${disc.property}" property.`);
|
|
179
|
+
lines.push(` /// </summary>`);
|
|
180
|
+
lines.push(` public class ${converterName} : Newtonsoft.Json.JsonConverter`);
|
|
181
|
+
lines.push(' {');
|
|
182
|
+
lines.push(
|
|
183
|
+
` public override bool CanConvert(Type objectType) => typeof(${baseClass}).IsAssignableFrom(objectType);`,
|
|
184
|
+
);
|
|
185
|
+
lines.push('');
|
|
186
|
+
lines.push(
|
|
187
|
+
' public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)',
|
|
188
|
+
);
|
|
189
|
+
lines.push(' {');
|
|
190
|
+
lines.push(' var jObject = JObject.Load(reader);');
|
|
191
|
+
lines.push(` var discriminatorValue = jObject["${disc.property}"]?.ToString();`);
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push(' object target;');
|
|
194
|
+
lines.push(' switch (discriminatorValue)');
|
|
195
|
+
lines.push(' {');
|
|
196
|
+
for (const [value, variantModelName] of Object.entries(disc.mapping)) {
|
|
197
|
+
const csName = modelClassName(variantModelName);
|
|
198
|
+
lines.push(` case "${value}": target = new ${csName}(); break;`);
|
|
199
|
+
}
|
|
200
|
+
lines.push(` default: target = new ${baseClass}(); break;`);
|
|
201
|
+
lines.push(' }');
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push(' serializer.Populate(jObject.CreateReader(), target);');
|
|
204
|
+
lines.push(' return target;');
|
|
205
|
+
lines.push(' }');
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push(
|
|
208
|
+
' public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)',
|
|
209
|
+
);
|
|
210
|
+
lines.push(' {');
|
|
211
|
+
lines.push(' serializer.Serialize(writer, value);');
|
|
212
|
+
lines.push(' }');
|
|
213
|
+
lines.push(' }');
|
|
214
|
+
lines.push('}');
|
|
215
|
+
|
|
216
|
+
files.push({
|
|
217
|
+
path: `Client/Utilities/${converterName}.cs`,
|
|
218
|
+
content: lines.join('\n'),
|
|
219
|
+
overwriteExisting: true,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
136
223
|
return prefixSourcePaths(ensureTrailingNewlines(files));
|
|
137
224
|
},
|
|
138
225
|
|
package/src/dotnet/models.ts
CHANGED
|
@@ -21,12 +21,24 @@ import {
|
|
|
21
21
|
import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
|
|
22
22
|
export { isListWrapperModel, isListMetadataModel };
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Context for discriminated union inheritance in generated models.
|
|
26
|
+
* When present, base models get a [JsonConverter] attribute and variant
|
|
27
|
+
* models extend the base class, inheriting common fields.
|
|
28
|
+
*/
|
|
29
|
+
export interface DiscriminatorContext {
|
|
30
|
+
/** Model names that are discriminated union bases. */
|
|
31
|
+
discriminatorBases: Set<string>;
|
|
32
|
+
/** Maps variant model name → base model name. */
|
|
33
|
+
variantToBase: Map<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
/**
|
|
25
37
|
* Generate C# model classes from IR Models.
|
|
26
38
|
* Each model becomes a separate .cs file under Services/{mount}/Entities/.
|
|
27
39
|
* For initial generation, all models go into a flat Entities/ directory.
|
|
28
40
|
*/
|
|
29
|
-
export function generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
41
|
+
export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: DiscriminatorContext): GeneratedFile[] {
|
|
30
42
|
if (models.length === 0) return [];
|
|
31
43
|
|
|
32
44
|
// Build a lookup from enum name → single wire value for 1-value enums so
|
|
@@ -44,6 +56,21 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
44
56
|
// Compute and publish model aliases so mapTypeRef rewrites references.
|
|
45
57
|
primeModelAliases(models);
|
|
46
58
|
|
|
59
|
+
// Build a lookup of base model field C# names → C# types for inheritance.
|
|
60
|
+
// Variant models skip inherited fields and use `new` for type-divergent ones.
|
|
61
|
+
const baseFieldLookup = new Map<string, Map<string, string>>();
|
|
62
|
+
if (discCtx) {
|
|
63
|
+
for (const model of models) {
|
|
64
|
+
if (discCtx.discriminatorBases.has(model.name)) {
|
|
65
|
+
const fieldMap = new Map<string, string>();
|
|
66
|
+
for (const field of model.fields) {
|
|
67
|
+
fieldMap.set(fieldName(field.name), mapTypeRef(field.type));
|
|
68
|
+
}
|
|
69
|
+
baseFieldLookup.set(model.name, fieldMap);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
for (const model of models) {
|
|
48
75
|
if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
|
|
49
76
|
|
|
@@ -81,7 +108,23 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
81
108
|
lines.push(` /// <summary>Represents ${articleFor(human)} ${human}.</summary>`);
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
|
|
111
|
+
// Discriminated union base: add JsonConverter so the deserializer dispatches
|
|
112
|
+
// to the correct variant subclass.
|
|
113
|
+
const isDiscBase = discCtx?.discriminatorBases.has(model.name) ?? false;
|
|
114
|
+
if (isDiscBase) {
|
|
115
|
+
lines.push(` [Newtonsoft.Json.JsonConverter(typeof(${csClassName}DiscriminatorConverter))]`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Variant: extend the base class to inherit common fields.
|
|
119
|
+
const baseName = discCtx?.variantToBase.get(model.name);
|
|
120
|
+
const baseClassName = baseName ? modelClassName(baseName) : null;
|
|
121
|
+
const baseFields = baseName ? baseFieldLookup.get(baseName) : undefined;
|
|
122
|
+
|
|
123
|
+
if (baseClassName) {
|
|
124
|
+
lines.push(` public class ${csClassName} : ${baseClassName}`);
|
|
125
|
+
} else {
|
|
126
|
+
lines.push(` public class ${csClassName}`);
|
|
127
|
+
}
|
|
85
128
|
lines.push(' {');
|
|
86
129
|
|
|
87
130
|
// Track Dictionary<string, object> fields so we can emit a typed
|
|
@@ -95,6 +138,21 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
95
138
|
if (seenFieldNames.has(csFieldName)) continue;
|
|
96
139
|
seenFieldNames.add(csFieldName);
|
|
97
140
|
|
|
141
|
+
// Inheritance: if this variant extends a base class, check each field
|
|
142
|
+
// against the base. Same C# type → skip (inherited). Different C# type
|
|
143
|
+
// → emit with `new` keyword so the variant has its own typed property.
|
|
144
|
+
let useNewModifier = false;
|
|
145
|
+
if (baseFields) {
|
|
146
|
+
const baseType = baseFields.get(csFieldName);
|
|
147
|
+
if (baseType !== undefined) {
|
|
148
|
+
const variantType = mapTypeRef(field.type);
|
|
149
|
+
if (baseType === variantType) {
|
|
150
|
+
continue; // Inherited from base — skip
|
|
151
|
+
}
|
|
152
|
+
useNewModifier = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
const isOptional = !field.required;
|
|
99
157
|
const baseType = mapTypeRef(field.type);
|
|
100
158
|
const isAlreadyNullable = baseType.endsWith('?');
|
|
@@ -141,7 +199,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
141
199
|
|
|
142
200
|
const isRequiredEnum = field.required && isEnumRef(field.type) && constInit === null;
|
|
143
201
|
lines.push(...emitJsonPropertyAttributes(field.name, { isRequiredEnum }));
|
|
144
|
-
|
|
202
|
+
const newMod = useNewModifier ? 'new ' : '';
|
|
203
|
+
lines.push(` public ${newMod}${csType} ${csFieldName} { get; ${setterModifier}set; }${initializer}`);
|
|
145
204
|
|
|
146
205
|
// Track additional-properties / metadata dictionaries for typed accessors.
|
|
147
206
|
// Skip deprecated fields so the generated accessor doesn't reference
|
package/src/kotlin/index.ts
CHANGED
|
@@ -34,7 +34,22 @@ export const kotlinEmitter: Emitter = {
|
|
|
34
34
|
|
|
35
35
|
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
36
36
|
const enriched = enrichModelsFromSpec(models);
|
|
37
|
-
|
|
37
|
+
// Kotlin uses sealed interfaces (WorkOSEvent) for event dispatch rather
|
|
38
|
+
// than class inheritance, so discriminated base models like EventSchema
|
|
39
|
+
// need their fields restored to be emitted as proper data classes.
|
|
40
|
+
// enrichModelsFromSpec clears fields for dispatcher-capable languages;
|
|
41
|
+
// restore the originals here so Kotlin emits a full data class.
|
|
42
|
+
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
43
|
+
const kotlinModels = enriched.map((m) => {
|
|
44
|
+
if ((m as any).discriminator && m.fields.length === 0) {
|
|
45
|
+
const original = originalByName.get(m.name);
|
|
46
|
+
if (original && original.fields.length > 0) {
|
|
47
|
+
return { ...m, fields: original.fields };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return m;
|
|
51
|
+
});
|
|
52
|
+
return ensureTrailingNewlines(generateModels(kotlinModels, ctx));
|
|
38
53
|
},
|
|
39
54
|
|
|
40
55
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
package/src/php/models.ts
CHANGED
|
@@ -150,6 +150,11 @@ function generateFromArrayAccessor(ref: TypeRef, wireName: string, required: boo
|
|
|
150
150
|
}
|
|
151
151
|
return `$data['${wireName}'] ?? null`;
|
|
152
152
|
}
|
|
153
|
+
// Literal fields have a statically known value; use ?? with a default
|
|
154
|
+
// so deserialization is resilient when the API omits the key.
|
|
155
|
+
if (ref.kind === 'literal') {
|
|
156
|
+
return `$data['${wireName}'] ?? ${phpLiteralDefault(ref.value)}`;
|
|
157
|
+
}
|
|
153
158
|
// Required field: access directly
|
|
154
159
|
return generateFromArrayValue(ref, `$data['${wireName}']`);
|
|
155
160
|
}
|
|
@@ -198,6 +203,14 @@ function generateFromArrayValue(ref: TypeRef, accessor: string): string {
|
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
|
|
206
|
+
/** Convert a LiteralType value to a PHP default expression for use with ??. */
|
|
207
|
+
function phpLiteralDefault(value: string | number | boolean | null): string {
|
|
208
|
+
if (value === null) return 'null';
|
|
209
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
210
|
+
if (typeof value === 'string') return `'${value}'`;
|
|
211
|
+
return String(value);
|
|
212
|
+
}
|
|
213
|
+
|
|
201
214
|
/**
|
|
202
215
|
* Check if a TypeRef needs special handling (not a simple key access).
|
|
203
216
|
*/
|
package/src/python/models.ts
CHANGED
|
@@ -356,7 +356,14 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
356
356
|
const pyFieldName = fieldName(field.name);
|
|
357
357
|
const wireKey = field.name; // Wire keys are snake_case from the spec
|
|
358
358
|
const isRequired = !isOptionalField(model.name, field, ctx);
|
|
359
|
-
|
|
359
|
+
let accessor: string;
|
|
360
|
+
if (field.type.kind === 'literal') {
|
|
361
|
+
// Literal fields have a statically known value; use .get() with a default
|
|
362
|
+
// so deserialization is resilient when the API omits the key.
|
|
363
|
+
accessor = `data.get("${wireKey}", ${pythonLiteralDefault(field.type.value)})`;
|
|
364
|
+
} else {
|
|
365
|
+
accessor = isRequired ? `data["${wireKey}"]` : `data.get("${wireKey}")`;
|
|
366
|
+
}
|
|
360
367
|
// For deserialization expressions, nullable types must always handle None
|
|
361
368
|
// even when the field itself is required (the key must be present, but value can be null).
|
|
362
369
|
const deserRequired = isRequired && field.type.kind !== 'nullable';
|
|
@@ -632,6 +639,14 @@ function isOptionalField(modelName: string, field: Model['fields'][number], ctx:
|
|
|
632
639
|
return false;
|
|
633
640
|
}
|
|
634
641
|
|
|
642
|
+
/** Convert a LiteralType value to a Python default expression for use in data.get(). */
|
|
643
|
+
function pythonLiteralDefault(value: string | number | boolean | null): string {
|
|
644
|
+
if (value === null) return 'None';
|
|
645
|
+
if (typeof value === 'boolean') return value ? 'True' : 'False';
|
|
646
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
647
|
+
return String(value);
|
|
648
|
+
}
|
|
649
|
+
|
|
635
650
|
function resolveModelFieldType(ref: any): string {
|
|
636
651
|
// Handle nullable datetime: return Optional[datetime] to preserve nullable wrapper
|
|
637
652
|
if (ref.kind === 'nullable' && isDateTimeType(ref.inner)) {
|
package/src/ruby/client.ts
CHANGED
|
@@ -162,9 +162,11 @@ function generateMainEntryFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
|
|
|
162
162
|
}
|
|
163
163
|
lines.push(`loader.ignore("#{__dir__}/workos/errors.rb")`);
|
|
164
164
|
lines.push(`loader.ignore("#{__dir__}/workos/inflections.rb")`);
|
|
165
|
+
lines.push(`loader.ignore("#{__dir__}/workos/configuration.rb")`);
|
|
165
166
|
lines.push('loader.setup');
|
|
166
167
|
lines.push('');
|
|
167
168
|
lines.push(`require 'workos/errors'`);
|
|
169
|
+
lines.push(`require 'workos/configuration'`);
|
|
168
170
|
|
|
169
171
|
return {
|
|
170
172
|
path: 'lib/workos.rb',
|