audb 0.1.11

AuDB - Compile-time database application framework with gold files
Documentation
//! Project loader for discovering and loading gold files

use crate::error::{Error, Result};
use crate::model::Project;
use crate::parser::{GoldFile, GoldParser};
use std::path::{Path, PathBuf};

/// Project loader for discovering and loading gold files
pub struct ProjectLoader {
    /// Root directory containing gold files
    root: PathBuf,
}

impl ProjectLoader {
    /// Create a new project loader for the given directory
    pub fn new<P: AsRef<Path>>(root: P) -> Self {
        Self {
            root: root.as_ref().to_path_buf(),
        }
    }

    /// Load all gold files from the directory and create a project
    pub fn load(&self) -> Result<Project> {
        let gold_files = self.discover_gold_files()?;

        if gold_files.is_empty() {
            return Err(Error::Config {
                message: format!(
                    "No gold files (.au) found in directory: {}",
                    self.root.display()
                ),
            });
        }

        let parsed_files = self.parse_gold_files(&gold_files)?;
        Project::from_gold_files(parsed_files)
    }

    /// Discover all `.au` files in the directory tree
    fn discover_gold_files(&self) -> Result<Vec<PathBuf>> {
        let mut gold_files = Vec::new();
        self.discover_recursive(&self.root, &mut gold_files)?;
        gold_files.sort();
        Ok(gold_files)
    }

    /// Recursively discover gold files
    fn discover_recursive(&self, dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
        if !dir.exists() {
            return Err(Error::Config {
                message: format!("Directory does not exist: {}", dir.display()),
            });
        }

        if !dir.is_dir() {
            return Err(Error::Config {
                message: format!("Path is not a directory: {}", dir.display()),
            });
        }

        let entries = std::fs::read_dir(dir).map_err(Error::Io)?;

        for entry in entries {
            let entry = entry.map_err(Error::Io)?;
            let path = entry.path();

            if path.is_dir() {
                // Skip hidden directories and target/build directories
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    if name.starts_with('.') || name == "target" || name == "build" {
                        continue;
                    }
                }
                self.discover_recursive(&path, files)?;
            } else if path.extension().and_then(|e| e.to_str()) == Some("au") {
                files.push(path);
            }
        }

        Ok(())
    }

    /// Parse all gold files
    fn parse_gold_files(&self, paths: &[PathBuf]) -> Result<Vec<GoldFile>> {
        let mut parsed = Vec::new();

        for path in paths {
            match GoldParser::parse_file(path) {
                Ok(file) => parsed.push(file),
                Err(e) => {
                    return Err(Error::Parse {
                        file: path.to_path_buf(),
                        line: 0,
                        column: 0,
                        message: format!("Failed to parse gold file: {}", e),
                    });
                }
            }
        }

        Ok(parsed)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_loader_creation() {
        let loader = ProjectLoader::new("./test");
        assert_eq!(loader.root, PathBuf::from("./test"));
    }

    #[test]
    fn test_discover_nonexistent_directory() {
        let loader = ProjectLoader::new("/nonexistent/path/that/does/not/exist");
        let result = loader.discover_gold_files();
        assert!(result.is_err());
    }
}