From c76510cc46cb5c38b289ba28663a42293f8276f5 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 17:47:54 -0400 Subject: [PATCH] fix(rust): don't panic when there are no top-level types (#1629) Rust's emitLeadingComments() assumed at least one top-level type exists to name in its example-usage comment, panicking with "Defined value expected, but got undefined" whenever this.topLevels was empty. This is legitimate, reachable state: TypeScript sources without a #TopLevel marker (the common case) fall back to uris: ["#/definitions/"], producing a non-empty type graph with zero named top levels. Now the renderer simply skips the top-level-specific example comment when there is nothing to name. Co-Authored-By: gpt-5.6-sol via pi --- .../src/language/Rust/RustRenderer.ts | 8 ++-- test/unit/rust-empty-top-levels.test.ts | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 test/unit/rust-empty-top-levels.test.ts diff --git a/packages/quicktype-core/src/language/Rust/RustRenderer.ts b/packages/quicktype-core/src/language/Rust/RustRenderer.ts index 3024d97d5b..4b1f4b1c36 100644 --- a/packages/quicktype-core/src/language/Rust/RustRenderer.ts +++ b/packages/quicktype-core/src/language/Rust/RustRenderer.ts @@ -13,7 +13,6 @@ import type { Name, Namer } from "../../Naming.js"; import type { RenderContext } from "../../Renderer.js"; import type { OptionValues } from "../../RendererOptions/index.js"; import { type Sourcelike, maybeAnnotated } from "../../Source.js"; -import { defined } from "../../support/Support.js"; import type { TargetLanguage } from "../../TargetLanguage.js"; import { type ClassType, @@ -351,9 +350,10 @@ export class RustRenderer extends ConvenienceRenderer { return; } - const topLevelName = defined( - mapFirst(this.topLevels), - ).getCombinedName(); + const topLevel = mapFirst(this.topLevels); + if (topLevel === undefined) return; + + const topLevelName = topLevel.getCombinedName(); this.emitMultiline( `// Example code that deserializes and serializes the model. // extern crate serde; diff --git a/test/unit/rust-empty-top-levels.test.ts b/test/unit/rust-empty-top-levels.test.ts new file mode 100644 index 0000000000..f5cc68f3fb --- /dev/null +++ b/test/unit/rust-empty-top-levels.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "vitest"; + +import { + InputData, + JSONSchemaInput, + quicktype, +} from "../../packages/quicktype-core/src/index.js"; + +async function renderTypeScriptSchema(definitions: object): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "", + schema: JSON.stringify({ definitions }), + uris: ["#/definitions/"], + isConverted: true, + }); + + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ inputData, lang: "rust" }); + return result.lines.join("\n"); +} + +// TypeScript inputs without a #TopLevel marker use #/definitions/ as their +// source URI. If the compiler did not produce any definitions, the renderer +// receives no top levels and must still emit Rust without crashing. +test("Rust renders TypeScript schemas with no top levels", async () => { + const output = await renderTypeScriptSchema({}); + + expect(output).toContain("use serde::{Serialize, Deserialize};"); +}); + +test("Rust still renders named TypeScript schema definitions", async () => { + const output = await renderTypeScriptSchema({ + Person: { + type: "object", + properties: { age: { type: "integer" } }, + required: ["age"], + }, + }); + + expect(output).toContain("pub struct Person"); +});