diff --git a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts index 0df4b758f2..c69107ebd9 100644 --- a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts @@ -68,8 +68,10 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { private readonly _options: OptionValues, ) { super(targetLanguage, renderContext, _options); - this._needHelpers = _options.features.helpers; - this._needAttributes = _options.features.attributes; + // `--just-types` wins over whatever `--features` says. + this._needHelpers = _options.features.helpers && !_options.justTypes; + this._needAttributes = + _options.features.attributes && !_options.justTypes; this._needNamespaces = _options.features.namespaces; } diff --git a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts index 8eb22b14b6..34204577b4 100644 --- a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts @@ -69,8 +69,10 @@ export class SystemTextJsonCSharpRenderer extends CSharpRenderer { >, ) { super(targetLanguage, renderContext, _options); - this._needHelpers = _options.features.helpers; - this._needAttributes = _options.features.attributes; + // `--just-types` wins over whatever `--features` says. + this._needHelpers = _options.features.helpers && !_options.justTypes; + this._needAttributes = + _options.features.attributes && !_options.justTypes; this._needNamespaces = _options.features.namespaces; } diff --git a/packages/quicktype-core/src/language/CSharp/language.ts b/packages/quicktype-core/src/language/CSharp/language.ts index d07785dd4a..0b0c7235f5 100644 --- a/packages/quicktype-core/src/language/CSharp/language.ts +++ b/packages/quicktype-core/src/language/CSharp/language.ts @@ -104,19 +104,10 @@ export const cSharpOptions = { helpers: false, attributes: true, }, - "just-types-and-namespace": { - namespaces: true, - helpers: false, - attributes: false, - }, - "just-types": { - namespaces: true, - helpers: false, - attributes: false, - }, } as const, "complete", ), + justTypes: new BooleanOption("just-types", "Plain types only", false), baseclass: new EnumOption( "base-class", "Base class", diff --git a/packages/quicktype-core/src/language/Kotlin/language.ts b/packages/quicktype-core/src/language/Kotlin/language.ts index c434958149..f806d423f9 100644 --- a/packages/quicktype-core/src/language/Kotlin/language.ts +++ b/packages/quicktype-core/src/language/Kotlin/language.ts @@ -1,6 +1,7 @@ import type { ConvenienceRenderer } from "../../ConvenienceRenderer.js"; import type { RenderContext } from "../../Renderer.js"; import { + BooleanOption, EnumOption, StringOption, getOptionValues, @@ -16,11 +17,11 @@ import { KotlinRenderer } from "./KotlinRenderer.js"; import { KotlinXRenderer } from "./KotlinXRenderer.js"; export const kotlinOptions = { + justTypes: new BooleanOption("just-types", "Plain types only", false), framework: new EnumOption( "framework", "Serialization framework", { - "just-types": "None", jackson: "Jackson", klaxon: "Klaxon", kotlinx: "KotlinX", @@ -62,9 +63,12 @@ export class KotlinTargetLanguage extends TargetLanguage< ): ConvenienceRenderer { const options = getOptionValues(kotlinOptions, untypedOptionValues); + // `--just-types` wins over whatever `--framework` says. + if (options.justTypes) { + return new KotlinRenderer(this, renderContext, options); + } + switch (options.framework) { - case "None": - return new KotlinRenderer(this, renderContext, options); case "Jackson": return new KotlinJacksonRenderer(this, renderContext, options); case "Klaxon": diff --git a/packages/quicktype-core/src/language/Scala3/language.ts b/packages/quicktype-core/src/language/Scala3/language.ts index 3f98d37177..f1e8d10630 100644 --- a/packages/quicktype-core/src/language/Scala3/language.ts +++ b/packages/quicktype-core/src/language/Scala3/language.ts @@ -1,6 +1,7 @@ import type { ConvenienceRenderer } from "../../ConvenienceRenderer.js"; import type { RenderContext } from "../../Renderer.js"; import { + BooleanOption, EnumOption, StringOption, getOptionValues, @@ -14,15 +15,15 @@ import { Scala3Renderer } from "./Scala3Renderer.js"; import { UpickleRenderer } from "./UpickleRenderer.js"; export const scala3Options = { + justTypes: new BooleanOption("just-types", "Plain types only", false), framework: new EnumOption( "framework", "Serialization framework", { - "just-types": "None", circe: "Circe", upickle: "Upickle", } as const, - "just-types", + "circe", ), packageName: new StringOption("package", "Package", "PACKAGE", "quicktype"), }; @@ -58,9 +59,12 @@ export class Scala3TargetLanguage extends TargetLanguage< ): ConvenienceRenderer { const options = getOptionValues(scala3Options, untypedOptionValues); + // `--just-types` wins over whatever `--framework` says. + if (options.justTypes) { + return new Scala3Renderer(this, renderContext, options); + } + switch (options.framework) { - case "None": - return new Scala3Renderer(this, renderContext, options); case "Upickle": return new UpickleRenderer(this, renderContext, options); case "Circe": diff --git a/packages/quicktype-core/src/language/Smithy4s/language.ts b/packages/quicktype-core/src/language/Smithy4s/language.ts index 1213c92fd0..5376a3a6a4 100644 --- a/packages/quicktype-core/src/language/Smithy4s/language.ts +++ b/packages/quicktype-core/src/language/Smithy4s/language.ts @@ -1,28 +1,19 @@ import type { ConvenienceRenderer } from "../../ConvenienceRenderer.js"; import type { RenderContext } from "../../Renderer.js"; import { - EnumOption, + BooleanOption, StringOption, getOptionValues, } from "../../RendererOptions/index.js"; -import { assertNever } from "../../support/Support.js"; import { TargetLanguage } from "../../TargetLanguage.js"; import type { LanguageName, RendererOptions } from "../../types.js"; import { Smithy4sRenderer } from "./Smithy4sRenderer.js"; -export enum Framework { - None = "None", -} - export const smithyOptions = { - // FIXME: why does this exist - framework: new EnumOption( - "framework", - "Serialization framework", - { "just-types": Framework.None } as const, - "just-types", - ), + // Plain types is the only mode Smithy supports; the flag is accepted + // for consistency with the other languages. + justTypes: new BooleanOption("just-types", "Plain types only", false), packageName: new StringOption("package", "Package", "PACKAGE", "quicktype"), }; @@ -55,13 +46,10 @@ export class SmithyTargetLanguage extends TargetLanguage< renderContext: RenderContext, untypedOptionValues: RendererOptions, ): ConvenienceRenderer { - const options = getOptionValues(smithyOptions, untypedOptionValues); - - switch (options.framework) { - case Framework.None: - return new Smithy4sRenderer(this, renderContext, options); - default: - return assertNever(options.framework); - } + return new Smithy4sRenderer( + this, + renderContext, + getOptionValues(smithyOptions, untypedOptionValues), + ); } } diff --git a/packages/quicktype-vscode/src/extension.ts b/packages/quicktype-vscode/src/extension.ts index 08f5e011d9..506efb9d97 100644 --- a/packages/quicktype-vscode/src/extension.ts +++ b/packages/quicktype-vscode/src/extension.ts @@ -106,19 +106,12 @@ async function runQuicktype( vscode.workspace.getConfiguration(configurationSection); const justTypes = forceJustTypes || configuration.justTypes; - const rendererOptions: RendererOptions = {}; - if (justTypes) { - // FIXME: The target language should have a property to return these options. - if (lang.name === "csharp") { - (rendererOptions as RendererOptions<"csharp">).features = - "just-types"; - } else if (lang.name === "kotlin") { - (rendererOptions as RendererOptions<"kotlin">).framework = - "just-types"; - } else if ("just-types" in rendererOptions) { - rendererOptions["just-types"] = "true"; - } - } + // Not every language has a `just-types` option — JSON Schema, for + // example, has no serialization helpers to leave out. + const rendererOptions: RendererOptions = + justTypes && lang.optionDefinitions.some((o) => o.name === "just-types") + ? ({ "just-types": "true" } as RendererOptions) + : {}; const inputData = new InputData(); switch (kind) { diff --git a/test/languages.ts b/test/languages.ts index b064f74c8d..a03ef19e2c 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1083,7 +1083,7 @@ I havea no idea how to encode these tests correctly. ], skipSchema: [], skipMiscJSON: false, - rendererOptions: { framework: "just-types" }, + rendererOptions: { "just-types": "true" }, quickTestRendererOptions: [], sourceFiles: ["src/language/Smithy4s/index.ts"], }; diff --git a/test/unit/just-types-option.test.ts b/test/unit/just-types-option.test.ts new file mode 100644 index 0000000000..8ae56e317a --- /dev/null +++ b/test/unit/just-types-option.test.ts @@ -0,0 +1,138 @@ +// Every quicktype language spells "generate plain types without +// (de)serialization helpers" as the boolean `just-types` option. C#'s +// `features=just-types` / `features=just-types-and-namespace` and Kotlin's, +// Scala 3's, and Smithy4s's `framework=just-types` are gone; the boolean +// wins over a conflicting explicit enum value. +import { describe, expect, test } from "vitest"; + +import { + InputData, + type LanguageName, + type RendererOptions, + jsonInputForTargetLanguage, + quicktype, +} from "quicktype-core"; + +async function linesFor( + lang: LanguageName, + rendererOptions: RendererOptions = {}, +): Promise { + const jsonInput = jsonInputForTargetLanguage(lang); + await jsonInput.addSource({ + name: "Person", + samples: ['{"name": "Alice", "age": 37}'], + }); + const inputData = new InputData(); + inputData.addInput(jsonInput); + const result = await quicktype({ inputData, lang, rendererOptions }); + return result.lines.join("\n"); +} + +describe("just-types generates plain types in every language", () => { + test("C#: no attributes, no helpers, but a namespace", async () => { + const output = await linesFor("csharp", { "just-types": true }); + expect(output).toContain("namespace QuickType"); + expect(output).toContain("public partial class Person"); + expect(output).not.toContain("JsonConverter"); + expect(output).not.toContain("JsonProperty"); + }); + + test("Kotlin: plain data classes, no Klaxon", async () => { + const output = await linesFor("kotlin", { "just-types": true }); + expect(output).toContain("data class Person"); + expect(output).not.toContain("Klaxon"); + }); + + test("Scala 3: plain case classes, no circe", async () => { + const output = await linesFor("scala3", { "just-types": true }); + expect(output).toContain("case class Person"); + expect(output).not.toContain("circe"); + }); + + test("Smithy: accepted (plain types is the only mode)", async () => { + const viaBoolean = await linesFor("smithy4a", { "just-types": true }); + expect(viaBoolean).toContain("structure Person"); + expect(viaBoolean).toEqual(await linesFor("smithy4a")); + }); +}); + +describe("the removed enum spellings are errors", () => { + // The old spellings aren't valid `RendererOptions` anymore, which is + // the point: they must also fail for API callers who evade the types. + test("C#: features=just-types", async () => { + const options = { features: "just-types" } as RendererOptions; + await expect(linesFor("csharp", options)).rejects.toThrow( + "Unknown value just-types for option features", + ); + }); + + test("C#: features=just-types-and-namespace", async () => { + const options = { + features: "just-types-and-namespace", + } as RendererOptions; + await expect(linesFor("csharp", options)).rejects.toThrow( + "Unknown value just-types-and-namespace for option features", + ); + }); + + test("Kotlin: framework=just-types", async () => { + const options = { framework: "just-types" } as RendererOptions; + await expect(linesFor("kotlin", options)).rejects.toThrow( + "Unknown value just-types for option framework", + ); + }); + + test("Scala 3: framework=just-types", async () => { + const options = { framework: "just-types" } as RendererOptions; + await expect(linesFor("scala3", options)).rejects.toThrow( + "Unknown value just-types for option framework", + ); + }); +}); + +describe("just-types wins over a conflicting enum option", () => { + test("C#: just-types beats features=attributes-only", async () => { + const output = await linesFor("csharp", { + "just-types": true, + features: "attributes-only", + }); + expect(output).toEqual( + await linesFor("csharp", { "just-types": true }), + ); + expect(output).not.toContain("JsonProperty"); + }); + + test("Kotlin: just-types beats framework=jackson", async () => { + const output = await linesFor("kotlin", { + "just-types": true, + framework: "jackson", + }); + expect(output).toEqual( + await linesFor("kotlin", { "just-types": true }), + ); + expect(output).not.toContain("jackson"); + }); + + test("Scala 3: just-types beats framework=upickle", async () => { + const output = await linesFor("scala3", { + "just-types": true, + framework: "upickle", + }); + expect(output).toEqual( + await linesFor("scala3", { "just-types": true }), + ); + expect(output).not.toContain("upickle"); + }); +}); + +describe("framework defaults", () => { + test("Scala 3 defaults to circe", async () => { + const output = await linesFor("scala3"); + expect(output).toContain("io.circe"); + }); + + test("Kotlin defaults to Klaxon", async () => { + const output = await linesFor("kotlin"); + expect(output).toContain("com.beust.klaxon"); + }); +}); diff --git a/test/unit/renderer-options.test-d.ts b/test/unit/renderer-options.test-d.ts index f86fcde9c3..33fd010369 100644 --- a/test/unit/renderer-options.test-d.ts +++ b/test/unit/renderer-options.test-d.ts @@ -52,8 +52,8 @@ describe("rendererOptions typing", () => { test("rejects another language's option name", () => { void quicktype({ inputData, - lang: "kotlin", - // @ts-expect-error `just-types` is a C#/TypeScript spelling; Kotlin has no such option + lang: "rust", + // @ts-expect-error Rust has no `just-types` option rendererOptions: { "just-types": "true" }, }); });