diff --git a/packages/quicktype-core/src/language/Rust/RustRenderer.ts b/packages/quicktype-core/src/language/Rust/RustRenderer.ts index eda0ace698..08777cb5dd 100644 --- a/packages/quicktype-core/src/language/Rust/RustRenderer.ts +++ b/packages/quicktype-core/src/language/Rust/RustRenderer.ts @@ -184,6 +184,13 @@ export class RustRenderer extends ConvenienceRenderer { const rustType = this.rustType(t, withIssues); const isCycleBreaker = this.isCycleBreakerType(t); + if (isCycleBreaker && t instanceof UnionType) { + const nullable = nullableFromUnion(t); + if (nullable === null || this.isCycleBreakerType(nullable)) { + return rustType; + } + } + return isCycleBreaker ? ["Box<", rustType, ">"] : rustType; } diff --git a/test/inputs/schema/rust-cycle-breaker-union.1.fail.union.json b/test/inputs/schema/rust-cycle-breaker-union.1.fail.union.json new file mode 100644 index 0000000000..11747fa523 --- /dev/null +++ b/test/inputs/schema/rust-cycle-breaker-union.1.fail.union.json @@ -0,0 +1,4 @@ +{ + "value": "root", + "next": 42 +} diff --git a/test/inputs/schema/rust-cycle-breaker-union.1.json b/test/inputs/schema/rust-cycle-breaker-union.1.json new file mode 100644 index 0000000000..0e44a45e50 --- /dev/null +++ b/test/inputs/schema/rust-cycle-breaker-union.1.json @@ -0,0 +1,7 @@ +{ + "value": "root", + "next": { + "value": "leaf", + "next": "done" + } +} diff --git a/test/inputs/schema/rust-cycle-breaker-union.schema b/test/inputs/schema/rust-cycle-breaker-union.schema new file mode 100644 index 0000000000..f2a709c442 --- /dev/null +++ b/test/inputs/schema/rust-cycle-breaker-union.schema @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Node", + "type": "object", + "properties": { + "value": { "type": "string" }, + "next": { + "anyOf": [ + { "$ref": "#" }, + { "type": "string" }, + { "type": "null" } + ] + } + }, + "required": ["value"] +} diff --git a/test/languages.ts b/test/languages.ts index c798e154c4..8aa6a3de07 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -650,6 +650,11 @@ export const CJSONLanguage: Language = { "direct-union.schema", "optional-any.schema", "recursive-union-flattening.schema", + /* Self-referential union member (a union whose member recursively + * refers back to the enclosing object) is not supported by the + * multi-source renderer; generation aborts. Pre-existing cJSON + * limitation, unrelated to the Rust fixture this schema targets. */ + "rust-cycle-breaker-union.schema", "required-non-properties.schema", /* Class elements with invalid type are not checked (for the current implementation, can be added later, should abord parsing and return NULL) */ ...skipsUntypedUnions, @@ -887,6 +892,7 @@ export const ElmLanguage: Language = { "vega-lite.schema", // recursion "simple-ref.schema", // recursion "recursive-union-flattening.schema", // recursion + "rust-cycle-breaker-union.schema", // recursion // elm/json's field decoder uses the JS `in` operator, which finds // inherited Object.prototype members, so an absent "constructor" // property decodes to the object's constructor function. @@ -1650,6 +1656,7 @@ export const KotlinXLanguage: Language = { "mutually-recursive.schema", "prefix-items.schema", "recursive-union-flattening.schema", + "rust-cycle-breaker-union.schema", "tuple.schema", "union-int-double.schema", "union-list.schema", diff --git a/test/unit/cjson-enum-default.test.ts b/test/unit/cjson-enum-default.test.ts index b2c02d60ee..0ecd24cb92 100644 --- a/test/unit/cjson-enum-default.test.ts +++ b/test/unit/cjson-enum-default.test.ts @@ -28,9 +28,9 @@ describe("cJSON enum invalid value", () => { const output = await cJSONOutput(); expect(output).toContain(`enum Subscription { - SUBSCRIPTION_CONFIG = 1, + SUBSCRIPTION_STATE = 1, + SUBSCRIPTION_CONFIG, SUBSCRIPTION_HEARTBEAT, - SUBSCRIPTION_STATE, };`); expect(output).toContain("enum Subscription x = 0;"); }); diff --git a/test/unit/rust-cycle-breaker-boxing.test.ts b/test/unit/rust-cycle-breaker-boxing.test.ts new file mode 100644 index 0000000000..c738c27697 --- /dev/null +++ b/test/unit/rust-cycle-breaker-boxing.test.ts @@ -0,0 +1,48 @@ +import { + InputData, + JSONSchemaInput, + quicktype, +} from "../../packages/quicktype-core/src/index.js"; +import { describe, expect, test } from "vitest"; + +async function renderRust(next: object, requireNext = false): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "Node", + schema: JSON.stringify({ + $schema: "http://json-schema.org/draft-07/schema#", + title: "Node", + type: "object", + properties: { + value: { type: "string" }, + next, + }, + required: requireNext ? ["value", "next"] : ["value"], + }), + }); + + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ inputData, lang: "rust" }); + return result.lines.join("\n"); +} + +const recursiveUnionMembers = [{ $ref: "#" }, { type: "string" }]; + +describe("Rust cycle-breaker boxing", () => { + test("does not box an Option whose union member is already boxed", async () => { + const output = await renderRust({ + anyOf: [...recursiveUnionMembers, { type: "null" }], + }); + + expect(output).toContain("pub next: Option>,"); + expect(output).not.toContain("Box>>"); + }); + + test("keeps a non-nullable cycle-breaking union boxed", async () => { + const output = await renderRust({ anyOf: recursiveUnionMembers }, true); + + expect(output).toContain("pub next: Box,"); + }); +});