diff --git a/src/schema-parser/base-schema-parsers/complex.ts b/src/schema-parser/base-schema-parsers/complex.ts index 8767d084..f066c24e 100644 --- a/src/schema-parser/base-schema-parsers/complex.ts +++ b/src/schema-parser/base-schema-parsers/complex.ts @@ -18,6 +18,14 @@ export class ComplexSchemaParser extends MonoSchemaParser { complexType ](this.schema); + // A $ref alongside `not`, `allOf` and friends is a sibling in OpenAPI 3.1: the + // keywords apply on top of the referenced schema. Parsing the reference too keeps + // the type, which would otherwise be lost with the complex keyword that cannot be + // expressed in TypeScript. + const shouldParseSimpleSchema = + this.schemaUtils.getInternalSchemaType(simpleSchema) === + SCHEMA_TYPES.OBJECT || this.schemaUtils.isRefSchema(simpleSchema); + return { ...(typeof this.schema === "object" ? this.schema : {}), $schemaPath: this.schemaPath.slice(), @@ -33,19 +41,21 @@ export class ComplexSchemaParser extends MonoSchemaParser { ), content: this.config.Ts.IntersectionType( - compact([ - this.config.Ts.ExpressionGroup(complexSchemaContent), - this.schemaUtils.getInternalSchemaType(simpleSchema) === - SCHEMA_TYPES.OBJECT && - this.config.Ts.ExpressionGroup( - this.schemaParserFabric - .createSchemaParser({ - schema: simpleSchema, - schemaPath: this.schemaPath, - }) - .getInlineParseContent(), - ), - ]), + this.schemaUtils + .filterSchemaContents( + compact([ + complexSchemaContent, + shouldParseSimpleSchema && + this.schemaParserFabric + .createSchemaParser({ + schema: simpleSchema, + schemaPath: this.schemaPath, + }) + .getInlineParseContent(), + ]), + (content) => content !== this.config.Ts.Keyword.Any, + ) + .map((content) => this.config.Ts.ExpressionGroup(content)), ) || this.config.Ts.Keyword.Any, }; } diff --git a/tests/spec/ref-with-sibling-keywords/__snapshots__/basic.test.ts.snap b/tests/spec/ref-with-sibling-keywords/__snapshots__/basic.test.ts.snap new file mode 100644 index 00000000..5c061d2e --- /dev/null +++ b/tests/spec/ref-with-sibling-keywords/__snapshots__/basic.test.ts.snap @@ -0,0 +1,36 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`basic > ref-with-sibling-keywords 1`] = ` +"/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +export enum Status { + STATUS_UNSPECIFIED = "STATUS_UNSPECIFIED", + ACTIVE = "ACTIVE", + PAUSED = "PAUSED", +} + +export interface Account { + plainRef?: Status; + /** + * annotatedRef + * a reference carrying annotations + */ + annotatedRef?: Status; + /** + * constrainedRef + * a reference the value must match, minus one member + */ + constrainedRef?: Status; +} +" +`; diff --git a/tests/spec/ref-with-sibling-keywords/basic.test.ts b/tests/spec/ref-with-sibling-keywords/basic.test.ts new file mode 100644 index 00000000..7c261437 --- /dev/null +++ b/tests/spec/ref-with-sibling-keywords/basic.test.ts @@ -0,0 +1,33 @@ +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { generateApi } from "../../../src/index.js"; + +describe("basic", async () => { + let tmpdir = ""; + + beforeAll(async () => { + tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); + }); + + afterAll(async () => { + await fs.rm(tmpdir, { recursive: true }); + }); + + test("ref-with-sibling-keywords", async () => { + await generateApi({ + fileName: "schema", + input: path.resolve(import.meta.dirname, "schema.json"), + output: tmpdir, + silent: true, + generateClient: false, + }); + + const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { + encoding: "utf8", + }); + + expect(content).toMatchSnapshot(); + }); +}); diff --git a/tests/spec/ref-with-sibling-keywords/schema.json b/tests/spec/ref-with-sibling-keywords/schema.json new file mode 100644 index 00000000..dd0439cf --- /dev/null +++ b/tests/spec/ref-with-sibling-keywords/schema.json @@ -0,0 +1,37 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Ref with sibling keywords", + "version": "1.0.0" + }, + "components": { + "schemas": { + "Status": { + "type": "string", + "enum": ["STATUS_UNSPECIFIED", "ACTIVE", "PAUSED"] + }, + "Account": { + "type": "object", + "properties": { + "plainRef": { + "$ref": "#/components/schemas/Status" + }, + "annotatedRef": { + "$ref": "#/components/schemas/Status", + "title": "annotatedRef", + "description": "a reference carrying annotations" + }, + "constrainedRef": { + "$ref": "#/components/schemas/Status", + "title": "constrainedRef", + "description": "a reference the value must match, minus one member", + "not": { + "enum": ["STATUS_UNSPECIFIED"] + } + } + } + } + } + }, + "paths": {} +}