fsvalidator 0.2.0

A file structure validator
Documentation
# File Structure Validator (fsvalidator)

A Rust library for validating real filesystem directories against a declarative, strongly-typed schema. Useful for enforcing project structure, data ingestion layout, or configuration rules.

[![Crates.io](https://img.shields.io/crates/v/fsvalidator.svg)](https://crates.io/crates/fsvalidator)
[![Documentation](https://docs.rs/fsvalidator/badge.svg)](https://docs.rs/fsvalidator)

## Features

- **Flexible Matching**: Validate files and directories using both literal names and regex patterns
- **Nested Hierarchies**: Support for complex directory trees with arbitrary depth
- **Template System**: Reuse common structures via a template reference system
- **Strictness Controls**: Fine-grained control over validation strictness (required files, restricted directories)
- **Format Options**: Define validation rules in TOML or JSON formats
- **Categorized Errors**: Validation errors are categorized for programmatic usage and visualization
- **Colorful Display**: Eye-catching, symbol-rich display with helpful visual indicators
- **Comprehensive Validation**: Reports all validation errors with clear, hierarchical organization

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
fsvalidator = { version = "0.2.0", features = ["toml"] } # or "json" feature
```

## Quick Start

```rust
use anyhow::Result;
use fsvalidator::from_toml;
use fsvalidator::display::{format_validation_result, COLORS, SYMBOLS};

fn main() -> Result<()> {
    // Print a colorful header
    println!("{}{}Filesystem Validator{}", COLORS.bold, COLORS.blue, COLORS.reset);
    
    // Load structure definition from TOML
    let root = from_toml("path/to/fsvalidator.toml")?;

    // Display the parsed structure (optional)
    println!("Structure Definition:\n{root}");

    let path = "./path/to/validate";
    
    // Validate with categorized errors
    let result = root.validate(path);
    
    // Display results with colorful, symbolic output
    println!("{}", format_validation_result(&result, path));
    
    // Programmatically check results
    if let Err(errors) = &result {
        // You can check error categories for different handling
        let has_missing = errors.children.iter()
            .any(|err| matches!(err.category, fsvalidator::ErrorCategory::Missing));
            
        if has_missing {
            println!("{}{}Missing required files!{}", 
                    COLORS.yellow, SYMBOLS.warning, COLORS.reset);
        }
    }

    Ok(())
}
```

## Definition Format

### TOML Example

```toml
# Root directory definition
[root]
type = "dir"
name = "project"
required = true
allow_defined_only = true
excluded = ["fsvalidator\\..*"]

# Define children of the root directory
[[root.children]]
type = "file"
name = "README.md"
required = true

[[root.children]]
type = "dir"
name = "src"
required = true

# Define children of the src directory
[[root.children.children]]
type = "file"
pattern = ".*\.rs"
required = true

# Reference to a template
[[root.children]]
type = "ref"
ref = "test_directory"

# Template definition
[template.test_directory]
type = "dir"
name = "tests"

[[template.test_directory.children]]
type = "file"
pattern = "test_.*\.rs"

# Global settings (applied to all nodes unless overridden)
[global]
required = false
allow_defined_only = false
excluded = ["^\\..*", "\\.DS_Store"]
```

### JSON Example

```json
{
  "root": {
    "type": "dir",
    "name": "project",
    "required": true,
    "allow_defined_only": true,
    "children": [
      {
        "type": "file",
        "name": "README.md",
        "required": true
      },
      {
        "type": "dir",
        "name": "src",
        "required": true,
        "children": [
          {
            "type": "file",
            "pattern": ".*\.rs",
            "required": true
          }
        ]
      },
      {
        "type": "ref",
        "ref": "test_directory"
      }
    ]
  },
  "template": {
    "test_directory": {
      "type": "dir",
      "name": "tests",
      "required": false,
      "children": [
        {
          "type": "file",
          "pattern": "test_.*\.rs",
          "required": false
        }
      ]
    }
  },
  "global": {
    "required": false,
    "allow_defined_only": false
  }
}
```

## Definition Structure

### Node Types

- `dir`: A directory node
- `file`: A file node
- `ref`: A reference to a template

### Node Properties

- `name`: Literal name of the file or directory
- `pattern`: Regex pattern to match against file or directory name (use either `name` or `pattern`, not both)
- `required`: Whether the node must exist (default: false)
- `allow_defined_only`: For directories, whether only defined children are allowed (default: false)
- `excluded`: Regex pattern to exclude files/dirs when validating child nodes
- `children`: List of child nodes (only valid for `dir` type)
- `ref`: Template reference name (only valid for `ref` type)

### Special Sections

- `root`: The root node of the validation tree
- `template`: Dictionary of reusable node templates
- `global`: Global settings applied to all nodes unless overridden
- `config`: Configuration options for the validator

## Model Structure

```rust
// Main node types
enum Node {
    Dir(Rc<RefCell<DirNode>>),
    File(Rc<RefCell<FileNode>>),
}

// Directory node with children
struct DirNode {
    name: NodeName,
    children: Vec<Node>,
    required: bool,
    allow_defined_only: bool,
}

// File node
struct FileNode {
    name: NodeName,
    required: bool,
}

// Node name (literal or pattern)
enum NodeName {
    Literal(String),
    Pattern(String), // Regex pattern
}
```

## Use Cases

- Enforcing consistent project layouts
- Validating data pipeline inputs/outputs
- Ensuring configuration directories are correctly structured
- Verifying deployment artifacts
- Testing file-based APIs

## Advanced Usage

### Programmatic Structure Creation

You can create validation structures programmatically:

```rust
use fsvalidator::model::{DirNode, FileNode, Node, NodeName};

// Create a file node
let readme = FileNode::new(NodeName::Literal("README.md".to_string()), true);

// Create a directory with pattern-matched files
let src_files = FileNode::new(NodeName::Pattern(".*\.rs".to_string()), true);
let src_dir = DirNode::new(
    NodeName::Literal("src".to_string()),
    vec![src_files],
    true,
    false,
    vec!["excluded_pattern"],
);

// Create the project root
let project = DirNode::new(
    NodeName::Literal("project".to_string()),
    vec![readme, src_dir],
    true,
    true,
    vec!["excluded_pattern"],
);

// Validate with colorful display
let path = "path/to/project";
let result = project.validate(path);
println!("{}", format_validation_result(&result, path));

// Process errors programmatically by category
if let Err(errors) = &result {
    // Count errors by category
    let missing_count = count_errors_by_category(errors, fsvalidator::ErrorCategory::Missing);
    let unexpected_count = count_errors_by_category(errors, fsvalidator::ErrorCategory::Unexpected);
    
    println!("Found {} missing files and {} unexpected entries", missing_count, unexpected_count);
}
```

## Error Categories

Validation errors are categorized for programmatic handling:

- **Missing**: Required files or directories that don't exist
- **WrongType**: Path exists but is the wrong type (e.g., file instead of directory)
- **NameMismatch**: File/directory name doesn't match the expected pattern
- **Unexpected**: Unexpected entry in a directory with `allow_defined_only=true`
- **InvalidPattern**: Error in a regex pattern
- **IoError**: Filesystem access errors
- **Other**: Miscellaneous errors

## License

MIT OR Apache-2.0