#validation #directory #structure

fsvalidator

A file structure validator

3 releases (breaking)

Uses new Rust 2024

new 0.3.0 May 11, 2025
0.2.0 May 10, 2025
0.1.5 May 9, 2025
0.1.0 Apr 30, 2025

#241 in Filesystem

Download history 211/week @ 2025-04-27 361/week @ 2025-05-04

574 downloads per month

MIT/Apache

74KB
1K SLoC

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 Documentation

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
  • Fluent Builder API: Chain methods to programmatically construct validation structures
  • 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:

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

Quick Start

Using TOML Configuration

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(())
}

Using Builder API

use anyhow::Result;
use fsvalidator::ModelBuilder;
use fsvalidator::display::format_validation_result;

fn main() -> Result<()> {
    // Create a project structure using the builder API
    let project = ModelBuilder::new_dir("project")
        .required(true)
        .allow_defined_only(true)
        .add_file("README.md", true)
        .add_dir("src", true)
            .add_file_pattern(".*\\.rs", true)
            .up()
        .add_dir("tests", false)
            .add_file_pattern("test_.*\\.rs", false)
            .build();

    // Validate a directory against the defined structure
    let path = "./path/to/validate";
    let validation_result = project.validate(path);

    // Display results with colorful, symbolic output
    println!("{}", format_validation_result(&validation_result, path));

    Ok(())
}

Definition Format

TOML Example

# 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

{
  "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

// 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

Using Direct Constructor Methods

You can create validation structures using the constructors directly:

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);
}

The builder API provides a more readable and chainable way to construct validation structures:

use fsvalidator::ModelBuilder;

// Create the project structure with a fluent builder API
let project = ModelBuilder::new_dir("project")
    .required(true)
    .allow_defined_only(true)
    .exclude_patterns(vec!["^\\..*", "\\.DS_Store"])
    .add_file("README.md", true)
    .add_dir("src", true)
        .add_file_pattern(".*\\.rs", true)
        .up()
    .add_dir("tests", false)
        .add_file_pattern("test_.*\\.rs", false)
        .build();

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

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

Dependencies

~2.6–4.5MB
~81K SLoC