Skip to content

Commit 8169953

Browse files
committed
Improve tests, fix additional bugs
1 parent 19d5088 commit 8169953

File tree

5 files changed

+117
-163
lines changed

5 files changed

+117
-163
lines changed

packages/openapi-typescript/src/load.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ComponentsObject, Fetch, GlobalContext, OpenAPI3, OperationObject, ParameterObject, PathItemObject, ReferenceObject, RequestBodyObject, ResponseObject, SchemaObject, Subschema } from "./types.js";
1+
import type { ComponentsObject, DiscriminatorObject, Fetch, GlobalContext, OpenAPI3, OperationObject, ParameterObject, PathItemObject, ReferenceObject, RequestBodyObject, ResponseObject, SchemaObject, Subschema } from "./types.js";
22
import fs from "node:fs";
33
import path from "node:path";
44
import { Readable } from "node:stream";
@@ -296,7 +296,19 @@ export default async function load(schema: URL | Subschema | Readable, options:
296296
walk(options.schemas[k].schema, (rawNode, nodePath) => {
297297
const node = rawNode as unknown as SchemaObject;
298298
if (!node.discriminator) return;
299-
options.discriminators[schemaID === "." ? makeTSIndex(nodePath) : makeTSIndex(["external", k, ...nodePath])] = node.discriminator;
299+
const discriminator: DiscriminatorObject = { ...node.discriminator };
300+
301+
// handle child oneOf types (mapping isn’t explicit from children)
302+
const oneOf: string[] = [];
303+
if (Array.isArray(node.oneOf)) {
304+
for (const child of node.oneOf) {
305+
if (!child || typeof child !== "object" || !("$ref" in child)) continue;
306+
oneOf.push(child.$ref);
307+
}
308+
}
309+
if (oneOf.length) discriminator.oneOf = oneOf;
310+
311+
options.discriminators[schemaID === "." ? makeTSIndex(nodePath) : makeTSIndex(["external", k, ...nodePath])] = discriminator;
300312
});
301313
}
302314
}

packages/openapi-typescript/src/transform/schema-object.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GlobalContext, ReferenceObject, SchemaObject } from "../types.js";
1+
import type { DiscriminatorObject, GlobalContext, ReferenceObject, SchemaObject } from "../types.js";
22
import { escObjKey, escStr, getEntries, getSchemaObjectComment, indent, parseRef, tsArrayOf, tsIntersectionOf, tsOmit, tsOneOf, tsOptionalProperty, tsReadonly, tsTupleOf, tsUnionOf, tsWithRequired } from "../utils.js";
33
import transformSchemaObjectMap from "./schema-object-map.js";
44

@@ -162,21 +162,24 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
162162
// core type: properties + additionalProperties
163163
const coreType: string[] = [];
164164

165-
// discriminators
165+
// discriminators: explicit mapping on schema object
166166
for (const k of ["oneOf", "allOf", "anyOf"] as ("oneOf" | "allOf" | "anyOf")[]) {
167167
if (!(schemaObject as any)[k]) continue;
168-
const discriminatorRef: ReferenceObject | undefined = (schemaObject as any)[k].find((t: SchemaObject | ReferenceObject) => "$ref" in t && ctx.discriminators[t.$ref]);
169-
if (discriminatorRef) {
170-
const discriminator = ctx.discriminators[discriminatorRef.$ref];
171-
// get the inferred propertyName value from the last section of the path (as the spec suggests to do)
172-
let value = parseRef(path).path.pop()!;
173-
// if mapping, and there’s a match, use this rather than the inferred name
174-
if (discriminator.mapping) {
175-
// Mapping value can either be a fully-qualified ref (#/components/schemas/XYZ) or a schema name (XYZ)
176-
const matchedValue = Object.entries(discriminator.mapping).find(([, v]) => (!v.startsWith("#") && v === value) || (v.startsWith("#") && parseRef(v).path.pop() === value));
177-
if (matchedValue) value = matchedValue[0]; // why was this designed backwards!?
178-
}
179-
coreType.unshift(indent(`${escObjKey(discriminator.propertyName)}: ${escStr(value)};`, indentLv + 1));
168+
const discriminatorRef: ReferenceObject | undefined = (schemaObject as any)[k].find(
169+
(t: SchemaObject | ReferenceObject) =>
170+
"$ref" in t &&
171+
(ctx.discriminators[t.$ref] || // explicit allOf from this node
172+
Object.values(ctx.discriminators).find((d) => d.oneOf?.includes(path))), // implicit oneOf from parent
173+
);
174+
if (discriminatorRef && ctx.discriminators[discriminatorRef.$ref]) {
175+
coreType.unshift(indent(getDiscriminatorPropertyName(path, ctx.discriminators[discriminatorRef.$ref]), indentLv + 1));
176+
break;
177+
}
178+
}
179+
// discriminators: implicit mapping from parent
180+
for (const d of Object.values(ctx.discriminators)) {
181+
if (d.oneOf?.includes(path)) {
182+
coreType.unshift(indent(getDiscriminatorPropertyName(path, d), indentLv + 1));
180183
break;
181184
}
182185
}
@@ -236,7 +239,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
236239
const output: string[] = [];
237240
for (const item of items) {
238241
const itemType = transformSchemaObject(item, { path, ctx: { ...ctx, indentLv } });
239-
if ("$ref" in item && ctx.discriminators[item.$ref]?.mapping) {
242+
if ("$ref" in item && ctx.discriminators[item.$ref]) {
240243
output.push(tsOmit(itemType, [ctx.discriminators[item.$ref].propertyName]));
241244
continue;
242245
}
@@ -274,3 +277,15 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
274277
// if no type could be generated, fall back to “empty object” type
275278
return ctx.emptyObjectsUnknown ? "Record<string, unknown>" : "Record<string, never>";
276279
}
280+
281+
export function getDiscriminatorPropertyName(path: string, discriminator: DiscriminatorObject): string {
282+
// get the inferred propertyName value from the last section of the path (as the spec suggests to do)
283+
let value = parseRef(path).path.pop()!;
284+
// if mapping, and there’s a match, use this rather than the inferred name
285+
if (discriminator.mapping) {
286+
// Mapping value can either be a fully-qualified ref (#/components/schemas/XYZ) or a schema name (XYZ)
287+
const matchedValue = Object.entries(discriminator.mapping).find(([, v]) => (!v.startsWith("#") && v === value) || (v.startsWith("#") && parseRef(v).path.pop() === value));
288+
if (matchedValue) value = matchedValue[0]; // why was this designed backwards!?
289+
}
290+
return `${escObjKey(discriminator.propertyName)}: ${escStr(value)};`;
291+
}

packages/openapi-typescript/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,8 @@ export interface DiscriminatorObject {
508508
propertyName: string;
509509
/** An object to hold mappings between payload values and schema names or references. */
510510
mapping?: Record<string, string>;
511+
/** If this exists, then a discriminator type should be added to objects matching this path */
512+
oneOf?: string[];
511513
}
512514

513515
/**

0 commit comments

Comments
 (0)