migrateConfig stamps a version string into config schemas. validateSchema then iterates the string's character indices, causing infinite recursion. Skip the 'version' key and guard against any non-object schema entries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
239 lines
9.7 KiB
JavaScript
239 lines
9.7 KiB
JavaScript
/**
|
|
* @file validation.js
|
|
*
|
|
* Permission is hereby granted to any person obtaining a copy of this software
|
|
* and associated documentation files (the "Software"), to use it for personal
|
|
* or non-commercial purposes, with the following restrictions:
|
|
*
|
|
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
|
* be copied, merged, distributed, sublicensed, or sold without explicit
|
|
* prior written permission from the author.
|
|
*
|
|
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
|
* a valid license, obtainable only with the explicit consent of the author.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
|
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
* Ownership of this code remains solely with the original author. Unauthorized
|
|
* use of this Software is strictly prohibited.
|
|
|
|
* @summary Validation utility for validating and constraining configuration values.
|
|
* @description Validation utility for validating and constraining configuration values.
|
|
* @module ValidationUtils
|
|
* @requires Logger
|
|
* @exports ValidationUtils
|
|
* @version 0.2.0
|
|
* @since 0.1.0
|
|
*/
|
|
|
|
const Logger = require("./logger");
|
|
const { validateNumber, validateInteger, validateBoolean, validateString, validateEnum } = require("./validators/typeValidators");
|
|
const { validateArray, validateSet, validateObject } = require("./validators/collectionValidators");
|
|
const { validateCurve, validateMachineCurve } = require("./validators/curveValidator");
|
|
|
|
// Strategy registry: maps rules.type to a handler function
|
|
const VALIDATORS = {
|
|
number: (cv, rules, fs, name, key, logger) => validateNumber(cv, rules, fs, name, key, logger),
|
|
integer: (cv, rules, fs, name, key, logger) => validateInteger(cv, rules, fs, name, key, logger),
|
|
boolean: (cv, _rules, _fs, name, key, logger) => validateBoolean(cv, name, key, logger),
|
|
string: (cv, rules, fs, name, key, logger) => validateString(cv, rules, fs, name, key, logger),
|
|
enum: (cv, rules, fs, name, key, logger) => validateEnum(cv, rules, fs, name, key, logger),
|
|
array: (cv, rules, fs, name, key, logger) => validateArray(cv, rules, fs, name, key, logger),
|
|
set: (cv, rules, fs, name, key, logger) => validateSet(cv, rules, fs, name, key, logger),
|
|
};
|
|
|
|
class ValidationUtils {
|
|
constructor(IloggerEnabled, IloggerLevel) {
|
|
const loggerEnabled = IloggerEnabled ?? true;
|
|
const loggerLevel = IloggerLevel ?? "warn";
|
|
this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils');
|
|
this._onceLogCache = new Set();
|
|
}
|
|
|
|
_logOnce(level, onceKey, message) {
|
|
if (onceKey && this._onceLogCache.has(onceKey)) {
|
|
return;
|
|
}
|
|
if (onceKey) {
|
|
this._onceLogCache.add(onceKey);
|
|
}
|
|
if (typeof this.logger?.[level] === "function") {
|
|
this.logger[level](message);
|
|
}
|
|
}
|
|
|
|
constrain(value, min, max) {
|
|
if (typeof value !== "number") {
|
|
this.logger?.warn(`Value '${value}' is not a number. Defaulting to ${min}.`);
|
|
return min;
|
|
}
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
validateSchema(config, schema, name) {
|
|
|
|
const validatedConfig = {};
|
|
let configValue;
|
|
|
|
// 1. Remove any unknown keys (keys not defined in the schema).
|
|
// Log a warning and omit them from the final config.
|
|
for (const key of Object.keys(config)) {
|
|
if (!(key in schema)) {
|
|
this.logger.warn(
|
|
`[${name}] Unknown key '${key}' found in config. Removing it.`
|
|
);
|
|
delete config[key];
|
|
}
|
|
}
|
|
|
|
// Validate each key in the schema and loop over wildcards if they are not in schema
|
|
for ( const key in schema ) {
|
|
|
|
if (key === "rules" || key === "description" || key === "schema" || key === "version") {
|
|
continue;
|
|
}
|
|
|
|
const fieldSchema = schema[key];
|
|
|
|
// Skip non-object schema entries (e.g. primitive values injected by migration)
|
|
if (fieldSchema === null || typeof fieldSchema !== 'object') {
|
|
this.logger.debug(`${name}.${key} has a non-object schema entry (${typeof fieldSchema}). Skipping.`);
|
|
validatedConfig[key] = fieldSchema;
|
|
continue;
|
|
}
|
|
|
|
const { rules = {} } = fieldSchema;
|
|
|
|
// Default to the schema's default value if the key is missing
|
|
if (config[key] === undefined) {
|
|
if (fieldSchema.default === undefined) {
|
|
// If there's a nested schema, go deeper with an empty object rather than logging "no rule"
|
|
if (rules.schema) {
|
|
this.logger.warn(`${name}.${key} has no default, but has a nested schema.`);
|
|
validatedConfig[key] = this.validateSchema({}, rules.schema, `${name}.${key}`);
|
|
}
|
|
else {
|
|
this.logger.info(
|
|
`There is no rule for ${name}.${key} and no default value. ` +
|
|
`Using full schema value but validating deeper levels first...`
|
|
);
|
|
|
|
const SubObject = this.validateSchema({}, fieldSchema, `${name}.${key}`);
|
|
|
|
validatedConfig[key] = SubObject;
|
|
|
|
continue;
|
|
}
|
|
} else {
|
|
this.logger.debug(`No value provided for ${name}.${key}. Using default value.`);
|
|
configValue = fieldSchema.default;
|
|
}
|
|
//continue;
|
|
} else {
|
|
// Use the provided value if it exists, otherwise use the default value
|
|
configValue = config[key] !== undefined ? config[key] : fieldSchema.default;
|
|
}
|
|
|
|
// Handle curve types (they use continue, so handle separately)
|
|
if (rules.type === "curve") {
|
|
validatedConfig[key] = validateCurve(configValue, fieldSchema.default, this.logger);
|
|
continue;
|
|
}
|
|
if (rules.type === "machineCurve") {
|
|
validatedConfig[key] = validateMachineCurve(configValue, fieldSchema.default, this.logger);
|
|
continue;
|
|
}
|
|
|
|
// Handle object type (needs recursive validateSchema reference)
|
|
if (rules.type === "object") {
|
|
validatedConfig[key] = validateObject(
|
|
configValue, rules, fieldSchema, name, key,
|
|
(c, s, n) => this.validateSchema(c, s, n),
|
|
this.logger
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Handle undefined type
|
|
if (rules.type === undefined) {
|
|
if (rules.schema && !rules.type) {
|
|
this.logger.warn(
|
|
`${name}.${key} has a nested schema but no type. ` +
|
|
`Treating it as type="object" to skip extra pass.`
|
|
);
|
|
} else {
|
|
validatedConfig[key] = this.validateUndefined(configValue, fieldSchema, name, key);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Use the strategy registry for all other types
|
|
const handler = VALIDATORS[rules.type];
|
|
if (handler) {
|
|
configValue = handler(configValue, rules, fieldSchema, name, key, this.logger);
|
|
} else {
|
|
this.logger.warn(`${name}.${key} has an unknown validation type: ${rules.type}. Skipping validation.`);
|
|
validatedConfig[key] = fieldSchema.default;
|
|
continue;
|
|
}
|
|
|
|
// Assign the validated or converted value
|
|
validatedConfig[key] = configValue;
|
|
}
|
|
|
|
// Ignore unknown keys by not processing them at all
|
|
this.logger.info(`Validation completed for ${name}.`);
|
|
|
|
return validatedConfig;
|
|
}
|
|
|
|
removeUnwantedKeys(obj) {
|
|
|
|
if (Array.isArray(obj)) {
|
|
return obj.map((item) => this.removeUnwantedKeys(item));
|
|
}
|
|
if (obj && typeof obj === "object") {
|
|
const newObj = {};
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
|
|
// Skip or remove keys like 'default', 'rules', 'description', etc.
|
|
if (["rules", "description"].includes(k)) {
|
|
continue;
|
|
}
|
|
|
|
if(v && typeof v === "object" && "default" in v){
|
|
//put the default value in the object
|
|
newObj[k] = v.default;
|
|
continue;
|
|
}
|
|
|
|
newObj[k] = this.removeUnwantedKeys(v);
|
|
}
|
|
return newObj;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
validateUndefined(configValue, fieldSchema, name, key) {
|
|
if (typeof configValue === "object" && !Array.isArray(configValue)) {
|
|
|
|
this.logger.debug(`${name}.${key} has no defined rules but is an object going 1 level deeper.`);
|
|
|
|
// Recursively validate the nested object
|
|
return this.validateSchema( configValue || {}, fieldSchema, `${name}.${key}`);
|
|
}
|
|
else {
|
|
this.logger.warn(`${name}.${key} has no defined rules. Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = ValidationUtils;
|