Skip to content

Commit

Permalink
Fix oneOf + empty object parent case (#1287)
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow authored Aug 9, 2023
1 parent dc6cbf9 commit 8a9d8ed
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-avocados-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": patch
---

Fix oneOf handling with empty object parent type
6 changes: 3 additions & 3 deletions packages/openapi-typescript/examples/github-api-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13959,7 +13959,7 @@ export interface components {
* Organization ruleset conditions
* @description Conditions for an organization ruleset
*/
"org-ruleset-conditions": Record<string, never> & ((components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]));
"org-ruleset-conditions": (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]);
/**
* creation
* @description Only allow users with bypass permission to create matching refs.
Expand Down Expand Up @@ -14177,7 +14177,7 @@ export interface components {
* Repository Rule
* @description A repository rule.
*/
"repository-rule": Record<string, never> & (components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"]);
"repository-rule": components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"];
/**
* Repository ruleset
* @description A set of rules to apply when specified conditions are met.
Expand Down Expand Up @@ -19126,7 +19126,7 @@ export interface components {
* Repository Rule
* @description A repository rule with ruleset details.
*/
"repository-rule-detailed": Record<string, never> & ((components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]));
"repository-rule-detailed": (components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]);
"secret-scanning-alert": {
number?: components["schemas"]["alert-number"];
created_at?: components["schemas"]["alert-created-at"];
Expand Down
6 changes: 3 additions & 3 deletions packages/openapi-typescript/examples/github-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13156,7 +13156,7 @@ export interface components {
* Organization ruleset conditions
* @description Conditions for an organization ruleset
*/
"org-ruleset-conditions": Record<string, never> & ((components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]));
"org-ruleset-conditions": (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]);
/**
* creation
* @description Only allow users with bypass permission to create matching refs.
Expand Down Expand Up @@ -13374,7 +13374,7 @@ export interface components {
* Repository Rule
* @description A repository rule.
*/
"repository-rule": Record<string, never> & (components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"]);
"repository-rule": components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"];
/**
* Repository ruleset
* @description A set of rules to apply when specified conditions are met.
Expand Down Expand Up @@ -20981,7 +20981,7 @@ export interface components {
* Repository Rule
* @description A repository rule with ruleset details.
*/
"repository-rule-detailed": Record<string, never> & ((components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]));
"repository-rule-detailed": (components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]);
"secret-scanning-alert": {
number?: components["schemas"]["alert-number"];
created_at?: components["schemas"]["alert-created-at"];
Expand Down
12 changes: 6 additions & 6 deletions packages/openapi-typescript/src/transform/schema-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
}

// oneOf (no discriminator)
const oneOf = ((typeof schemaObject === "object" && !schemaObject.discriminator && (schemaObject as any).oneOf) || schemaObject.enum || undefined) as (SchemaObject | ReferenceObject)[] | undefined; // note: for objects, treat enum as oneOf
const oneOf = ((typeof schemaObject === "object" && !schemaObject.discriminator && schemaObject.oneOf) || schemaObject.enum || undefined) as (SchemaObject | ReferenceObject)[] | undefined; // note: for objects, treat enum as oneOf
if (oneOf && !oneOf.some((t) => "$ref" in t && ctx.discriminators[t.$ref])) {
const oneOfNormalized = oneOf.map((item) => transformSchemaObject(item, { path, ctx }));
if (schemaObject.nullable) oneOfNormalized.push("null");
Expand All @@ -86,7 +86,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
const oneOfTypes = oneOfNormalized.some((t) => typeof t === "string" && t.includes("{")) ? tsOneOf(...oneOfNormalized) : tsUnionOf(...oneOfNormalized);

// handle oneOf + object type
if ("type" in schemaObject && schemaObject.type === "object") {
if ("type" in schemaObject && schemaObject.type === "object" && (schemaObject.properties || schemaObject.additionalProperties)) {
return tsIntersectionOf(transformSchemaObject({ ...schemaObject, oneOf: undefined, enum: undefined } as any, { path, ctx }), oneOfTypes);
}

Expand Down Expand Up @@ -226,19 +226,19 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
return output;
}
// oneOf (discriminator)
if ("oneOf" in schemaObject && Array.isArray(schemaObject.oneOf)) {
if (Array.isArray(schemaObject.oneOf) && schemaObject.oneOf.length) {
const oneOfType = tsUnionOf(...collectCompositions(schemaObject.oneOf));
finalType = finalType ? tsIntersectionOf(finalType, oneOfType) : oneOfType;
} else {
// allOf
if ("allOf" in schemaObject && Array.isArray(schemaObject.allOf)) {
finalType = tsIntersectionOf(...(finalType ? [finalType] : []), ...collectCompositions(schemaObject.allOf));
if (Array.isArray((schemaObject as any).allOf) && schemaObject.allOf!.length) {
finalType = tsIntersectionOf(...(finalType ? [finalType] : []), ...collectCompositions(schemaObject.allOf!));
if ("required" in schemaObject && Array.isArray(schemaObject.required)) {
finalType = tsWithRequired(finalType, schemaObject.required);
}
}
// anyOf
if ("anyOf" in schemaObject && Array.isArray(schemaObject.anyOf)) {
if (Array.isArray(schemaObject.anyOf) && schemaObject.anyOf.length) {
const anyOfTypes = tsUnionOf(...collectCompositions(schemaObject.anyOf));
finalType = finalType ? tsIntersectionOf(finalType, anyOfTypes) : anyOfTypes;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,12 @@ export type SchemaObject = {
format?: string;
/** @deprecated in 3.1 (still valid for 3.0) */
nullable?: boolean;
oneOf?: (SchemaObject | ReferenceObject)[];
allOf?: (SchemaObject | ReferenceObject)[];
anyOf?: (SchemaObject | ReferenceObject)[];
required?: string[];
[key: `x-${string}`]: any;
} & (
| { oneOf: (SchemaObject | ReferenceObject)[] }
| StringSubtype
| NumberSubtype
| IntegerSubtype
Expand All @@ -443,8 +446,6 @@ export type SchemaObject = {
| NullSubtype
| ObjectSubtype
| { type: ("string" | "number" | "integer" | "array" | "boolean" | "null" | "object")[] }
| { allOf: (SchemaObject | ReferenceObject)[]; anyOf?: (SchemaObject | ReferenceObject)[]; required?: string[] }
| { allOf?: (SchemaObject | ReferenceObject)[]; anyOf: (SchemaObject | ReferenceObject)[]; required?: string[] }
// eslint-disable-next-line @typescript-eslint/ban-types
| {}
);
Expand Down
46 changes: 44 additions & 2 deletions packages/openapi-typescript/test/schema-object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,48 @@ describe("Schema Object", () => {
expect(generated).toBe("0 | 1");
});

test("empty object + oneOf is ignored", () => {
const schema: SchemaObject = {
type: "object",
oneOf: [
{
title: "DetailsId",
type: "object",
required: ["id"],
properties: {
id: { type: "string", description: "The ID of an existing resource that exists before the pipeline is run." },
},
},
{
title: "DetailsFrom",
type: "object",
required: ["from"],
properties: {
from: {
type: "object",
description: "The stage and step to report on.",
required: ["step"],
properties: { stage: { type: "string", description: "An identifier for the stage the step being reported on resides in." }, step: { type: "string", description: "An identifier for the step to be reported on." } },
},
},
},
],
};
const generated = transformSchemaObject(schema, options);
expect(generated).toBe(`OneOf<[{
/** @description The ID of an existing resource that exists before the pipeline is run. */
id: string;
}, {
/** @description The stage and step to report on. */
from: {
/** @description An identifier for the stage the step being reported on resides in. */
stage?: string;
/** @description An identifier for the step to be reported on. */
step: string;
};
}]>`);
});

test("nullable: true", () => {
const schema: SchemaObject = { nullable: true, oneOf: [{ type: "integer" }, { type: "string" }] };
const generated = transformSchemaObject(schema, options);
Expand Down Expand Up @@ -719,8 +761,8 @@ describe("ReferenceObject", () => {
properties: { string: { type: "string" } },
"x-extension": true,
},
options
)
options,
),
).toBe("{\n string?: string;\n}");
});
});

0 comments on commit 8a9d8ed

Please sign in to comment.