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
JavaScript
;
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