diff --git a/packages/quicktype-core/src/attributes/AccessorNames.ts b/packages/quicktype-core/src/attributes/AccessorNames.ts index 6b3283cf71..554e157276 100644 --- a/packages/quicktype-core/src/attributes/AccessorNames.ts +++ b/packages/quicktype-core/src/attributes/AccessorNames.ts @@ -1,4 +1,5 @@ import { + areEqual, iterableFirst, mapFromIterable, mapFromObject, @@ -28,11 +29,38 @@ export type AccessorEntry = string | Map; export type AccessorNames = Map; +// Combines the accessor names of types that turn out to be the same type, +// such as an enum and the same enum in a union, or two enums with the same +// cases: an entry they all agree on is kept; one they disagree on is +// dropped, so that name falls back to the default. +function combineAccessorNames(arr: AccessorNames[]): AccessorNames { + const result: AccessorNames = new Map(); + const conflicting = new Set(); + for (const accessors of arr) { + for (const [key, entry] of accessors) { + if (conflicting.has(key)) continue; + const existing = result.get(key); + if (existing === undefined) { + result.set(key, entry); + } else if (!areEqual(existing, entry)) { + result.delete(key); + conflicting.add(key); + } + } + } + + return result; +} + class AccessorNamesTypeAttributeKind extends TypeAttributeKind { public constructor() { super("accessorNames"); } + public combine(arr: AccessorNames[]): AccessorNames { + return combineAccessorNames(arr); + } + public makeInferred(_: AccessorNames): undefined { return undefined; } diff --git a/packages/quicktype-core/src/attributes/EnumValues.ts b/packages/quicktype-core/src/attributes/EnumValues.ts index 9cfa0678e7..83f34deb04 100644 --- a/packages/quicktype-core/src/attributes/EnumValues.ts +++ b/packages/quicktype-core/src/attributes/EnumValues.ts @@ -1,4 +1,4 @@ -import { mapMap } from "collection-utils"; +import { areEqual, mapMap } from "collection-utils"; import type { JSONSchemaAttributes, @@ -20,6 +20,14 @@ class EnumValuesTypeAttributeKind extends TypeAttributeKind { super("enumValues"); } + // Copies of an enum's values that are all the same are kept; any other + // combination still fails, because dropping a value the copies disagree + // on could give two cases the same value. + public combine(arr: AccessorNames[]): AccessorNames | undefined { + if (arr.every((values) => areEqual(values, arr[0]))) return arr[0]; + return super.combine(arr); + } + public makeInferred(_: AccessorNames): undefined { return undefined; } diff --git a/test/inputs/schema/accessors-enum-union.1.json b/test/inputs/schema/accessors-enum-union.1.json new file mode 100644 index 0000000000..9699165a47 --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.1.json @@ -0,0 +1,4 @@ +{ + "f": "a", + "g": "b" +} diff --git a/test/inputs/schema/accessors-enum-union.2.json b/test/inputs/schema/accessors-enum-union.2.json new file mode 100644 index 0000000000..c335be1f19 --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.2.json @@ -0,0 +1,4 @@ +{ + "f": null, + "g": "a" +} diff --git a/test/inputs/schema/accessors-enum-union.3.fail.enum.json b/test/inputs/schema/accessors-enum-union.3.fail.enum.json new file mode 100644 index 0000000000..c19c5e3612 --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.3.fail.enum.json @@ -0,0 +1,4 @@ +{ + "f": "c", + "g": "a" +} diff --git a/test/inputs/schema/accessors-enum-union.schema b/test/inputs/schema/accessors-enum-union.schema new file mode 100644 index 0000000000..7f8aa0e6ce --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.schema @@ -0,0 +1,21 @@ +{ + "type": "object", + "properties": { + "f": { + "anyOf": [ + { "type": "string", "enum": ["a", "b"] }, + { "type": "null" } + ] + }, + "g": { "$ref": "#/definitions/E" } + }, + "required": ["f", "g"], + "additionalProperties": false, + "definitions": { + "E": { + "type": "string", + "enum": ["a", "b"], + "qt-accessors": { "a": "Alpha", "b": "Beta" } + } + } +} diff --git a/test/unit/enum-values-combine.test.ts b/test/unit/enum-values-combine.test.ts new file mode 100644 index 0000000000..f8124dda72 --- /dev/null +++ b/test/unit/enum-values-combine.test.ts @@ -0,0 +1,48 @@ +import { InputData, JSONSchemaInput, quicktype } from "quicktype-core"; +import { describe, expect, test } from "vitest"; + +// Two definitions of the same enum are one type, so their `qt-enum-values` +// are combined. +async function cJSONOutput(fValues: Record): Promise { + const schema = JSON.stringify({ + definitions: { + E: { + type: "string", + enum: ["a", "b"], + "qt-enum-values": { a: "0", b: "1" }, + }, + F: { + type: "string", + enum: ["a", "b"], + "qt-enum-values": fValues, + }, + }, + }); + const schemaInput = new JSONSchemaInput(undefined); + for (const name of ["E", "F"]) { + await schemaInput.addSource({ + name, + schema, + uris: [`#/definitions/${name}`], + }); + } + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ inputData, lang: "cjson" }); + return result.lines.join("\n"); +} + +describe("enum values of the same enum", () => { + test("combine when every copy is the same", async () => { + const output = await cJSONOutput({ b: "1", a: "0" }); + expect(output).toContain("_A = 0,"); + expect(output).toContain("_B = 1,"); + }); + + test("still fail when the copies differ", async () => { + await expect(cJSONOutput({ a: "0", b: "2" })).rejects.toThrow( + "Cannot combine type attribute enumValues", + ); + }); +});