/** * @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") { continue; } const fieldSchema = schema[key]; 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;