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.
202 lines (201 loc) • 7.96 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigValidator = void 0;
/**
* Application service for validating configuration files
*/
class ConfigValidator {
/**
* Validate configuration object against schema
*/
validateConfig(config) {
const errors = [];
const warnings = [];
// Check if config is an object
if (!config || typeof config !== 'object') {
errors.push('Configuration must be an object');
return { isValid: false, errors, warnings };
}
// Handle new structure format
let structure;
let root;
if (config.structure) {
// New format with structure property
structure = config.structure;
root = config.root;
// Validate root if provided
if (root && typeof root !== 'string') {
errors.push('Root must be a string');
}
}
else {
// Legacy format (direct structure)
structure = config;
root = undefined;
}
// Validate each folder configuration
for (const [folder, patterns] of Object.entries(structure)) {
const folderValidation = this.validateFolderConfig(folder, patterns);
errors.push(...folderValidation.errors);
warnings.push(...folderValidation.warnings);
}
// Check for required global patterns
if (!structure['**'] && !structure['global']) {
warnings.push('Consider adding global patterns with "**" key for common files like .md, .json, etc.');
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Validate individual folder configuration
*/
validateFolderConfig(folder, patterns) {
const errors = [];
const warnings = [];
// Validate folder name
if (!folder || typeof folder !== 'string') {
errors.push('Folder name must be a non-empty string');
return { errors, warnings };
}
// Check for reserved folder names
if (folder === 'root' || folder === 'global') {
warnings.push(`"${folder}" is a reserved folder name, consider using a different name`);
}
// Validate patterns
if (Array.isArray(patterns)) {
// Simple array format
if (patterns.length === 0) {
errors.push(`No patterns specified for folder "${folder}"`);
}
for (const pattern of patterns) {
if (typeof pattern !== 'string') {
errors.push(`Pattern in folder "${folder}" must be a string, got ${typeof pattern}`);
}
else if (!pattern.trim()) {
errors.push(`Empty pattern found in folder "${folder}"`);
}
else {
const patternValidation = this.validatePattern(pattern);
if (!patternValidation.isValid) {
errors.push(`Invalid pattern "${pattern}" in folder "${folder}": ${patternValidation.error}`);
}
}
}
}
else if (typeof patterns === 'object' && patterns !== null) {
// Advanced object format
const advancedPatterns = patterns;
if (!Array.isArray(advancedPatterns.patterns)) {
errors.push(`"patterns" must be an array in folder "${folder}"`);
}
else if (advancedPatterns.patterns.length === 0) {
errors.push(`No patterns specified for folder "${folder}"`);
}
else {
// Validate patterns array
for (const pattern of advancedPatterns.patterns) {
if (typeof pattern !== 'string') {
errors.push(`Pattern in folder "${folder}" must be a string, got ${typeof pattern}`);
}
else {
const patternValidation = this.validatePattern(pattern);
if (!patternValidation.isValid) {
errors.push(`Invalid pattern "${pattern}" in folder "${folder}": ${patternValidation.error}`);
}
}
}
// Validate exclude patterns if present
if (advancedPatterns.exclude && Array.isArray(advancedPatterns.exclude)) {
for (const excludePattern of advancedPatterns.exclude) {
if (typeof excludePattern !== 'string') {
errors.push(`Exclude pattern in folder "${folder}" must be a string`);
}
else {
const patternValidation = this.validatePattern(excludePattern);
if (!patternValidation.isValid) {
errors.push(`Invalid exclude pattern "${excludePattern}" in folder "${folder}": ${patternValidation.error}`);
}
}
}
}
// Validate maxDepth if present
if (advancedPatterns.maxDepth !== undefined) {
if (typeof advancedPatterns.maxDepth !== 'number' || advancedPatterns.maxDepth < 0) {
errors.push(`maxDepth in folder "${folder}" must be a non-negative number`);
}
}
// Validate validator if present
if (advancedPatterns.validator !== undefined && typeof advancedPatterns.validator !== 'function') {
errors.push(`validator in folder "${folder}" must be a function`);
}
}
}
else {
errors.push(`Invalid configuration for folder "${folder}": must be an array or object`);
}
return { errors, warnings };
}
/**
* Validate individual pattern
*/
validatePattern(pattern) {
if (!pattern || typeof pattern !== 'string') {
return { isValid: false, error: 'Pattern must be a non-empty string' };
}
// Check for basic glob syntax
const validGlobChars = /^[a-zA-Z0-9.*?{}[\]()|\\\/\-_]+$/;
if (!validGlobChars.test(pattern)) {
return { isValid: false, error: 'Pattern contains invalid characters' };
}
// Check for balanced braces in variable placeholders
const braceCount = (pattern.match(/\{/g) || []).length;
const closeBraceCount = (pattern.match(/\}/g) || []).length;
if (braceCount !== closeBraceCount) {
return { isValid: false, error: 'Unbalanced braces in variable placeholder' };
}
// Check for valid variable placeholder syntax
const variablePattern = /\{[^}]+\}/g;
const matches = pattern.match(variablePattern);
if (matches) {
for (const match of matches) {
if (match.length < 3) { // {a} is minimum
return { isValid: false, error: 'Invalid variable placeholder syntax' };
}
}
}
return { isValid: true };
}
/**
* Get configuration schema for documentation
*/
getSchema() {
return `
Configuration Schema:
{
"folderName": string[] | {
patterns: string[],
exclude?: string[],
nested?: boolean,
maxDepth?: number,
validator?: (file: any) => boolean
}
}
Examples:
{
"components": ["*.tsx", "*.jsx"],
"services": {
patterns: ["*.service.ts"],
exclude: ["*.test.ts"],
nested: true,
maxDepth: 2
},
"**": ["*.md", "*.json"] // Global patterns
}
`;
}
}
exports.ConfigValidator = ConfigValidator;
//# sourceMappingURL=ConfigValidator.js.map