Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/quicktype-core/src/RendererOptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ export type OptionValue<O> =
export type OptionKey<O> =
O extends EnumOption<string, Record<string, unknown>, infer EnumKey>
? EnumKey
: O;
: O extends Option<string, infer Value>
? Value extends boolean
? // `BooleanOption.getValue` also accepts the strings
// "true" and "false", which is what the CLI passes.
boolean | `${boolean}`
: Value
: never;

// FIXME: Merge these and use camelCase user-facing keys (v24)
export type OptionMap<T> = {
Expand Down
21 changes: 11 additions & 10 deletions packages/quicktype-core/src/Run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ export interface NonInferenceOptions<Lang extends LanguageName = LanguageName> {
*/
outputFilename: string;
/** Options for the target language's renderer */
rendererOptions: RendererOptions<Lang>;
rendererOptions: Partial<RendererOptions<Lang>>;
}

export type Options = NonInferenceOptions & InferenceFlags;
export type Options<Lang extends LanguageName = LanguageName> =
NonInferenceOptions<Lang> & InferenceFlags;

const defaultOptions: NonInferenceOptions = {
lang: "ts",
Expand Down Expand Up @@ -600,15 +601,15 @@ class Run implements RunContext {
* @param options Partial options. For options that are not defined, the
* defaults will be used.
*/
export async function quicktypeMultiFile(
options: Partial<Options>,
): Promise<MultiFileRenderResult> {
export async function quicktypeMultiFile<
Lang extends LanguageName = LanguageName,
>(options: Partial<Options<Lang>>): Promise<MultiFileRenderResult> {
return await new Run(options).run();
}

export function quicktypeMultiFileSync(
options: Partial<Options>,
): MultiFileRenderResult {
export function quicktypeMultiFileSync<
Lang extends LanguageName = LanguageName,
>(options: Partial<Options<Lang>>): MultiFileRenderResult {
return new Run(options).runSync();
}

Expand Down Expand Up @@ -664,8 +665,8 @@ export function combineRenderResults(
* @param options Partial options. For options that are not defined, the
* defaults will be used.
*/
export async function quicktype(
options: Partial<Options>,
export async function quicktype<Lang extends LanguageName = LanguageName>(
options: Partial<Options<Lang>>,
): Promise<SerializedRenderResult> {
const result = await quicktypeMultiFile(options);
return combineRenderResults(result);
Expand Down
13 changes: 3 additions & 10 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,6 @@ export const CPlusPlusLanguage: Language = {
],
rendererOptions: {},
quickTestRendererOptions: [
{ unions: "indirection" },
{ "source-style": "multi-source" },
{ "code-format": "with-struct" },
{ wstring: "use-wstring" },
Expand Down Expand Up @@ -852,7 +851,6 @@ export const TypeScriptLanguage: Language = {
{ "runtime-typecheck": "false" },
{ "runtime-typecheck-ignore-unknown-properties": "true" },
{ "nice-property-names": "true" },
{ "declare-unions": "true" },
["pokedex.json", { "prefer-types": "true" }],
{ "acronym-style": "pascal" },
{ converters: "all-objects" },
Expand Down Expand Up @@ -910,11 +908,7 @@ export const JavaScriptPropTypesLanguage: Language = {
skipSchema: [],
skipMiscJSON: false,
rendererOptions: { "module-system": "es6" },
quickTestRendererOptions: [
{ "runtime-typecheck": "false" },
{ "runtime-typecheck-ignore-unknown-properties": "true" },
{ converters: "top-level" },
],
quickTestRendererOptions: [{ converters: "top-level" }],
sourceFiles: ["src/language/JavaScriptPropTypes/index.ts"],
};

Expand All @@ -940,7 +934,6 @@ export const FlowLanguage: Language = {
{ "runtime-typecheck": "false" },
{ "runtime-typecheck-ignore-unknown-properties": "true" },
{ "nice-property-names": "true" },
{ "declare-unions": "true" },
],
sourceFiles: ["src/language/Flow/index.ts"],
};
Expand Down Expand Up @@ -1602,7 +1595,7 @@ export const TypeScriptZodLanguage: Language = {
"required-non-properties.schema",
],
rendererOptions: {},
quickTestRendererOptions: [{ "array-type": "list" }],
quickTestRendererOptions: [],
sourceFiles: ["src/language/TypeScriptZod/index.ts"],
};

Expand Down Expand Up @@ -1717,7 +1710,7 @@ export const TypeScriptEffectSchemaLanguage: Language = {
"required-non-properties.schema",
],
rendererOptions: {},
quickTestRendererOptions: [{ "array-type": "list" }],
quickTestRendererOptions: [],
sourceFiles: ["src/language/TypeScriptEffectSchema/index.ts"],
};

Expand Down
90 changes: 90 additions & 0 deletions test/unit/renderer-options.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Type-level tests: `rendererOptions` keys and values are validated by the
// type system against the target language given in `lang`. When `lang` is
// a string literal, `quicktype` infers it and unknown option names or
// invalid enum values are compile errors — there is no runtime validation.
// See https://github.com/glideapps/quicktype/issues/2933.
import { describe, test } from "vitest";

import {
InputData,
type LanguageName,
quicktype,
quicktypeMultiFile,
} from "../../packages/quicktype-core/src/index.js";

const inputData = new InputData();

describe("rendererOptions typing", () => {
test("accepts a language's own options", () => {
void quicktype({
inputData,
lang: "csharp",
rendererOptions: {
namespace: "Acme",
framework: "SystemTextJson",
"csharp-version": "6",
},
});
});

test("boolean options accept booleans and their string forms", () => {
void quicktype({
inputData,
lang: "typescript",
rendererOptions: { "just-types": true },
});
void quicktype({
inputData,
lang: "typescript",
rendererOptions: { "just-types": "true" },
});
});

test("rejects an unknown option name", () => {
void quicktype({
inputData,
lang: "typescript",
// @ts-expect-error unknown option name
rendererOptions: { "totally-bogus-option": "yes" },
});
});

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
rendererOptions: { "just-types": "true" },
});
});

test("rejects an invalid enum option value", () => {
void quicktype({
inputData,
lang: "csharp",
// @ts-expect-error GSON is not a C# serialization framework
rendererOptions: { framework: "GSON" },
});
});

test("quicktypeMultiFile enforces the same typing", () => {
void quicktypeMultiFile({
inputData,
lang: "csharp",
// @ts-expect-error unknown option name
rendererOptions: { "totally-bogus-option": "yes" },
});
});

test("stays permissive when the language is not a literal", () => {
// Callers that don't pin `lang` to a literal — or omit it — keep
// the old, unchecked behavior.
const lang: LanguageName = "csharp" as LanguageName;
void quicktype({
inputData,
lang,
rendererOptions: { "just-types": "true" },
});
void quicktype({ inputData, rendererOptions: {} });
});
});
11 changes: 11 additions & 0 deletions test/unit/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"lib": ["ES2022"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true
},
"include": ["./**/*.test-d.ts"]
}
5 changes: 5 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@ export default defineConfig({
environment: "node",
include: ["test/unit/**/*.test.ts"],
testTimeout: 30_000,
typecheck: {
enabled: true,
include: ["test/unit/**/*.test-d.ts"],
tsconfig: "./test/unit/tsconfig.json",
},
},
});
Loading