use crate::ast::{Scope, Statement};
use crate::{Result, error};
use std::path::Path;
mod standard;
mod walk;
pub use standard::*;
pub use walk::*;
pub trait Loader {
fn is_resolution_enabled(&self) -> bool;
fn skip_macro_resolution(&mut self) -> Result<&mut Self>;
fn read(&self) -> Result<Statement>;
fn load(&self) -> Result<Statement> {
let mut module = self.read()?;
if self.is_resolution_enabled() {
let mut scope = Scope::new(&module);
module = scope.apply()?;
}
Ok(module)
}
fn validate(&self) -> Result<()> {
let module = self.read()?;
module.validate()?;
if self.is_resolution_enabled() {
let scope = Scope::new(&module);
scope.validate_macros()?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct LoaderConfig {
pub resolve_macros: bool,
pub allow_collisions: bool,
pub max_recursion_depth: usize,
pub validate_on_load: bool,
pub search_paths: Vec<std::path::PathBuf>,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
resolve_macros: true,
allow_collisions: false,
max_recursion_depth: 100,
validate_on_load: false,
search_paths: vec![std::env::current_dir().unwrap_or_else(|_| ".".into())],
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LoadStats {
pub files_processed: usize,
pub modules_created: usize,
pub macros_resolved: usize,
pub processing_time_ms: u64,
pub memory_usage_bytes: usize,
}
impl LoadStats {
pub fn new() -> Self {
Self::default()
}
pub fn merge(&mut self, other: &LoadStats) {
self.files_processed += other.files_processed;
self.modules_created += other.modules_created;
self.macros_resolved += other.macros_resolved;
self.processing_time_ms += other.processing_time_ms;
self.memory_usage_bytes += other.memory_usage_bytes;
}
}
pub mod utils {
use super::*;
use std::ffi::OsStr;
pub fn is_barkml_file<P: AsRef<Path>>(path: P) -> bool {
path.as_ref()
.extension()
.and_then(OsStr::to_str)
.map(|ext| ext.eq_ignore_ascii_case("bml"))
.unwrap_or(false)
}
pub fn is_barkml_dir<P: AsRef<Path>>(path: P) -> bool {
path.as_ref()
.extension()
.and_then(OsStr::to_str)
.map(|ext| ext.eq_ignore_ascii_case("d"))
.unwrap_or(false)
}
pub fn basename<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
let file_name = path
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| error::Error::Basename)?;
if let Some(extension) = path.extension().and_then(OsStr::to_str) {
let suffix = format!(".{}", extension);
Ok(file_name
.strip_suffix(&suffix)
.unwrap_or(file_name)
.to_string())
} else {
Ok(file_name.to_string())
}
}
pub fn discover_files<P: AsRef<Path>>(dir: P) -> Result<Vec<std::path::PathBuf>> {
let dir = dir.as_ref();
let mut files = Vec::new();
if !dir.exists() {
return Ok(files);
}
let entries = std::fs::read_dir(dir).map_err(|e| error::Error::Io {
reason: e.to_string(),
})?;
for entry in entries {
let entry = entry.map_err(|e| error::Error::Io {
reason: e.to_string(),
})?;
let path = entry.path();
if path.is_file() && is_barkml_file(&path) {
files.push(path);
}
}
files.sort();
Ok(files)
}
pub fn validate_path<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if !path.exists() {
return Err(error::Error::NotFound {
path: path.to_path_buf(),
});
}
if path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(error::Error::Io {
reason: "Path traversal not allowed".to_string(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::utils::*;
use super::*;
#[test]
fn test_is_barkml_file() {
assert!(is_barkml_file("test.bml"));
assert!(is_barkml_file("test.BML"));
assert!(!is_barkml_file("test.txt"));
assert!(!is_barkml_file("test"));
}
#[test]
fn test_is_barkml_dir() {
assert!(is_barkml_dir("test.d"));
assert!(is_barkml_dir("test.D"));
assert!(!is_barkml_dir("test.txt"));
assert!(!is_barkml_dir("test"));
}
#[test]
fn test_basename() {
assert_eq!(basename("test.bml").unwrap(), "test");
assert_eq!(basename("path/to/test.bml").unwrap(), "test");
assert_eq!(basename("test").unwrap(), "test");
}
#[test]
fn test_loader_config_default() {
let config = LoaderConfig::default();
assert!(config.resolve_macros);
assert!(!config.allow_collisions);
assert_eq!(config.max_recursion_depth, 100);
assert!(!config.validate_on_load);
}
#[test]
fn test_load_stats() {
let mut stats1 = LoadStats::new();
stats1.files_processed = 5;
stats1.modules_created = 3;
let mut stats2 = LoadStats::new();
stats2.files_processed = 2;
stats2.modules_created = 1;
stats1.merge(&stats2);
assert_eq!(stats1.files_processed, 7);
assert_eq!(stats1.modules_created, 4);
}
}