fsvalidator 0.3.0

A file structure validator
Documentation
//! # Schema Loader
//!
//! Converts raw configuration data into a structured validation model.
//!
//! This module handles the transformation of the raw deserialized data from
//! configuration files into the structured model used for validation.

use crate::model::{DirNode, FileNode, Node, NodeName};
use crate::raw::{RawNode, RawNodeType, RawRoot};
use anyhow::{Result, anyhow};
use regex::Regex;
use std::cell::RefCell;
use std::collections::HashMap;

use std::rc::Rc;

/// Loads a raw root configuration into a structured validation model.
///
/// This is the main entry point for converting deserialized configuration data
/// into a validation model that can be used to validate filesystem paths.
///
/// # Arguments
///
/// * `raw` - The raw root configuration from a TOML or JSON file
///
/// # Returns
///
/// * `Result<Node>` - The root node of the validation model
pub fn load_root(raw: &RawRoot) -> Result<Node> {
    let mut ctx = TemplateContext {
        templates: &raw.template,
        built_templates: HashMap::new(),
    };

    let global = &raw.global;

    build_node(&raw.root, &mut ctx, global)
}

/// Context for template resolution during loading.
///
/// This structure keeps track of available templates and already built templates
/// to handle recursive references and avoid duplicating work.
struct TemplateContext<'a> {
    /// Available templates from the configuration
    templates: &'a HashMap<String, RawNode>,
    /// Templates that have already been built
    built_templates: HashMap<String, Rc<RefCell<DirNode>>>,
}

/// Builds a structured node from a raw node configuration.
///
/// Recursively processes a raw node and its children, resolving references to templates
/// and applying global settings when node-specific settings are not provided.
///
/// # Arguments
///
/// * `raw` - The raw node to process
/// * `ctx` - The template context for resolving references
/// * `global` - Optional global settings to apply
///
/// # Returns
///
/// * `Result<Node>` - The processed node
fn build_node(
    raw: &RawNode,
    ctx: &mut TemplateContext,
    global: &Option<crate::raw::RawGlobal>,
) -> Result<Node> {
    match &raw.r#type {
        RawNodeType::Dir => build_dir_node(raw, ctx, global),
        RawNodeType::File => build_file_node(raw, global),
        RawNodeType::Ref => build_ref_node(raw, ctx, global),
    }
}

fn build_dir_node(
    raw: &RawNode,
    ctx: &mut TemplateContext,
    global: &Option<crate::raw::RawGlobal>,
) -> Result<Node> {
    let name = match (&raw.name, &raw.pattern) {
        (Some(n), None) => NodeName::Literal(n.clone()),
        (None, Some(p)) => NodeName::Pattern(p.clone()),
        _ => {
            return Err(anyhow!(
                "Node must have either 'name' or 'pattern', but not both"
            ));
        }
    };

    let excluded = match &raw.excluded {
        None => vec![],
        Some(e) => {
            let mut cloned_excluded = e.clone();
            cloned_excluded.extend_from_slice(
                global
                    .as_ref()
                    .and_then(|c| c.excluded.as_ref())
                    .and_then(|c| Some(c.as_ref()))
                    .unwrap_or(vec![].as_slice()),
            );
            let excluded_patterns: Vec<Regex> = cloned_excluded
                .iter()
                .map(|p| Regex::new(p))
                .collect::<Result<Vec<_>, _>>()?;

            excluded_patterns
        }
    };

    let required = raw
        .required
        .or(global.as_ref().and_then(|c| c.required))
        .unwrap_or(false);
    let allow_defined_only = raw
        .allow_defined_only
        .or(global.as_ref().and_then(|c| c.allow_defined_only))
        .unwrap_or(false);

    let children = match &raw.children {
        Some(raw_children) => {
            let mut out = Vec::new();
            for c in raw_children {
                out.push(build_node(c, ctx, global)?);
            }
            out
        }
        None => vec![],
    };

    Ok(DirNode::new(
        name,
        children,
        required,
        allow_defined_only,
        excluded,
    ))
}

fn build_file_node(raw: &RawNode, global: &Option<crate::raw::RawGlobal>) -> Result<Node> {
    let name = match (&raw.name, &raw.pattern) {
        (Some(n), None) => NodeName::Literal(n.clone()),
        (None, Some(p)) => NodeName::Pattern(p.clone()),
        _ => {
            return Err(anyhow!(
                "Node must have either 'name' or 'pattern', but not both"
            ));
        }
    };

    let required = raw
        .required
        .or(global.as_ref().and_then(|c| c.required))
        .unwrap_or(false);

    Ok(FileNode::new(name, required))
}

fn build_ref_node(
    raw: &RawNode,
    ctx: &mut TemplateContext,
    global: &Option<crate::raw::RawGlobal>,
) -> Result<Node> {
    let ref_key = raw
        .r#ref
        .as_ref()
        .ok_or_else(|| anyhow!("Ref node missing 'ref' field"))?;

    if let Some(built_template) = ctx.built_templates.get(ref_key) {
        return Ok(Node::Dir(built_template.clone()));
    }

    let excluded = match &raw.excluded {
        None => vec![],
        Some(e) => {
            let mut cloned_excluded = e.clone();
            cloned_excluded.extend_from_slice(
                global
                    .as_ref()
                    .and_then(|c| c.excluded.as_ref())
                    .and_then(|c| Some(c.as_ref()))
                    .unwrap_or(vec![].as_slice()),
            );
            let excluded_patterns: Vec<Regex> = cloned_excluded
                .iter()
                .map(|p| Regex::new(p))
                .collect::<Result<Vec<_>, _>>()?;

            excluded_patterns
        }
    };

    let template = ctx
        .templates
        .get(ref_key)
        .ok_or_else(|| anyhow!("Unknown template ref: {}", ref_key))?;

    let name = match (&template.name, &template.pattern) {
        (Some(n), None) => NodeName::Literal(n.clone()),
        (None, Some(p)) => NodeName::Pattern(p.clone()),
        _ => {
            return Err(anyhow!(
                "Node must have either 'name' or 'pattern', but not both"
            ));
        }
    };

    let required = template
        .required
        .or(global.as_ref().and_then(|c| c.required))
        .unwrap_or(false);
    let allow_defined_only = template
        .allow_defined_only
        .or(global.as_ref().and_then(|c| c.allow_defined_only))
        .unwrap_or(false);

    let node = Rc::new(RefCell::new(DirNode {
        name,
        children: Vec::new(),
        required,
        allow_defined_only,
        excluded,
    }));

    ctx.built_templates.insert(ref_key.clone(), node.clone());

    if let Some(children) = &template.children {
        for c in children {
            let child_node = build_node(c, ctx, global)?;
            node.borrow_mut().children.push(child_node);
        }
    };

    Ok(Node::Dir(node.clone()))
}