From cee0056fc0174e7e545ffc4b48c8bb2ec138657f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 00:43:05 +0100 Subject: [PATCH] Allow nested checking --- lib/entry-points.js | 126 +++++++++++++++++++++++++++++------------ src/json/index.test.ts | 18 +++++- src/json/index.ts | 90 ++++++++++++++++++++++++----- 3 files changed, 179 insertions(+), 55 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ccfb1da9f..648e9c50a 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -29647,13 +29647,13 @@ var require_helpers = __commonJS({ exports2.encodePath = function encodePointer(a) { return a.map(pathEncoder).join(""); }; - exports2.getDecimalPlaces = function getDecimalPlaces(number) { + exports2.getDecimalPlaces = function getDecimalPlaces(number2) { var decimalPlaces = 0; - if (isNaN(number)) return decimalPlaces; - if (typeof number !== "number") { - number = Number(number); + if (isNaN(number2)) return decimalPlaces; + if (typeof number2 !== "number") { + number2 = Number(number2); } - var parts = number.toString().split("e"); + var parts = number2.toString().split("e"); if (parts.length === 2) { if (parts[1][0] !== "-") { return decimalPlaces; @@ -78733,9 +78733,9 @@ var require_enum_object = __commonJS({ if (!isEnumObject(enumObject)) throw new Error("not a typescript enum object"); let values = []; - for (let [name, number] of Object.entries(enumObject)) - if (typeof number == "number") - values.push({ name, number }); + for (let [name, number2] of Object.entries(enumObject)) + if (typeof number2 == "number") + values.push({ name, number: number2 }); return values; } exports2.listEnumValues = listEnumValues; @@ -92751,10 +92751,10 @@ var require_util12 = __commonJS({ return arg == null; } exports2.isNullOrUndefined = isNullOrUndefined; - function isNumber(arg) { + function isNumber2(arg) { return typeof arg === "number"; } - exports2.isNumber = isNumber; + exports2.isNumber = isNumber2; function isString3(arg) { return typeof arg === "string"; } @@ -101524,24 +101524,24 @@ var require_operators = __commonJS({ } }.call(this); } - function toIntegerOrInfinity(number) { - number = Number2(number); - if (NumberIsNaN(number)) { + function toIntegerOrInfinity(number2) { + number2 = Number2(number2); + if (NumberIsNaN(number2)) { return 0; } - if (number < 0) { - throw new ERR_OUT_OF_RANGE("number", ">= 0", number); + if (number2 < 0) { + throw new ERR_OUT_OF_RANGE("number", ">= 0", number2); } - return number; + return number2; } - function drop(number, options = void 0) { + function drop(number2, options = void 0) { if (options != null) { validateObject(options, "options"); } if ((options === null || options === void 0 ? void 0 : options.signal) != null) { validateAbortSignal(options.signal, "options.signal"); } - number = toIntegerOrInfinity(number); + number2 = toIntegerOrInfinity(number2); return async function* drop2() { var _options$signal5; if (options !== null && options !== void 0 && (_options$signal5 = options.signal) !== null && _options$signal5 !== void 0 && _options$signal5.aborted) { @@ -101552,20 +101552,20 @@ var require_operators = __commonJS({ if (options !== null && options !== void 0 && (_options$signal6 = options.signal) !== null && _options$signal6 !== void 0 && _options$signal6.aborted) { throw new AbortError(); } - if (number-- <= 0) { + if (number2-- <= 0) { yield val; } } }.call(this); } - function take(number, options = void 0) { + function take(number2, options = void 0) { if (options != null) { validateObject(options, "options"); } if ((options === null || options === void 0 ? void 0 : options.signal) != null) { validateAbortSignal(options.signal, "options.signal"); } - number = toIntegerOrInfinity(number); + number2 = toIntegerOrInfinity(number2); return async function* take2() { var _options$signal7; if (options !== null && options !== void 0 && (_options$signal7 = options.signal) !== null && _options$signal7 !== void 0 && _options$signal7.aborted) { @@ -101576,10 +101576,10 @@ var require_operators = __commonJS({ if (options !== null && options !== void 0 && (_options$signal8 = options.signal) !== null && _options$signal8 !== void 0 && _options$signal8.aborted) { throw new AbortError(); } - if (number-- > 0) { + if (number2-- > 0) { yield val; } - if (number <= 0) { + if (number2 <= 0) { return; } } @@ -124893,8 +124893,8 @@ var require_util16 = __commonJS({ parts.push(format.substring(last)); return parts.join(""); }; - util3.formatNumber = function(number, decimals, dec_point, thousands_sep) { - var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; + util3.formatNumber = function(number2, decimals, dec_point, thousands_sep) { + var n = number2, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; var d = dec_point === void 0 ? "," : dec_point; var t = thousands_sep === void 0 ? "." : thousands_sep, s = n < 0 ? "-" : ""; var i = parseInt(n = Math.abs(+n || 0).toFixed(c), 10) + ""; @@ -144502,17 +144502,47 @@ function isArray(value) { function isString(value) { return typeof value === "string"; } +function isNumber(value) { + return typeof value === "number"; +} function isStringOrUndefined(value) { return value === void 0 || isString(value); } -var string = { - validate: isString, - required: true -}; -function array(validator) { +function defaultCheck(validate) { + return (arg) => ({ unknownKeys: [], valid: validate(arg) }); +} +function makeValidator(validate, required = true) { return { - validate: (val) => { - return isArray(val) && val.every((e) => validator.validate(e)); + validate, + check: defaultCheck(validate), + required + }; +} +var string = makeValidator(isString); +var number = makeValidator(isNumber); +function array(validator) { + const validate = (val) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }; + return { + validate, + check: (val, path29) => { + const result = { valid: true, unknownKeys: [] }; + if (!isArray(val)) { + result.valid = false; + return result; + } + let index2 = 0; + for (const e of val) { + const eResult = validator.check(e, `${path29}[${index2}].`); + result.unknownKeys.push(...eResult.unknownKeys); + index2++; + if (!eResult.valid) { + result.valid = false; + continue; + } + } + return result; }, required: true }; @@ -144522,6 +144552,12 @@ function object(schema) { validate: (val) => { return isObject(val) && validateSchema(schema, val); }, + check: (val, path29) => { + if (!isObject(val)) { + return { valid: false, unknownKeys: [] }; + } + return checkSchema(schema, val, {}, path29); + }, required: true }; } @@ -144530,6 +144566,12 @@ function optionalOrNull(validator) { validate: (val) => { return val === void 0 || val === null || validator.validate(val); }, + check: (val, path29) => { + if (val === void 0 || val === null) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path29); + }, required: false }; } @@ -144538,6 +144580,12 @@ function optional(validator) { validate: (val) => { return val === void 0 || validator.validate(val); }, + check: (val, path29) => { + if (val === void 0) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path29); + }, required: false }; } @@ -144564,12 +144612,16 @@ function checkSchema(schema, obj, options = {}, path29 = "") { } continue; } - if (hasKey && !validator.validate(obj[key])) { - result.valid = false; - if (options.failFast) { - return result; + if (hasKey) { + const checkResult = validator.check(obj[key], `${path29}${key}.`); + result.unknownKeys.push(...checkResult.unknownKeys); + if (!checkResult.valid) { + result.valid = false; + if (options.failFast) { + return result; + } + continue; } - continue; } inputKeys.delete(key); } diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 71f800003..47956c938 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -103,12 +103,24 @@ test("validateSchema - validates objects", async (t) => { t.false(json.validateSchema(objectSchema, { objectKey: 123 })); }); +const checkSchemaTestSchema = { + rootKey: json.object(objectSchema), +}; + test("validateSchema - checkSchema reports unknown keys", async (t) => { - const result = json.checkSchema(testSchema, { - requiredKey: "foo", + const result = json.checkSchema(checkSchemaTestSchema, { + rootKey: { + objectKey: { + arrayKey: [], + }, + nestedExtraKey: "foo", + }, extraKey: "bar", }); t.true(result.valid); - t.deepEqual(result.unknownKeys, ["extraKey"]); + t.deepEqual( + result.unknownKeys.sort(), + ["extraKey", "rootKey.nestedExtraKey"].sort(), + ); }); diff --git a/src/json/index.ts b/src/json/index.ts index 325689d33..032952ad5 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -48,29 +48,65 @@ export function isStringOrUndefined( */ export type Validator = { validate: (val: unknown) => val is T; + check: (val: unknown, path: string) => CheckSchemaResult; required: boolean; }; +function defaultCheck( + validate: (val: unknown) => val is any, +): (arg: unknown) => CheckSchemaResult { + return (arg) => ({ unknownKeys: [], valid: validate(arg) }); +} + +function makeValidator( + validate: (arg: unknown) => arg is T, + required: boolean = true, +) { + return { + validate, + check: defaultCheck(validate), + required, + } as const satisfies Validator; +} + /** Extracts `T` from `Validator`. */ export type UnwrapValidator = V extends Validator ? A : never; /** A validator for string fields in schemas. */ -export const string = { - validate: isString, - required: true, -} as const satisfies Validator; +export const string = makeValidator(isString); /** A validator for number fields in schemas. */ -export const number = { - validate: isNumber, - required: true, -} as const satisfies Validator; +export const number = makeValidator(isNumber); /** A validator for arrays. */ export function array(validator: Validator) { + const validate = (val: unknown) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }; return { - validate: (val: unknown) => { - return isArray(val) && val.every((e) => validator.validate(e)); + validate, + check: (val: unknown, path: string) => { + const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + + if (!isArray(val)) { + result.valid = false; + return result; + } + + let index = 0; + for (const e of val) { + const eResult = validator.check(e, `${path}[${index}].`); + + result.unknownKeys.push(...eResult.unknownKeys); + index++; + + if (!eResult.valid) { + result.valid = false; + continue; + } + } + + return result; }, required: true, } as const satisfies Validator; @@ -85,6 +121,12 @@ export function object< validate: (val: unknown) => { return isObject(val) && validateSchema(schema, val); }, + check: (val, path) => { + if (!isObject(val)) { + return { valid: false, unknownKeys: [] }; + } + return checkSchema(schema, val, {}, path); + }, required: true, } as const satisfies Validator; } @@ -98,6 +140,12 @@ export function optionalOrNull(validator: Validator) { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); }, + check: (val, path) => { + if (val === undefined || val === null) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path); + }, required: false, } as const satisfies Validator; } @@ -111,6 +159,12 @@ export function optional(validator: Validator) { validate: (val: unknown): val is T | undefined => { return val === undefined || validator.validate(val); }, + check: (val, path) => { + if (val === undefined) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path); + }, required: false, } as const satisfies Validator; } @@ -193,13 +247,19 @@ export function checkSchema( } // If the property is present, validate it. - if (hasKey && !validator.validate(obj[key])) { - result.valid = false; + if (hasKey) { + const checkResult = validator.check(obj[key], `${path}${key}.`); - if (options.failFast) { - return result; + result.unknownKeys.push(...checkResult.unknownKeys); + + if (!checkResult.valid) { + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } - continue; } // If we reach this point, the key has been successfully validated.