529 lines
20 KiB
JavaScript
529 lines
20 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.1.0
|
|
* @since 0.1.0
|
|
*/
|
|
|
|
const Logger = require("./logger");
|
|
|
|
class ValidationUtils {
|
|
constructor(IloggerEnabled, IloggerLevel) {
|
|
const loggerEnabled = IloggerEnabled || true;
|
|
const loggerLevel = IloggerLevel || "warn";
|
|
this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils');
|
|
}
|
|
|
|
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.info(`There is 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;
|
|
}
|
|
|
|
// Attempt to parse the value to the expected type if possible
|
|
switch (rules.type) {
|
|
|
|
case "number":
|
|
configValue = this.validateNumber(configValue, rules, fieldSchema, name, key);
|
|
break;
|
|
case "boolean":
|
|
configValue = this.validateBoolean(configValue, name, key);
|
|
break;
|
|
|
|
case "string":
|
|
configValue = this.validateString(configValue,rules,fieldSchema, name, key);
|
|
break;
|
|
|
|
case "array":
|
|
configValue = this.validateArray(configValue, rules, fieldSchema, name, key);
|
|
break;
|
|
|
|
case "set":
|
|
configValue = this.validateSet(configValue, rules, fieldSchema, name, key);
|
|
break;
|
|
|
|
case "object":
|
|
configValue = this.validateObject(configValue, rules, fieldSchema, name, key);
|
|
break;
|
|
|
|
case "enum":
|
|
configValue = this.validateEnum(configValue, rules, fieldSchema, name, key);
|
|
break;
|
|
|
|
case "curve":
|
|
validatedConfig[key] = this.validateCurve(configValue,fieldSchema.default);
|
|
continue;
|
|
|
|
case "machineCurve":
|
|
validatedConfig[key] = this.validateMachineCurve(configValue,fieldSchema.default);
|
|
continue;
|
|
|
|
case "integer":
|
|
validatedConfig[key] = this.validateInteger(configValue, rules, fieldSchema, name, key);
|
|
continue;
|
|
|
|
case undefined:
|
|
// If we see 'rules.schema' but no 'rules.type', treat it like an object:
|
|
if (rules.schema && !rules.type) {
|
|
// Log a warning and skip the extra pass for nested schema
|
|
this.logger.warn(
|
|
`${name}.${key} has a nested schema but no type. ` +
|
|
`Treating it as type="object" to skip extra pass.`
|
|
);
|
|
} else {
|
|
// Otherwise, fallback to your existing "validateUndefined" logic
|
|
validatedConfig[key] = this.validateUndefined(configValue, fieldSchema, name, key);
|
|
}
|
|
continue;
|
|
|
|
default:
|
|
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("default" in v){
|
|
//put the default value in the object
|
|
newObj[k] = v.default;
|
|
continue;
|
|
}
|
|
|
|
newObj[k] = this.removeUnwantedKeys(v);
|
|
}
|
|
return newObj;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
validateMachineCurve(curve, defaultCurve) {
|
|
if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
|
|
this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
|
|
return defaultCurve;
|
|
}
|
|
|
|
// Validate that nq and np exist and are objects
|
|
const { nq, np } = curve;
|
|
if (!nq || typeof nq !== "object" || !np || typeof np !== "object") {
|
|
this.logger.warn("Curve must contain valid 'nq' and 'np' objects. Defaulting to basic curve.");
|
|
return defaultCurve;
|
|
}
|
|
|
|
// Validate that each dimension key points to a valid object with x and y arrays
|
|
const validatedNq = this.validateDimensionStructure(nq, "nq");
|
|
const validatedNp = this.validateDimensionStructure(np, "np");
|
|
|
|
if (!validatedNq || !validatedNp) {
|
|
return defaultCurve;
|
|
}
|
|
|
|
return { nq: validatedNq, np: validatedNp }; // Return the validated curve
|
|
}
|
|
|
|
validateCurve(curve, defaultCurve) {
|
|
if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
|
|
this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
|
|
return defaultCurve;
|
|
}
|
|
|
|
// Validate that each dimension key points to a valid object with x and y arrays
|
|
const validatedCurve = this.validateDimensionStructure(curve, "curve");
|
|
if (!validatedCurve) {
|
|
return defaultCurve;
|
|
}
|
|
|
|
return validatedCurve; // Return the validated curve
|
|
}
|
|
|
|
validateDimensionStructure(dimension, name) {
|
|
const validatedDimension = {};
|
|
|
|
for (const [key, value] of Object.entries(dimension)) {
|
|
// Validate that each key points to an object with x and y arrays
|
|
if (typeof value !== "object") {
|
|
this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
|
|
return false;
|
|
}
|
|
// Validate that x and y are arrays
|
|
else if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
|
|
this.logger.warn(`Dimension '${name}' key '${key}' is missing x or y arrays. Converting to arrays.`);
|
|
// Try to convert to arrays first
|
|
value.x = Object.values(value.x);
|
|
value.y = Object.values(value.y);
|
|
|
|
// If still not arrays return false
|
|
if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
|
|
this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
|
|
return false;
|
|
}
|
|
}
|
|
// Validate that x and y arrays are the same length
|
|
else if (value.x.length !== value.y.length) {
|
|
this.logger.warn(`Dimension '${name}' key '${key}' has mismatched x and y lengths. Ignoring this key.`);
|
|
return false;
|
|
}
|
|
// Validate that x values are in ascending order
|
|
else if (!this.isSorted(value.x)) {
|
|
this.logger.warn(`Dimension '${name}' key '${key}' has unsorted x values. Sorting...`);
|
|
return false;
|
|
}
|
|
// Validate that x values are unique
|
|
else if (!this.isUnique(value.x)) {
|
|
this.logger.warn(`Dimension '${name}' key '${key}' has duplicate x values. Removing duplicates...`);
|
|
return false;
|
|
}
|
|
// Validate that y values are numbers
|
|
else if (!this.areNumbers(value.y)) {
|
|
this.logger.warn(`Dimension '${name}' key '${key}' has non-numeric y values. Ignoring this key.`);
|
|
return false;
|
|
}
|
|
|
|
validatedDimension[key] = value;
|
|
}
|
|
return validatedDimension;
|
|
}
|
|
|
|
isSorted(arr) {
|
|
return arr.every((_, i) => i === 0 || arr[i] >= arr[i - 1]);
|
|
}
|
|
|
|
isUnique(arr) {
|
|
return new Set(arr).size === arr.length;
|
|
}
|
|
|
|
areNumbers(arr) {
|
|
return arr.every((x) => typeof x === "number");
|
|
}
|
|
|
|
validateNumber(configValue, rules, fieldSchema, name, key) {
|
|
|
|
if (typeof configValue !== "number") {
|
|
const parsedValue = parseFloat(configValue);
|
|
if (!isNaN(parsedValue)) {
|
|
this.logger.warn(`${name}.${key} was parsed to a number: ${configValue} -> ${parsedValue}`);
|
|
configValue = parsedValue;
|
|
}
|
|
}
|
|
|
|
if (rules.min !== undefined && configValue < rules.min) {
|
|
this.logger.warn(
|
|
`${name}.${key} is below the minimum (${rules.min}). Using default value.`
|
|
);
|
|
return fieldSchema.default;
|
|
}
|
|
if (rules.max !== undefined && configValue > rules.max) {
|
|
this.logger.warn(
|
|
`${name}.${key} exceeds the maximum (${rules.max}). Using default value.`
|
|
);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
this.logger.debug(`${name}.${key} is a valid number: ${configValue}`);
|
|
|
|
return configValue;
|
|
}
|
|
|
|
|
|
validateInteger(configValue, rules, fieldSchema, name, key) {
|
|
if (typeof configValue !== "number" || !Number.isInteger(configValue)) {
|
|
const parsedValue = parseInt(configValue, 10);
|
|
if (!isNaN(parsedValue) && Number.isInteger(parsedValue)) {
|
|
this.logger.warn(`${name}.${key} was parsed to an integer: ${configValue} -> ${parsedValue}`);
|
|
configValue = parsedValue;
|
|
} else {
|
|
this.logger.warn(`${name}.${key} is not a valid integer. Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
}
|
|
|
|
if (rules.min !== undefined && configValue < rules.min) {
|
|
this.logger.warn(`${name}.${key} is below the minimum integer value (${rules.min}). Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
if (rules.max !== undefined && configValue > rules.max) {
|
|
this.logger.warn(`${name}.${key} exceeds the maximum integer value (${rules.max}). Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
this.logger.debug(`${name}.${key} is a valid integer: ${configValue}`);
|
|
return configValue;
|
|
}
|
|
|
|
validateBoolean(configValue, name, key) {
|
|
if (typeof configValue !== "boolean") {
|
|
if (configValue === "true" || configValue === "false") {
|
|
const parsedValue = configValue === "true";
|
|
this.logger.debug(`${name}.${key} was parsed to a boolean: ${configValue} -> ${parsedValue}`);
|
|
configValue = parsedValue;
|
|
}
|
|
}
|
|
return configValue;
|
|
}
|
|
|
|
validateString(configValue, rules, fieldSchema, name, key) {
|
|
let newConfigValue = configValue;
|
|
|
|
if (typeof configValue !== "string") {
|
|
//check if the value is nullable
|
|
if(rules.nullable){
|
|
if(configValue === null){
|
|
return null;
|
|
}
|
|
}
|
|
|
|
this.logger.warn(`${name}.${key} is not a string. Trying to convert to string.`);
|
|
newConfigValue = String(configValue); // Coerce to string if not already
|
|
}
|
|
|
|
//check if the string is a valid string after conversion
|
|
if (typeof newConfigValue !== "string") {
|
|
this.logger.warn(`${name}.${key} is not a valid string. Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
return newConfigValue;
|
|
}
|
|
|
|
validateSet(configValue, rules, fieldSchema, name, key) {
|
|
// 1. Ensure we have a Set. If not, use default.
|
|
if (!(configValue instanceof Set)) {
|
|
this.logger.info(`${name}.${key} is not a Set. Converting to one using default value.`);
|
|
return new Set(fieldSchema.default);
|
|
}
|
|
|
|
// 2. Convert the Set to an array for easier filtering.
|
|
const validatedArray = [...configValue]
|
|
.filter((item) => {
|
|
// 3. Filter based on `rules.itemType`.
|
|
switch (rules.itemType) {
|
|
case "number":
|
|
return typeof item === "number";
|
|
case "string":
|
|
return typeof item === "string";
|
|
case "null":
|
|
// "null" might mean no type restriction (your usage may vary).
|
|
return true;
|
|
default:
|
|
// Fallback if itemType is something else
|
|
return typeof item === rules.itemType;
|
|
}
|
|
})
|
|
.slice(0, rules.maxLength || Infinity);
|
|
|
|
// 4. Check if the filtered array meets the minimum length.
|
|
if (validatedArray.length < (rules.minLength || 1)) {
|
|
this.logger.warn(
|
|
`${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
|
|
);
|
|
return new Set(fieldSchema.default);
|
|
}
|
|
|
|
// 5. Return a new Set containing only the valid items.
|
|
return new Set(validatedArray);
|
|
}
|
|
|
|
validateArray(configValue, rules, fieldSchema, name, key) {
|
|
if (!Array.isArray(configValue)) {
|
|
this.logger.info(`${name}.${key} is not an array. Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
// Validate individual items in the array
|
|
const validatedArray = configValue
|
|
.filter((item) => {
|
|
switch (rules.itemType) {
|
|
case "number":
|
|
return typeof item === "number";
|
|
case "string":
|
|
return typeof item === "string";
|
|
case "null":
|
|
// anything goes
|
|
return true;
|
|
default:
|
|
return typeof item === rules.itemType;
|
|
}
|
|
})
|
|
.slice(0, rules.maxLength || Infinity);
|
|
|
|
if (validatedArray.length < (rules.minLength || 1)) {
|
|
this.logger.warn(
|
|
`${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
|
|
);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
return validatedArray;
|
|
}
|
|
|
|
validateObject(configValue, rules, fieldSchema, name, key) {
|
|
if (typeof configValue !== "object" || Array.isArray(configValue)) {
|
|
this.logger.warn(`${name}.${key} is not a valid object. Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
if (rules.schema) {
|
|
// Recursively validate nested objects if a schema is defined
|
|
return this.validateSchema(configValue || {}, rules.schema, `${name}.${key}`);
|
|
} else {
|
|
// If no schema is defined, log a warning and use the default
|
|
this.logger.warn(`${name}.${key} is an object with no schema. Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
}
|
|
|
|
validateEnum(configValue, rules, fieldSchema, name, key) {
|
|
|
|
if (Array.isArray(rules.values)) {
|
|
|
|
//if value is null take default
|
|
if(configValue === null){
|
|
this.logger.warn(`${name}.${key} is null. Using default value.`);
|
|
return fieldSchema.default;
|
|
}
|
|
|
|
const validValues = rules.values.map(e => e.value.toLowerCase());
|
|
|
|
//remove caps
|
|
configValue = configValue.toLowerCase();
|
|
|
|
if (!validValues.includes(configValue)) {
|
|
this.logger.warn(
|
|
`${name}.${key} has an invalid value : ${configValue}. Allowed values: [${validValues.join(", ")}]. Using default value.`
|
|
);
|
|
return fieldSchema.default;
|
|
}
|
|
} else {
|
|
this.logger.warn(
|
|
`${name}.${key} is an enum with no 'values' array. Using default value.`
|
|
);
|
|
return fieldSchema.default;
|
|
}
|
|
return configValue;
|
|
}
|
|
|
|
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;
|