@workos/oagen-emitters 0.6.4 → 0.6.6

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-CZoeqixh.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-BgVrq-hM.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.4",
3
+ "version": "0.6.6",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -127,7 +127,7 @@ export const dotnetEmitter: Emitter = {
127
127
  lines.push(' public override bool CanConvert(Type objectType) => objectType == typeof(object);');
128
128
  lines.push('');
129
129
  lines.push(
130
- ' public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)',
130
+ ' public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)',
131
131
  );
132
132
  lines.push(' {');
133
133
  lines.push(' var jObject = JObject.Load(reader);');
@@ -143,7 +143,7 @@ export const dotnetEmitter: Emitter = {
143
143
  lines.push(' }');
144
144
  lines.push('');
145
145
  lines.push(
146
- ' public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)',
146
+ ' public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer)',
147
147
  );
148
148
  lines.push(' {');
149
149
  lines.push(' serializer.Serialize(writer, value);');
@@ -161,8 +161,10 @@ export const dotnetEmitter: Emitter = {
161
161
 
162
162
  // Generate converters for discriminated base models (model-level
163
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.
164
+ // ReadJson uses Populate (not Deserialize) to avoid infinite recursion
165
+ // with the [JsonConverter] attribute applied to the base class.
166
+ // CanWrite is false so serialization uses the default path and never
167
+ // re-enters WriteJson.
166
168
  for (const [baseName, disc] of modelDiscriminators) {
167
169
  const baseClass = modelClassName(baseName);
168
170
  const converterName = `${baseClass}DiscriminatorConverter`;
@@ -184,7 +186,7 @@ export const dotnetEmitter: Emitter = {
184
186
  );
185
187
  lines.push('');
186
188
  lines.push(
187
- ' public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)',
189
+ ' public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)',
188
190
  );
189
191
  lines.push(' {');
190
192
  lines.push(' var jObject = JObject.Load(reader);');
@@ -204,11 +206,15 @@ export const dotnetEmitter: Emitter = {
204
206
  lines.push(' return target;');
205
207
  lines.push(' }');
206
208
  lines.push('');
209
+ lines.push(' public override bool CanWrite => false;');
210
+ lines.push('');
207
211
  lines.push(
208
- ' public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)',
212
+ ' public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer)',
209
213
  );
210
214
  lines.push(' {');
211
- lines.push(' serializer.Serialize(writer, value);');
215
+ lines.push(
216
+ ' throw new NotImplementedException("Serialization is handled by the default serializer.");',
217
+ );
212
218
  lines.push(' }');
213
219
  lines.push(' }');
214
220
  lines.push('}');
@@ -161,10 +161,12 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
161
161
  let initializer = '';
162
162
  let setterModifier = '';
163
163
 
164
- if (constInit !== null) {
164
+ if (constInit !== null && !isOptional) {
165
165
  // Discriminator-style single-value enum/literal: emit with a const
166
166
  // initializer and a non-public setter so callers can't drift the
167
167
  // wire value. The converter still reads whatever the server sends.
168
+ // Only for required fields — optional literal fields must be nullable
169
+ // so absent keys round-trip correctly.
168
170
  csType = baseType;
169
171
  initializer = ` = ${constInit};`;
170
172
  setterModifier = 'internal ';
@@ -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
- models: Model[];
29
- enums: Enum[];
30
- services: any[];
31
- }): { path: string; content: string }[] {
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/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
 
@@ -304,11 +304,13 @@ function renderFields(fields: Field[], overrideFields: Set<string> = new Set()):
304
304
  let kotlinType: string;
305
305
  let defaultExpr: string | null = null;
306
306
 
307
- // Const literal fields: always emit a hardcoded default matching the
308
- // literal value so callers don't have to pass it.
307
+ // Const literal fields: emit a hardcoded default matching the literal
308
+ // value so callers don't have to pass it — but only when the field is
309
+ // required. Optional literal fields must default to null so that absent
310
+ // keys round-trip correctly.
309
311
  const literalDefault = literalDefaultExpr(field.type);
310
312
 
311
- if (literalDefault !== null) {
313
+ if (literalDefault !== null && field.required) {
312
314
  kotlinType = baseType;
313
315
  defaultExpr = literalDefault;
314
316
  } else if (!field.required) {
@@ -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 — wrapped in ignore markers so the merger
167
- // matches them positionally and doesn't displace them.
168
- lines.push('');
169
- lines.push('# @oagen-ignore-start non-spec service imports (hand-maintained)');
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
 
@@ -357,8 +357,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
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
360
+ if (field.type.kind === 'literal' && isRequired) {
361
+ // Required literal fields have a statically known value; use .get() with a default
362
362
  // so deserialization is resilient when the API omits the key.
363
363
  accessor = `data.get("${wireKey}", ${pythonLiteralDefault(field.type.value)})`;
364
364
  } else {