refactor: centralize numeric coercion helpers

This commit is contained in:
Peter Steinberger
2026-05-28 18:27:26 -04:00
parent 359c31b7e7
commit b877fc58a5
6 changed files with 87 additions and 61 deletions
+7 -3
View File
@@ -1,8 +1,12 @@
import type { TSchema } from "typebox";
import { readLocalFileSafely } from "../../infra/fs-safe.js";
import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js";
import { detectMime } from "../../media/mime.js";
import { readSnakeCaseParamRaw } from "../../param-key.js";
import {
asPositiveSafeInteger,
asSafeIntegerInRange,
parseStrictFiniteNumber,
} from "../../shared/number-coercion.js";
import { normalizeStringEntries } from "../../shared/string-normalization.js";
import type { ImageSanitizationLimits } from "../image-sanitization.js";
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "../runtime/index.js";
@@ -194,10 +198,10 @@ export function readNumberParam(
return undefined;
}
if (positiveInteger) {
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
return asPositiveSafeInteger(value);
}
if (nonNegativeInteger) {
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
return asSafeIntegerInRange(value, { min: 0 });
}
return integer ? Math.trunc(value) : value;
}
+7 -54
View File
@@ -1,54 +1,7 @@
function normalizeNumericString(value: string): string | undefined {
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
export function parseFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
return parseStrictFiniteNumber(value);
}
return undefined;
}
export function parseStrictInteger(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isSafeInteger(value) ? value : undefined;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = normalizeNumericString(value);
if (!normalized || !/^[+-]?\d+$/.test(normalized)) {
return undefined;
}
const parsed = Number(normalized);
return Number.isSafeInteger(parsed) ? parsed : undefined;
}
export function parseStrictFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = normalizeNumericString(value);
if (!normalized || !/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(normalized)) {
return undefined;
}
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : undefined;
}
export function parseStrictPositiveInteger(value: unknown): number | undefined {
const parsed = parseStrictInteger(value);
return parsed !== undefined && parsed > 0 ? parsed : undefined;
}
export function parseStrictNonNegativeInteger(value: unknown): number | undefined {
const parsed = parseStrictInteger(value);
return parsed !== undefined && parsed >= 0 ? parsed : undefined;
}
export {
parseFiniteNumber,
parseStrictFiniteNumber,
parseStrictInteger,
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
} from "../shared/number-coercion.js";
+1 -1
View File
@@ -6,4 +6,4 @@ export {
parseStrictFiniteNumber,
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
} from "../infra/parse-finite-number.js";
} from "../shared/number-coercion.js";
+4
View File
@@ -20,6 +20,10 @@ export {
asPositiveSafeInteger,
asSafeIntegerInRange,
parseFiniteNumber,
parseStrictFiniteNumber,
parseStrictInteger,
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
} from "../shared/number-coercion.js";
export { asBoolean, parseBooleanValue } from "../utils/boolean.js";
export {
+28
View File
@@ -4,6 +4,10 @@ import {
asFiniteNumberInRange,
asSafeIntegerInRange,
parseFiniteNumber,
parseStrictFiniteNumber,
parseStrictInteger,
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
} from "./number-coercion.js";
describe("number-coercion", () => {
@@ -37,4 +41,28 @@ describe("number-coercion", () => {
expect(parseFiniteNumber("")).toBeUndefined();
expect(parseFiniteNumber("nope")).toBeUndefined();
});
test("parseStrictInteger accepts only safe integer tokens", () => {
expect(parseStrictInteger("42")).toBe(42);
expect(parseStrictInteger(" -7 ")).toBe(-7);
expect(parseStrictInteger("+9")).toBe(9);
expect(parseStrictInteger("1.5")).toBeUndefined();
expect(parseStrictInteger("1e3")).toBeUndefined();
expect(parseStrictInteger(Number.MAX_SAFE_INTEGER + 1)).toBeUndefined();
});
test("parseStrictFiniteNumber rejects partial numeric strings", () => {
expect(parseStrictFiniteNumber("42")).toBe(42);
expect(parseStrictFiniteNumber(".5")).toBe(0.5);
expect(parseStrictFiniteNumber("1e3")).toBe(1000);
expect(parseStrictFiniteNumber("3.14ms")).toBeUndefined();
expect(parseStrictFiniteNumber("0x10")).toBeUndefined();
});
test("strict integer range helpers enforce sign", () => {
expect(parseStrictPositiveInteger("9")).toBe(9);
expect(parseStrictPositiveInteger("0")).toBeUndefined();
expect(parseStrictNonNegativeInteger("0")).toBe(0);
expect(parseStrictNonNegativeInteger("-1")).toBeUndefined();
});
});
+40 -3
View File
@@ -47,21 +47,58 @@ export function asSafeIntegerInRange(
return value;
}
function normalizeNumericString(value: string): string | undefined {
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
export function parseFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
return parseStrictFiniteNumber(value);
}
export function parseStrictInteger(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isSafeInteger(value) ? value : undefined;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = normalizeNumericString(value);
if (!normalized || !/^[+-]?\d+$/.test(normalized)) {
return undefined;
}
const parsed = Number(normalized);
return Number.isSafeInteger(parsed) ? parsed : undefined;
}
export function parseStrictFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
if (!trimmed || !/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(trimmed)) {
const normalized = normalizeNumericString(value);
if (!normalized || !/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(normalized)) {
return undefined;
}
const parsed = Number(trimmed);
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : undefined;
}
export function asPositiveSafeInteger(value: unknown): number | undefined {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
}
export function parseStrictPositiveInteger(value: unknown): number | undefined {
const parsed = parseStrictInteger(value);
return parsed !== undefined && parsed > 0 ? parsed : undefined;
}
export function parseStrictNonNegativeInteger(value: unknown): number | undefined {
const parsed = parseStrictInteger(value);
return parsed !== undefined && parsed >= 0 ? parsed : undefined;
}