Skip to content

Commit cdb2792

Browse files
authored
Bugfix for discriminators using allOf (#1578)
* Fixed base class discriminators to only be handled in allOfs * Improved Windows compatibility for update:examples script * Support multiple discriminator values pointing to the same refs in allOf composition * Updated examples for improved discriminator allOf support * Added jsdoc for new patchDiscriminatorEnum function * Readded composition test for oneOf inside an object as its own unit test --------- Co-authored-by: mat <[email protected]>
1 parent 9da96cd commit cdb2792

File tree

6 files changed

+294
-117
lines changed

6 files changed

+294
-117
lines changed

packages/openapi-typescript/examples/digital-ocean-api.ts

+30-14
Original file line numberDiff line numberDiff line change
@@ -9881,18 +9881,26 @@ export interface components {
98819881
*/
98829882
type: "assign" | "unassign";
98839883
};
9884-
floating_ip_action_assign: {
9885-
type: "assign";
9886-
} & (Omit<components["schemas"]["floatingIPsAction"], "type"> & {
9884+
floating_ip_action_assign: Omit<components["schemas"]["floatingIPsAction"], "type"> & {
98879885
/**
98889886
* @description The ID of the Droplet that the floating IP will be assigned to.
98899887
* @example 758604968
98909888
*/
98919889
droplet_id: number;
9892-
});
9893-
floating_ip_action_unassign: {
9890+
} & {
9891+
/**
9892+
* @description discriminator enum property added by openapi-typescript
9893+
* @enum {string}
9894+
*/
9895+
type: "assign";
9896+
};
9897+
floating_ip_action_unassign: Omit<components["schemas"]["floatingIPsAction"], "type"> & Record<string, never> & {
9898+
/**
9899+
* @description discriminator enum property added by openapi-typescript
9900+
* @enum {string}
9901+
*/
98949902
type: "unassign";
9895-
} & (Omit<components["schemas"]["floatingIPsAction"], "type"> & Record<string, never>);
9903+
};
98969904
namespace_info: {
98979905
/**
98989906
* @description The namespace's API hostname. Each function in a namespace is provided an endpoint at the namespace's hostname.
@@ -11555,18 +11563,26 @@ export interface components {
1155511563
*/
1155611564
type: "assign" | "unassign";
1155711565
};
11558-
reserved_ip_action_assign: {
11559-
type: "assign";
11560-
} & (Omit<components["schemas"]["reserved_ip_action_type"], "type"> & {
11566+
reserved_ip_action_assign: Omit<components["schemas"]["reserved_ip_action_type"], "type"> & {
1156111567
/**
1156211568
* @description The ID of the Droplet that the reserved IP will be assigned to.
1156311569
* @example 758604968
1156411570
*/
1156511571
droplet_id: number;
11566-
});
11567-
reserved_ip_action_unassign: {
11572+
} & {
11573+
/**
11574+
* @description discriminator enum property added by openapi-typescript
11575+
* @enum {string}
11576+
*/
11577+
type: "assign";
11578+
};
11579+
reserved_ip_action_unassign: Omit<components["schemas"]["reserved_ip_action_type"], "type"> & Record<string, never> & {
11580+
/**
11581+
* @description discriminator enum property added by openapi-typescript
11582+
* @enum {string}
11583+
*/
1156811584
type: "unassign";
11569-
} & (Omit<components["schemas"]["reserved_ip_action_type"], "type"> & Record<string, never>);
11585+
};
1157011586
snapshots: {
1157111587
/**
1157211588
* @description The unique identifier for the snapshot.
@@ -19323,7 +19339,7 @@ export interface operations {
1932319339
* */
1932419340
requestBody?: {
1932519341
content: {
19326-
"application/json": Omit<components["schemas"]["floating_ip_action_unassign"], "type"> | Omit<components["schemas"]["floating_ip_action_assign"], "type">;
19342+
"application/json": components["schemas"]["floating_ip_action_unassign"] | components["schemas"]["floating_ip_action_assign"];
1932719343
};
1932819344
};
1932919345
responses: {
@@ -22367,7 +22383,7 @@ export interface operations {
2236722383
* */
2236822384
requestBody?: {
2236922385
content: {
22370-
"application/json": Omit<components["schemas"]["reserved_ip_action_unassign"], "type"> | Omit<components["schemas"]["reserved_ip_action_assign"], "type">;
22386+
"application/json": components["schemas"]["reserved_ip_action_unassign"] | components["schemas"]["reserved_ip_action_assign"];
2237122387
};
2237222388
};
2237322389
responses: {

packages/openapi-typescript/scripts/update-examples.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,31 @@ async function generateSchemas() {
1515
const updateSchema = async (name: string, ext: string) => {
1616
const start = performance.now();
1717

18-
await execa(
19-
"./bin/cli.js",
20-
[`./examples/${name}${ext}`, "-o", `./examples/${name}.ts`],
21-
{ cwd },
22-
);
23-
24-
schemasDoneCount++;
25-
const timeMs = Math.round(performance.now() - start);
26-
27-
console.log(
28-
`✔︎ [${schemasDoneCount}/${schemaTotalCount}] Updated ${name} (${timeMs}ms)`,
29-
);
18+
try {
19+
await execa(
20+
"./bin/cli.js",
21+
[`./examples/${name}${ext}`, "-o", `./examples/${name}.ts`],
22+
{
23+
cwd:
24+
process.platform === "win32"
25+
? // execa/cross-spawn can not handle URL objects on Windows, so convert it to string and cut away the protocol
26+
cwd.toString().slice("file:///".length)
27+
: cwd,
28+
},
29+
);
30+
31+
schemasDoneCount++;
32+
const timeMs = Math.round(performance.now() - start);
33+
34+
console.log(
35+
`✔︎ [${schemasDoneCount}/${schemaTotalCount}] Updated ${name} (${timeMs}ms)`,
36+
);
37+
} catch (error) {
38+
console.error(
39+
`✘ [${schemasDoneCount}/${schemaTotalCount}] Failed to update ${name}`,
40+
{ error: error instanceof Error ? error.message : error },
41+
);
42+
}
3043
};
3144

3245
console.log("Updating examples...");

packages/openapi-typescript/src/lib/utils.ts

+110-58
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,68 @@ function createDiscriminatorEnum(
184184
};
185185
}
186186

187+
/** Adds or replaces the discriminator enum with the passed `values` in a schema defined by `ref` */
188+
function patchDiscriminatorEnum(
189+
schema: SchemaObject,
190+
ref: string,
191+
values: string[],
192+
discriminator: DiscriminatorObject,
193+
discriminatorRef: string,
194+
options: OpenAPITSOptions,
195+
): boolean {
196+
const resolvedSchema = resolveRef<SchemaObject>(schema, ref, {
197+
silent: options.silent ?? false,
198+
});
199+
200+
if (resolvedSchema?.allOf) {
201+
// if the schema is an allOf, we can append a new schema object to the allOf array
202+
resolvedSchema.allOf.push({
203+
type: "object",
204+
// discriminator enum properties always need to be required
205+
required: [discriminator.propertyName],
206+
properties: {
207+
[discriminator.propertyName]: createDiscriminatorEnum(values),
208+
},
209+
});
210+
211+
return true;
212+
} else if (
213+
typeof resolvedSchema === "object" &&
214+
"type" in resolvedSchema &&
215+
resolvedSchema.type === "object"
216+
) {
217+
// if the schema is an object, we can apply the discriminator enums to its properties
218+
if (!resolvedSchema.properties) {
219+
resolvedSchema.properties = {};
220+
}
221+
222+
// discriminator enum properties always need to be required
223+
if (!resolvedSchema.required) {
224+
resolvedSchema.required = [discriminator.propertyName];
225+
} else if (!resolvedSchema.required.includes(discriminator.propertyName)) {
226+
resolvedSchema.required.push(discriminator.propertyName);
227+
}
228+
229+
// add/replace the discriminator enum property
230+
resolvedSchema.properties[discriminator.propertyName] =
231+
createDiscriminatorEnum(
232+
values,
233+
resolvedSchema.properties[discriminator.propertyName],
234+
);
235+
236+
return true;
237+
}
238+
239+
warn(
240+
`Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${ref} => ${values.join(
241+
", ",
242+
)} (Discriminator: ${discriminatorRef})`,
243+
options.silent,
244+
);
245+
246+
return false;
247+
}
248+
187249
type InternalDiscriminatorMapping = Record<
188250
string,
189251
{ inferred?: string; defined?: string[] }
@@ -267,57 +329,18 @@ export function scanDiscriminators(
267329
// the inferred enum values from the schema might not represent the actual enum values of the discriminator,
268330
// so if we have defined values, use them instead
269331
const mappedValues = defined ?? [inferred!];
270-
const resolvedSchema = resolveRef<SchemaObject>(schema, mappedRef, {
271-
silent: options.silent ?? false,
272-
});
273-
274-
if (resolvedSchema?.allOf) {
275-
// if the schema is an allOf, we can append a new schema object to the allOf array
276-
resolvedSchema.allOf.push({
277-
type: "object",
278-
// discriminator enum properties always need to be required
279-
required: [discriminator.propertyName],
280-
properties: {
281-
[discriminator.propertyName]: createDiscriminatorEnum(mappedValues),
282-
},
283-
});
284332

285-
refsHandled.push(mappedRef);
286-
} else if (
287-
typeof resolvedSchema === "object" &&
288-
"type" in resolvedSchema &&
289-
resolvedSchema.type === "object"
333+
if (
334+
patchDiscriminatorEnum(
335+
schema,
336+
mappedRef,
337+
mappedValues,
338+
discriminator,
339+
ref,
340+
options,
341+
)
290342
) {
291-
// if the schema is an object, we can apply the discriminator enums to its properties
292-
if (!resolvedSchema.properties) {
293-
resolvedSchema.properties = {};
294-
}
295-
296-
// discriminator enum properties always need to be required
297-
if (!resolvedSchema.required) {
298-
resolvedSchema.required = [discriminator.propertyName];
299-
} else if (
300-
!resolvedSchema.required.includes(discriminator.propertyName)
301-
) {
302-
resolvedSchema.required.push(discriminator.propertyName);
303-
}
304-
305-
// add/replace the discriminator enum property
306-
resolvedSchema.properties[discriminator.propertyName] =
307-
createDiscriminatorEnum(
308-
mappedValues,
309-
resolvedSchema.properties[discriminator.propertyName],
310-
);
311-
312343
refsHandled.push(mappedRef);
313-
} else {
314-
warn(
315-
`Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${mappedRef} => ${mappedValues.join(
316-
", ",
317-
)} (Discriminator: ${ref})`,
318-
options.silent,
319-
);
320-
continue;
321344
}
322345
}
323346
});
@@ -326,19 +349,48 @@ export function scanDiscriminators(
326349
// (sometimes this mapping is implicit, so it can’t be done until we know
327350
// about every discriminator in the document)
328351
walk(schema, (obj, path) => {
329-
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
330-
if (obj && Array.isArray(obj[key])) {
331-
for (const item of (obj as any)[key]) {
332-
if ("$ref" in item) {
333-
if (objects[item.$ref]) {
334-
objects[createRef(path)] = {
335-
...objects[item.$ref],
336-
};
352+
if (!obj || !Array.isArray(obj.allOf)) {
353+
return;
354+
}
355+
356+
for (const item of (obj as any).allOf) {
357+
if ("$ref" in item) {
358+
if (!objects[item.$ref]) {
359+
return;
360+
}
361+
362+
const ref = createRef(path);
363+
const discriminator = objects[item.$ref];
364+
const mappedValues: string[] = [];
365+
366+
if (discriminator.mapping) {
367+
for (const mappedValue in discriminator.mapping) {
368+
if (discriminator.mapping[mappedValue] === ref) {
369+
mappedValues.push(mappedValue);
370+
}
371+
}
372+
373+
if (mappedValues.length > 0) {
374+
if (
375+
patchDiscriminatorEnum(
376+
schema,
377+
ref,
378+
mappedValues,
379+
discriminator,
380+
item.$ref,
381+
options,
382+
)
383+
) {
384+
refsHandled.push(ref);
337385
}
338-
} else if (item.discriminator?.propertyName) {
339-
objects[createRef(path)] = { ...item.discriminator };
340386
}
341387
}
388+
389+
objects[ref] = {
390+
...objects[item.$ref],
391+
};
392+
} else if (item.discriminator?.propertyName) {
393+
objects[createRef(path)] = { ...item.discriminator };
342394
}
343395
}
344396
});

0 commit comments

Comments
 (0)