@workos/oagen-emitters 0.6.2 → 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/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as workosEmittersPlugin } from "./plugin-DgjQSh2G.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-CZoeqixh.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -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
- const files = generateModels(enriched, c);
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.replace(/([a-z])([A-Z])/g, '$1$2');
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
 
@@ -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
- lines.push(` public class ${csClassName}`);
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
- lines.push(` public ${csType} ${csFieldName} { get; ${setterModifier}set; }${initializer}`);
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
@@ -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
- return ensureTrailingNewlines(generateModels(enriched, ctx));
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
  */
@@ -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
- const accessor = isRequired ? `data["${wireKey}"]` : `data.get("${wireKey}")`;
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)) {
@@ -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',