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
19 changes: 19 additions & 0 deletions packages/quicktype-core/src/TargetLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { Type } from "./Type/Type.js";
import type { StringTypeMapping } from "./Type/TypeBuilderUtils.js";
import type { TypeGraph } from "./Type/TypeGraph.js";
import type { Comment } from "./support/Comments.js";
import { INT64_RANGE, type IntegerRange } from "./support/IntegerRange.js";
import { defined } from "./support/Support.js";
import type { LanguageName, RendererOptions } from "./types.js";

Expand Down Expand Up @@ -121,4 +122,22 @@ export abstract class TargetLanguage<
public get dateTimeRecognizer(): DateTimeRecognizer {
return new DefaultDateTimeRecognizer();
}

/**
* The inclusive range of whole numbers in input JSON that quicktype
* infers as the language's integer type. Whole numbers outside the
* range are inferred as `double` instead, because the integer type
* could not round-trip them (issue #2931). `null` means the
* language's integers are arbitrary-precision.
*
* Languages whose integer width depends on a renderer option (like
* cJSON's `integer-size`) override this and inspect
* `rendererOptions`, which are the same untyped option values that
* `makeRenderer` receives.
*/
public getSupportedIntegerRange(
_rendererOptions: Record<string, unknown> = {},
): IntegerRange | null {
return INT64_RANGE;
}
}
9 changes: 9 additions & 0 deletions packages/quicktype-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ export {
type InferenceFlagName,
} from "./Inference.js";
export { CompressedJSON, type Value } from "./input/CompressedJSON.js";
export {
INT8_RANGE,
INT16_RANGE,
INT32_RANGE,
INT64_RANGE,
type IntegerRange,
JS_SAFE_INTEGER_RANGE,
integerStringInRange,
} from "./support/IntegerRange.js";
export {
type Input,
InputData,
Expand Down
48 changes: 43 additions & 5 deletions packages/quicktype-core/src/input/CompressedJSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { addHashCode, hashCodeInit, hashString } from "collection-utils";

import { inferTransformedStringTypeKindForString } from "../attributes/StringTypes.js";
import type { DateTimeRecognizer } from "../DateTime.js";
import {
INT64_RANGE,
type IntegerRange,
integerStringInRange,
} from "../support/IntegerRange.js";
import { assert, defined, panic } from "../support/Support.js";
import {
type TransformedStringTypeKind,
Expand Down Expand Up @@ -66,13 +71,50 @@ export abstract class CompressedJSON<T> {

private readonly _arrays: Value[][] = [];

/**
* `supportedIntegerRange` is the range of whole numbers in the input
* that get inferred as `integer`; whole numbers outside it are inferred
* as `double`, because the target language's integer type could not
* round-trip them. `null` means the target's integers are
* arbitrary-precision. See `TargetLanguage.getSupportedIntegerRange`.
*/
public constructor(
public readonly dateTimeRecognizer: DateTimeRecognizer,
public readonly handleRefs: boolean,
public readonly supportedIntegerRange: IntegerRange | null = INT64_RANGE,
) {}

public abstract parse(input: T): Promise<Value>;

/**
* Whether a whole number in the input, given as the decimal string of
* its JSON literal, fits `supportedIntegerRange`. Works on the digit
* string because such literals can exceed what a JavaScript number can
* represent exactly.
*/
protected integerStringFits(integerString: string): boolean {
const range = this.supportedIntegerRange;
if (range === null) return true;
return integerStringInRange(integerString, range);
}

/**
* Whether a number that `JSON.parse` produced should be inferred as
* `double`. The original literal is gone at this point, but for whole
* numbers below 1e21, `toFixed(0)` gives the exact decimal value of the
* double, and it errs on the right side at range boundaries: a literal
* like 9223372036854775807 (INT64_MAX) parses to the double
* 9223372036854775808, which is correctly outside the int64 range. At
* 1e21 doubles are far beyond any fixed-size integer type and `toFixed`
* switches to exponential notation, so those are doubles outright.
*/
protected parsedNumberIsDouble(n: number): boolean {
if (n !== Math.floor(n)) return true;
if (this.supportedIntegerRange === null) return false;
if (Math.abs(n) >= 1e21) return true;
return !this.integerStringFits(n.toFixed(0));
}

public parseSync(_input: T): Value {
return panic("parseSync not implemented in CompressedJSON");
}
Expand Down Expand Up @@ -340,11 +382,7 @@ export class CompressedJSONFromString extends CompressedJSON<string> {
} else if (typeof json === "string") {
this.commitString(json);
} else if (typeof json === "number") {
const isDouble =
json !== Math.floor(json) ||
json < Number.MIN_SAFE_INTEGER ||
json > Number.MAX_SAFE_INTEGER;
this.commitNumber(isDouble);
this.commitNumber(this.parsedNumberIsDouble(json));
} else if (Array.isArray(json)) {
this.pushArrayContext();
for (const v of json) {
Expand Down
2 changes: 2 additions & 0 deletions packages/quicktype-core/src/input/Inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export function jsonInputForTargetLanguage(
targetLanguage: LanguageName | TargetLanguage,
languages?: TargetLanguage[],
handleJSONRefs = false,
rendererOptions: Record<string, unknown> = {},
): JSONInput<string> {
if (typeof targetLanguage === "string") {
targetLanguage = defined(languageNamed(targetLanguage, languages));
Expand All @@ -214,6 +215,7 @@ export function jsonInputForTargetLanguage(
const compressedJSON = new CompressedJSONFromString(
targetLanguage.dateTimeRecognizer,
handleJSONRefs,
targetLanguage.getSupportedIntegerRange(rendererOptions),
);
return new JSONInput(compressedJSON);
}
Expand Down
29 changes: 29 additions & 0 deletions packages/quicktype-core/src/language/CJSON/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import {
StringOption,
getOptionValues,
} from "../../RendererOptions/index.js";
import {
INT8_RANGE,
INT16_RANGE,
INT32_RANGE,
INT64_RANGE,
type IntegerRange,
} from "../../support/IntegerRange.js";
import { TargetLanguage } from "../../TargetLanguage.js";
import type { LanguageName, RendererOptions } from "../../types.js";

Expand Down Expand Up @@ -142,6 +149,28 @@ export class CJSONTargetLanguage extends TargetLanguage<
return true;
}

/**
* Return the range of whole numbers the generated integer type can
* represent, which depends on the `integer-size` renderer option
* (int64_t by default)
* @param rendererOptions: untyped renderer option values
* @return the range of the configured integer type
*/
public getSupportedIntegerRange(
rendererOptions: Record<string, unknown> = {},
): IntegerRange | null {
switch (cJSONOptions.typeIntegerSize.getValue(rendererOptions)) {
case "int8_t":
return INT8_RANGE;
case "int16_t":
return INT16_RANGE;
case "int32_t":
return INT32_RANGE;
default:
return INT64_RANGE;
}
}

/**
* Indicate if language support optional class properties
* @return true
Expand Down
6 changes: 6 additions & 0 deletions packages/quicktype-core/src/language/Crystal/language.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RenderContext } from "../../Renderer.js";
import { INT32_RANGE, type IntegerRange } from "../../support/IntegerRange.js";
import { TargetLanguage } from "../../TargetLanguage.js";

import { CrystalRenderer } from "./CrystalRenderer.js";
Expand All @@ -16,6 +17,11 @@ export class CrystalTargetLanguage extends TargetLanguage<
super(crystalLanguageConfig);
}

// The Crystal renderer emits `Int32` for inferred integers.
public getSupportedIntegerRange(): IntegerRange | null {
return INT32_RANGE;
}

protected makeRenderer(renderContext: RenderContext): CrystalRenderer {
return new CrystalRenderer(this, renderContext);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/quicktype-core/src/language/Elixir/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
StringOption,
getOptionValues,
} from "../../RendererOptions/index.js";
import type { IntegerRange } from "../../support/IntegerRange.js";
import { TargetLanguage } from "../../TargetLanguage.js";
import type { LanguageName, RendererOptions } from "../../types.js";

Expand All @@ -28,6 +29,11 @@ export const elixirLanguageConfig = {
export class ElixirTargetLanguage extends TargetLanguage<
typeof elixirLanguageConfig
> {
// Elixir's integers are arbitrary-precision.
public getSupportedIntegerRange(): IntegerRange | null {
return null;
}

public constructor() {
super(elixirLanguageConfig);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/quicktype-core/src/language/Elm/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
StringOption,
getOptionValues,
} from "../../RendererOptions/index.js";
import {
type IntegerRange,
JS_SAFE_INTEGER_RANGE,
} from "../../support/IntegerRange.js";
import { TargetLanguage } from "../../TargetLanguage.js";
import type { LanguageName, RendererOptions } from "../../types.js";

Expand Down Expand Up @@ -55,6 +59,12 @@ export class ElmTargetLanguage extends TargetLanguage<
return true;
}

// Elm compiles to JavaScript, where `Int` is an IEEE-754 double at
// runtime, so integers are only exact within the JS safe range.
public getSupportedIntegerRange(): IntegerRange | null {
return JS_SAFE_INTEGER_RANGE;
}

protected makeRenderer<Lang extends LanguageName = "elm">(
renderContext: RenderContext,
untypedOptionValues: RendererOptions<Lang>,
Expand Down
6 changes: 6 additions & 0 deletions packages/quicktype-core/src/language/JSONSchema/language.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RenderContext } from "../../Renderer.js";
import type { IntegerRange } from "../../support/IntegerRange.js";
import { TargetLanguage } from "../../TargetLanguage.js";
import {
type StringTypeMapping,
Expand All @@ -17,6 +18,11 @@ export const JSONSchemaLanguageConfig = {
export class JSONSchemaTargetLanguage extends TargetLanguage<
typeof JSONSchemaLanguageConfig
> {
// JSON Schema's `integer` type is unbounded.
public getSupportedIntegerRange(): IntegerRange | null {
return null;
}

public constructor() {
super(JSONSchemaLanguageConfig);
}
Expand Down
8 changes: 8 additions & 0 deletions packages/quicktype-core/src/language/JavaScript/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
} from "../../RendererOptions/index.js";
import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms.js";
import { convertersOption } from "../../support/Converters.js";
import {
JS_SAFE_INTEGER_RANGE,
type IntegerRange,
} from "../../support/IntegerRange.js";
import { TargetLanguage } from "../../TargetLanguage.js";
import type {
PrimitiveStringTypeKind,
Expand Down Expand Up @@ -51,6 +55,10 @@ export const javaScriptLanguageConfig = {
export class JavaScriptTargetLanguage extends TargetLanguage<
typeof javaScriptLanguageConfig
> {
public getSupportedIntegerRange(): IntegerRange | null {
return JS_SAFE_INTEGER_RANGE;
}

public constructor() {
super(javaScriptLanguageConfig);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import type { RenderContext } from "../../Renderer.js";
import { EnumOption, getOptionValues } from "../../RendererOptions/index.js";
import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms.js";
import { convertersOption } from "../../support/Converters.js";
import {
JS_SAFE_INTEGER_RANGE,
type IntegerRange,
} from "../../support/IntegerRange.js";
import { TargetLanguage } from "../../TargetLanguage.js";
import type { LanguageName, RendererOptions } from "../../types.js";

Expand Down Expand Up @@ -30,6 +34,10 @@ export const javaScriptPropTypesLanguageConfig = {
export class JavaScriptPropTypesTargetLanguage extends TargetLanguage<
typeof javaScriptPropTypesLanguageConfig
> {
public getSupportedIntegerRange(): IntegerRange | null {
return JS_SAFE_INTEGER_RANGE;
}

public constructor() {
super(javaScriptPropTypesLanguageConfig);
}
Expand Down
Loading
Loading