From 109f0cb4729a503c9f896435d0ab2a8207545f30 Mon Sep 17 00:00:00 2001 From: Javis Date: Fri, 13 Mar 2026 13:59:11 +0800 Subject: [PATCH] feat(core): add between and lengthBetween validation functions Adds two new convenience validation functions: - `between`: Check if numeric value is within a range (inclusive) - `lengthBetween`: Check if string length is within a range (inclusive) These combine min/max and minLength/maxLength checks into single validations, reducing boilerplate when both bounds are needed. Also adds corresponding `check.between()` and `check.lengthBetween()` helper functions for easier check creation. --- packages/core/src/validation.test.ts | 117 +++++++++++++++++++++++++++ packages/core/src/validation.ts | 40 +++++++++ 2 files changed, 157 insertions(+) diff --git a/packages/core/src/validation.test.ts b/packages/core/src/validation.test.ts index 85fefb47..8d51b99b 100644 --- a/packages/core/src/validation.test.ts +++ b/packages/core/src/validation.test.ts @@ -135,6 +135,91 @@ describe("builtInValidationFunctions", () => { }); }); + describe("between", () => { + it("passes when number is within range", () => { + expect(builtInValidationFunctions.between(5, { min: 1, max: 10 })).toBe( + true, + ); + expect(builtInValidationFunctions.between(1, { min: 1, max: 10 })).toBe( + true, + ); + expect(builtInValidationFunctions.between(10, { min: 1, max: 10 })).toBe( + true, + ); + }); + + it("fails when number is below range", () => { + expect(builtInValidationFunctions.between(0, { min: 1, max: 10 })).toBe( + false, + ); + }); + + it("fails when number is above range", () => { + expect(builtInValidationFunctions.between(11, { min: 1, max: 10 })).toBe( + false, + ); + }); + + it("fails for non-numbers", () => { + expect(builtInValidationFunctions.between("5", { min: 1, max: 10 })).toBe( + false, + ); + }); + + it("fails when min or max is not provided", () => { + expect(builtInValidationFunctions.between(5, { min: 1 })).toBe(false); + expect(builtInValidationFunctions.between(5, { max: 10 })).toBe(false); + expect(builtInValidationFunctions.between(5, {})).toBe(false); + }); + }); + + describe("lengthBetween", () => { + it("passes when string length is within range", () => { + expect( + builtInValidationFunctions.lengthBetween("hello", { min: 3, max: 10 }), + ).toBe(true); + expect( + builtInValidationFunctions.lengthBetween("abc", { min: 3, max: 10 }), + ).toBe(true); + expect( + builtInValidationFunctions.lengthBetween("abcdefghij", { + min: 3, + max: 10, + }), + ).toBe(true); + }); + + it("fails when string is too short", () => { + expect( + builtInValidationFunctions.lengthBetween("ab", { min: 3, max: 10 }), + ).toBe(false); + }); + + it("fails when string is too long", () => { + expect( + builtInValidationFunctions.lengthBetween("abcdefghijk", { + min: 3, + max: 10, + }), + ).toBe(false); + }); + + it("fails for non-strings", () => { + expect( + builtInValidationFunctions.lengthBetween(12345, { min: 3, max: 10 }), + ).toBe(false); + }); + + it("fails when min or max is not provided", () => { + expect( + builtInValidationFunctions.lengthBetween("hello", { min: 3 }), + ).toBe(false); + expect( + builtInValidationFunctions.lengthBetween("hello", { max: 10 }), + ).toBe(false); + }); + }); + describe("numeric", () => { it("passes for numbers", () => { expect(builtInValidationFunctions.numeric(42)).toBe(true); @@ -602,6 +687,38 @@ describe("check helper", () => { }); }); + describe("between", () => { + it("creates between check with args", () => { + const c = check.between(1, 100, "Out of range"); + + expect(c.type).toBe("between"); + expect(c.args).toEqual({ min: 1, max: 100 }); + expect(c.message).toBe("Out of range"); + }); + + it("uses default message", () => { + const c = check.between(0, 10); + + expect(c.message).toBe("Must be between 0 and 10"); + }); + }); + + describe("lengthBetween", () => { + it("creates lengthBetween check with args", () => { + const c = check.lengthBetween(3, 50, "Invalid length"); + + expect(c.type).toBe("lengthBetween"); + expect(c.args).toEqual({ min: 3, max: 50 }); + expect(c.message).toBe("Invalid length"); + }); + + it("uses default message", () => { + const c = check.lengthBetween(5, 100); + + expect(c.message).toBe("Must be between 5 and 100 characters"); + }); + }); + describe("url", () => { it("creates url check", () => { const c = check.url("Must be a URL"); diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index fd9c20e8..ba36485e 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -148,6 +148,30 @@ export const builtInValidationFunctions: Record = { return value <= max; }, + /** + * Check if numeric value is within a range (inclusive). + * Combines min and max checks in a single validation. + */ + between: (value: unknown, args?: Record) => { + if (typeof value !== "number") return false; + const min = args?.min; + const max = args?.max; + if (typeof min !== "number" || typeof max !== "number") return false; + return value >= min && value <= max; + }, + + /** + * Check if string length is within a range (inclusive). + * Combines minLength and maxLength checks in a single validation. + */ + lengthBetween: (value: unknown, args?: Record) => { + if (typeof value !== "string") return false; + const min = args?.min; + const max = args?.max; + if (typeof min !== "number" || typeof max !== "number") return false; + return value.length >= min && value.length <= max; + }, + /** * Check if value is a number */ @@ -386,6 +410,22 @@ export const check = { message: message ?? `Must be at most ${max}`, }), + between: (min: number, max: number, message?: string): ValidationCheck => ({ + type: "between", + args: { min, max }, + message: message ?? `Must be between ${min} and ${max}`, + }), + + lengthBetween: ( + min: number, + max: number, + message?: string, + ): ValidationCheck => ({ + type: "lengthBetween", + args: { min, max }, + message: message ?? `Must be between ${min} and ${max} characters`, + }), + url: (message = "Invalid URL"): ValidationCheck => ({ type: "url", message,