fsvalidator 0.3.0

A file structure validator
Documentation
use anyhow::Result;
use regex::Regex;

use fsvalidator::loader::load_root;
use fsvalidator::model::{Node, NodeName};
use fsvalidator::raw::{RawNode, RawNodeType, RawRoot};
use std::collections::HashMap;

#[test]
fn test_load_basic_structure() -> Result<()> {
    // Create a basic raw structure
    let raw_root = RawRoot {
        root: RawNode {
            r#type: RawNodeType::Dir,
            name: Some("test_dir".to_string()),
            pattern: None,
            children: Some(vec![RawNode {
                r#type: RawNodeType::File,
                name: Some("test_file.txt".to_string()),
                pattern: None,
                children: None,
                r#ref: None,
                required: Some(true),
                allow_defined_only: None,
                excluded: None,
            }]),
            r#ref: None,
            required: Some(true),
            allow_defined_only: None,
            excluded: None,
        },
        template: HashMap::new(),
        config: None,
        global: None,
    };

    // Load the structure
    let node = load_root(&raw_root)?;

    // Verify it's a directory node
    match &node {
        Node::Dir(dir_rc) => {
            let dir = dir_rc.borrow();
            assert_eq!(dir.name, NodeName::Literal("test_dir".to_string()));
            assert_eq!(dir.required, true);
            assert_eq!(dir.allow_defined_only, false);
            assert_eq!(dir.children.len(), 1);

            // Check the child
            match &dir.children[0] {
                Node::File(file_rc) => {
                    let file = file_rc.borrow();
                    assert_eq!(file.name, NodeName::Literal("test_file.txt".to_string()));
                    assert_eq!(file.required, true);
                }
                _ => panic!("Expected a file node"),
            }
        }
        _ => panic!("Expected a directory node"),
    }

    Ok(())
}

#[test]
fn test_load_with_pattern() -> Result<()> {
    // Create a raw structure with patterns
    let raw_root = RawRoot {
        root: RawNode {
            r#type: RawNodeType::Dir,
            name: None,
            pattern: Some("test_dir_\\d+".to_string()),
            children: Some(vec![RawNode {
                r#type: RawNodeType::File,
                name: None,
                pattern: Some(".*\\.log".to_string()),
                children: None,
                r#ref: None,
                required: Some(true),
                allow_defined_only: None,
                excluded: None,
            }]),
            r#ref: None,
            required: Some(true),
            allow_defined_only: None,
            excluded: None,
        },
        template: HashMap::new(),
        config: None,
        global: None,
    };

    // Load the structure
    let node = load_root(&raw_root)?;

    // Verify it's a directory node with pattern
    match &node {
        Node::Dir(dir_rc) => {
            let dir = dir_rc.borrow();
            match &dir.name {
                NodeName::Pattern(pattern) => {
                    assert_eq!(pattern, "test_dir_\\d+");
                    // Verify the regex compiles
                    let re = Regex::new(pattern)?;
                    assert!(re.is_match("test_dir_123"));
                }
                _ => panic!("Expected a pattern node name"),
            }

            // Check the child
            match &dir.children[0] {
                Node::File(file_rc) => {
                    let file = file_rc.borrow();
                    match &file.name {
                        NodeName::Pattern(pattern) => {
                            assert_eq!(pattern, ".*\\.log");
                            // Verify the regex compiles
                            let re = Regex::new(pattern)?;
                            assert!(re.is_match("server.log"));
                        }
                        _ => panic!("Expected a pattern node name"),
                    }
                }
                _ => panic!("Expected a file node"),
            }
        }
        _ => panic!("Expected a directory node"),
    }

    Ok(())
}

#[test]
fn test_load_with_templates() -> Result<()> {
    // Create a raw structure with templates
    let mut templates = HashMap::new();
    templates.insert(
        "config".to_string(),
        RawNode {
            r#type: RawNodeType::Dir,
            name: Some("config".to_string()),
            pattern: None,
            children: Some(vec![RawNode {
                r#type: RawNodeType::File,
                name: Some("settings.json".to_string()),
                pattern: None,
                children: None,
                r#ref: None,
                required: Some(true),
                allow_defined_only: None,
                excluded: None,
            }]),
            r#ref: None,
            required: None,
            allow_defined_only: None,
            excluded: None,
        },
    );

    let raw_root = RawRoot {
        root: RawNode {
            r#type: RawNodeType::Dir,
            name: Some("project".to_string()),
            pattern: None,
            children: Some(vec![RawNode {
                r#type: RawNodeType::Ref,
                name: None,
                pattern: Some("config.*".to_string()),
                children: None,
                r#ref: Some("config".to_string()),
                required: Some(true),
                allow_defined_only: None,
                excluded: None,
            }]),
            r#ref: None,
            required: Some(true),
            allow_defined_only: None,
            excluded: None,
        },
        template: templates,
        config: None,
        global: None,
    };

    // Load the structure
    let node = load_root(&raw_root)?;

    // Verify the root node
    match &node {
        Node::Dir(dir_rc) => {
            let dir = dir_rc.borrow();
            assert_eq!(dir.name, NodeName::Literal("project".to_string()));

            // Check the child (template reference)
            match &dir.children[0] {
                Node::Dir(template_rc) => {
                    let template = template_rc.borrow();
                    match &template.name {
                        NodeName::Literal(literal) => {
                            assert_eq!(literal, "config");
                        }
                        _ => panic!("Expected a literal node name"),
                    }

                    // Check the template's child
                    match &template.children[0] {
                        Node::File(file_rc) => {
                            let file = file_rc.borrow();
                            assert_eq!(file.name, NodeName::Literal("settings.json".to_string()));
                        }
                        _ => panic!("Expected a file node"),
                    }
                }
                _ => panic!("Expected a directory node"),
            }
        }
        _ => panic!("Expected a directory node"),
    }

    Ok(())
}

#[test]
fn test_load_with_global_settings() -> Result<()> {
    // Create a raw structure with global settings
    let raw_root = RawRoot {
        root: RawNode {
            r#type: RawNodeType::Dir,
            name: Some("project".to_string()),
            pattern: None,
            children: Some(vec![RawNode {
                r#type: RawNodeType::File,
                name: Some("config.json".to_string()),
                pattern: None,
                children: None,
                r#ref: None,
                required: None, // Use global required
                allow_defined_only: None,
                excluded: None,
            }]),
            r#ref: None,
            required: None,           // Use global required
            allow_defined_only: None, // Use global allow_defined_only
            excluded: None,
        },
        template: HashMap::new(),
        config: None,
        global: Some(fsvalidator::raw::RawGlobal {
            required: Some(true),
            allow_defined_only: Some(true),
            excluded: Some(vec![".*\\.bak".to_string()]),
        }),
    };

    // Load the structure
    let node = load_root(&raw_root)?;

    // Verify the global settings propagated
    match &node {
        Node::Dir(dir_rc) => {
            let dir = dir_rc.borrow();
            assert_eq!(dir.required, true); // From global
            assert_eq!(dir.allow_defined_only, true); // From global

            // Check the child inherited global settings
            match &dir.children[0] {
                Node::File(file_rc) => {
                    let file = file_rc.borrow();
                    assert_eq!(file.required, true); // From global
                }
                _ => panic!("Expected a file node"),
            }
        }
        _ => panic!("Expected a directory node"),
    }

    Ok(())
}

#[test]
fn test_load_error_missing_name_and_pattern() {
    // Create a raw structure with neither name nor pattern
    let raw_root = RawRoot {
        root: RawNode {
            r#type: RawNodeType::Dir,
            name: None,
            pattern: None, // Missing both name and pattern
            children: None,
            r#ref: None,
            required: None,
            allow_defined_only: None,
            excluded: None,
        },
        template: HashMap::new(),
        config: None,
        global: None,
    };

    // Loading should fail
    let result = load_root(&raw_root);
    assert!(result.is_err());
    assert!(
        result
            .unwrap_err()
            .to_string()
            .contains("Node must have either 'name' or 'pattern'")
    );
}

#[test]
fn test_load_error_missing_ref() {
    // Create a raw structure with a ref type but missing ref field
    let raw_root = RawRoot {
        root: RawNode {
            r#type: RawNodeType::Ref,
            name: Some("test".to_string()),
            pattern: None,
            children: None,
            r#ref: None, // Missing ref
            required: None,
            allow_defined_only: None,
            excluded: None,
        },
        template: HashMap::new(),
        config: None,
        global: None,
    };

    // Loading should fail
    let result = load_root(&raw_root);
    assert!(result.is_err());
    assert!(
        result
            .unwrap_err()
            .to_string()
            .contains("Ref node missing 'ref' field")
    );
}

#[test]
fn test_load_error_unknown_template() {
    // Create a raw structure referencing non-existent template
    let raw_root = RawRoot {
        root: RawNode {
            r#type: RawNodeType::Ref,
            name: Some("test".to_string()),
            pattern: None,
            children: None,
            r#ref: Some("non_existent_template".to_string()),
            required: None,
            allow_defined_only: None,
            excluded: None,
        },
        template: HashMap::new(), // Empty templates
        config: None,
        global: None,
    };

    // Loading should fail
    let result = load_root(&raw_root);
    assert!(result.is_err());
    assert!(
        result
            .unwrap_err()
            .to_string()
            .contains("Unknown template ref")
    );
}