UNPKG

structure-validation

Version:

A Node.js CLI tool for validating codebase folder and file structure using a clean declarative configuration. Part of the guardz ecosystem for comprehensive TypeScript development.

184 lines 9.19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ValidationService = void 0; /** * Domain service for validating file structure against rules */ class ValidationService { /** * Validate files against validation rules */ validateFiles(files, rules, verifyRoot = false) { const errors = []; const suggestions = []; for (const file of files) { const fileValidation = this.validateFile(file, rules, verifyRoot); errors.push(...fileValidation.errors); suggestions.push(...fileValidation.suggestions); } return { isValid: errors.length === 0, errors, suggestions }; } /** * Validate a single file against all rules */ validateFile(file, rules, verifyRoot = false) { const errors = []; const suggestions = []; // Skip root folder validation if verifyRoot is false if (!verifyRoot && file.folder === 'root') { return { errors, suggestions }; } // Find applicable rules for this file const applicableRules = rules.filter(rule => rule.isFileInFolder(file.folder)); // Check if file matches variable placeholder pattern first (e.g., {a}, {b}, {folderName}, etc.) const variableRule = rules.find(rule => rule.folder.match(/^\{[^}]+\}$/)); const matchesAPattern = variableRule && variableRule.isFileMatchingPatterns(file.filename, file.folder.split('/').pop()); // Special handling for root folder with {root} pattern if (file.folder === 'root' && verifyRoot) { const rootRule = rules.find(rule => rule.folder === '{root}'); if (rootRule) { // For root files, we need to check if the file matches any pattern in the {root} rule // The patterns might contain {root} which should be replaced with 'root' const matchesRootPattern = rootRule.patterns.some(pattern => { // Replace {root} in the pattern with 'root' for matching const processedPattern = pattern.replace(/\{root\}/g, 'root'); const regexPattern = this.globToRegex(processedPattern); return regexPattern.test(file.filename); }); if (!matchesRootPattern) { errors.push({ file, type: 'pattern_mismatch', message: `File ${file.filename} in root folder doesn't match any allowed pattern`, expectedPatterns: rootRule.patterns }); // Add suggestions for moving to appropriate folders const suggestedFolder = file.getSuggestedFolder(rules); if (suggestedFolder && suggestedFolder !== '{root}') { suggestions.push({ file, suggestedFolder, reason: `File matches patterns for ${suggestedFolder} folder` }); } } return { errors, suggestions }; } } if (applicableRules.length === 0) { // No rules found for this folder const suggestedFolder = file.getSuggestedFolder(rules); if (suggestedFolder) { suggestions.push({ file, suggestedFolder, reason: `File matches patterns for ${suggestedFolder} folder` }); } return { errors, suggestions }; } // Check if file matches any pattern in applicable rules const matchingRules = applicableRules.filter(rule => { if (rule.folder.match(/^\{[^}]+\}$/)) { return rule.isFileMatchingPatterns(file.filename, file.folder.split('/').pop()); } return rule.isFileMatchingPatterns(file.filename, file.folder); }); // Also check if file matches patterns for other folders const allMatchingRules = rules.filter(rule => { if (rule.folder.match(/^\{[^}]+\}$/)) { return rule.isFileMatchingPatterns(file.filename, file.folder.split('/').pop()); } return rule.isFileMatchingPatterns(file.filename, file.folder); }); if (matchingRules.length === 0) { // Check if there's a global wildcard rule that matches const globalWildcardRule = applicableRules.find(rule => rule.folder === '**'); if (globalWildcardRule && globalWildcardRule.isFileMatchingPatterns(file.filename, file.folder)) { // File is allowed by global wildcard, no error return { errors, suggestions }; } // File doesn't match any pattern in its folder const expectedPatterns = applicableRules .filter(rule => rule.folder !== '**' && !rule.folder.match(/^\{[^}]+\}$/)) .flatMap(rule => rule.patterns); errors.push({ file, type: 'pattern_mismatch', message: `File ${file.filename} in ${file.folder}/ doesn't match any allowed pattern`, expectedPatterns }); // Add suggestion if file matches patterns for another folder // But only if it doesn't already match a {a} pattern if (!matchesAPattern) { const suggestedFolder = file.getSuggestedFolder(rules); if (suggestedFolder && suggestedFolder !== file.folder) { // Check if the file is already in a folder that matches the suggested folder const isAlreadyInCorrectFolder = file.folder.includes(`/${suggestedFolder}/`) || file.folder.endsWith(`/${suggestedFolder}`); if (!isAlreadyInCorrectFolder) { suggestions.push({ file, suggestedFolder, reason: `File matches patterns for ${suggestedFolder} folder` }); } } } } else { // Check if file is in the correct folder for its matching rules const correctFolders = matchingRules .filter(rule => rule.folder !== '**' && !rule.folder.match(/^\{[^}]+\}$/)) .map(rule => rule.folder); if (correctFolders.length > 0) { // Check if the file is already in a folder that matches the correct folder const isAlreadyInCorrectFolder = correctFolders.some(folder => file.folder === folder || file.folder.includes(`/${folder}/`) || file.folder.endsWith(`/${folder}`)); if (!isAlreadyInCorrectFolder) { const suggestedFolder = file.getSuggestedFolder(rules); errors.push({ file, type: 'folder_mismatch', message: `File ${file.filename} should be in one of: ${correctFolders.join(', ')}`, expectedFolder: (suggestedFolder || correctFolders[0]) ?? undefined }); } } } // Check if file matches patterns for other folders (folder mismatch) // But only if the file doesn't already match a variable placeholder pattern and doesn't match its own folder if (!matchesAPattern && matchingRules.length === 0) { const otherMatchingRules = allMatchingRules.filter(rule => rule.folder !== '**' && !rule.folder.match(/^\{[^}]+\}$/) && rule.folder !== file.folder); if (otherMatchingRules.length > 0) { // Check if the file is already in a folder that matches the suggested folder const isAlreadyInCorrectFolder = otherMatchingRules.some(rule => file.folder.includes(`/${rule.folder}/`) || file.folder.endsWith(`/${rule.folder}`)); if (!isAlreadyInCorrectFolder) { const correctFolders = otherMatchingRules.map(rule => rule.folder); const suggestedFolder = file.getSuggestedFolder(rules); errors.push({ file, type: 'folder_mismatch', message: `File ${file.filename} should be in one of: ${correctFolders.join(', ')}`, expectedFolder: (suggestedFolder || correctFolders[0]) ?? undefined }); } } } return { errors, suggestions }; } /** * Convert glob pattern to regex */ globToRegex(pattern) { const escaped = pattern .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') .replace(/\\\*/g, '.*') .replace(/\\\?/g, '.'); return new RegExp(`^${escaped}$`); } } exports.ValidationService = ValidationService; //# sourceMappingURL=ValidationService.js.map