const DEFAULT_LABEL = "Field"; const normalizePath = (path) => { return String(path || "") .replace(/\[(\d+)\]/g, ".$1") .replace(/\[\*\]/g, ".*") .replace(/\[\]/g, ".*") .split(".") .filter(Boolean); }; const toLabel = (path) => { if (!path) { return DEFAULT_LABEL; } return String(path) .replace(/\[\d+\]/g, "") .replace(/\.\*/g, "") .replace(/[._-]+/g, " ") .replace(/\s+/g, " ") .trim() .replace(/^./, (ch) => ch.toUpperCase()); }; const collectMatches = (value, segments, currentPath = [], results = []) => { if (segments.length === 0) { results.push({ path: currentPath.join("."), value, }); return results; } const [segment, ...rest] = segments; if (segment === "*") { if (!Array.isArray(value)) { return results; } value.forEach((item, index) => { collectMatches(item, rest, [...currentPath, String(index)], results); }); return results; } if (value === null || value === undefined) { return results; } if (typeof value !== "object") { return results; } if (!Object.prototype.hasOwnProperty.call(value, segment)) { return results; } return collectMatches(value[segment], rest, [...currentPath, segment], results); }; const countWords = (value) => { const normalized = String(value || "") .replace(/\s+/g, " ") .trim(); if (!normalized) { return 0; } return normalized.split(" ").length; }; const buildErrorMessage = (label, path, maxLength, maxWords) => { const target = path ? `${label} (${path})` : label; if (maxLength && maxWords) { return `${target} must not exceed ${maxLength} characters or ${maxWords} words.`; } if (maxLength) { return `${target} must not exceed ${maxLength} characters.`; } return `${target} must not exceed ${maxWords} words.`; }; const validateLengthRules = (payload, rules = []) => { const errors = []; for (const rule of rules) { const paths = Array.isArray(rule.paths) ? rule.paths : [rule.path].filter(Boolean); for (const path of paths) { const segments = normalizePath(path); const matches = collectMatches(payload, segments); for (const match of matches) { if (typeof match.value !== "string") { continue; } const normalized = match.value.trim(); if (!normalized && rule.allowEmpty !== false) { continue; } const actualLength = normalized.length; const actualWords = countWords(normalized); const maxLength = Number(rule.maxLength); const maxWords = Number(rule.maxWords); const exceedsLength = Number.isFinite(maxLength) && maxLength > 0 && actualLength > maxLength; const exceedsWords = Number.isFinite(maxWords) && maxWords > 0 && actualWords > maxWords; if (!exceedsLength && !exceedsWords) { continue; } const label = rule.label || toLabel(path); errors.push({ path: match.path, label, message: buildErrorMessage(label, match.path, exceedsLength ? maxLength : null, exceedsWords ? maxWords : null), maxLength: Number.isFinite(maxLength) ? maxLength : undefined, maxWords: Number.isFinite(maxWords) ? maxWords : undefined, actualLength, actualWords, }); } } } return { valid: errors.length === 0, errors, }; }; const summarizeLengthErrors = (validation, limit = 1) => { if (!validation || !Array.isArray(validation.errors) || validation.errors.length === 0) { return ""; } return validation.errors .slice(0, limit) .map((error) => error.message) .join(" "); }; module.exports = { validateLengthRules, summarizeLengthErrors, toLabel, };