use super::markdown_parser::MarkdownParser;
use super::{Domain, OntologyBlock};
use crate::{Result, VowlError};
use regex::Regex;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone)]
pub struct LoaderConfig {
pub cache_size: usize,
pub strict_validation: bool,
pub parallel_loading: bool,
pub file_pattern: String,
pub recursive: bool,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
cache_size: 128,
strict_validation: false,
parallel_loading: cfg!(feature = "parallel"),
file_pattern: "*.md".to_string(),
recursive: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LoaderStatistics {
pub total_blocks: usize,
pub by_domain: HashMap<Domain, usize>,
pub by_status: HashMap<String, usize>,
pub cache_hits: usize,
pub cache_misses: usize,
pub load_time_ms: u128,
}
impl LoaderStatistics {
pub fn cache_hit_rate(&self) -> f64 {
let total = self.cache_hits + self.cache_misses;
if total == 0 {
0.0
} else {
self.cache_hits as f64 / total as f64
}
}
pub fn domain_summary(&self) -> String {
let mut summary = String::new();
for (domain, count) in &self.by_domain {
summary.push_str(&format!(" {}: {}\n", domain.prefix(), count));
}
summary
}
}
impl Default for OntologyLoader {
fn default() -> Self {
Self::new(LoaderConfig::default())
}
}
pub struct OntologyLoader {
parser: MarkdownParser,
config: LoaderConfig,
cache: HashMap<PathBuf, Option<OntologyBlock>>,
cache_hits: usize,
cache_misses: usize,
}
impl OntologyLoader {
pub fn new(config: LoaderConfig) -> Self {
Self {
parser: MarkdownParser::new(),
config,
cache: HashMap::with_capacity(128),
cache_hits: 0,
cache_misses: 0,
}
}
pub fn load_file(&mut self, path: &Path) -> Result<Option<OntologyBlock>> {
let path_buf = path.to_path_buf();
if let Some(cached) = self.cache.get(&path_buf) {
self.cache_hits += 1;
return Ok(cached.clone());
}
self.cache_misses += 1;
let content = fs::read_to_string(path)
.map_err(|e| VowlError::ParseError(format!("Failed to read file {:?}: {}", path, e)))?;
let block = match self.parser.parse(&content) {
Ok(block) => Some(block),
Err(VowlError::ParseError(_)) => {
None
}
Err(e) => return Err(e),
};
if self.cache.len() >= self.config.cache_size {
if let Some(first_key) = self.cache.keys().next().cloned() {
self.cache.remove(&first_key);
}
}
self.cache.insert(path_buf, block.clone());
Ok(block)
}
pub fn load_directory(
&mut self,
path: &Path,
domain: Option<Domain>,
) -> Result<Vec<OntologyBlock>> {
let _start = std::time::Instant::now();
let files = self.collect_files(path)?;
let blocks = if self.config.parallel_loading && cfg!(feature = "parallel") {
self.load_files_parallel(&files)?
} else {
self.load_files_sequential(&files)?
};
let filtered = if let Some(d) = domain {
blocks.into_iter().filter(|b| b.domain == d).collect()
} else {
blocks
};
Ok(filtered)
}
pub fn load_files(&mut self, paths: &[PathBuf]) -> Result<Vec<OntologyBlock>> {
self.load_files_sequential(paths)
}
pub fn filter_by_domain(
&self,
blocks: Vec<OntologyBlock>,
domain: Domain,
) -> Vec<OntologyBlock> {
blocks.into_iter().filter(|b| b.domain == domain).collect()
}
pub fn filter_by_pattern(
&self,
blocks: Vec<OntologyBlock>,
pattern: &str,
) -> Result<Vec<OntologyBlock>> {
let regex = Regex::new(pattern)
.map_err(|e| VowlError::ParseError(format!("Invalid regex: {}", e)))?;
Ok(blocks
.into_iter()
.filter(|b| regex.is_match(&b.term_id))
.collect())
}
pub fn get_statistics(&self, blocks: &[OntologyBlock]) -> LoaderStatistics {
let mut stats = LoaderStatistics {
total_blocks: blocks.len(),
cache_hits: self.cache_hits,
cache_misses: self.cache_misses,
..Default::default()
};
for block in blocks {
*stats.by_domain.entry(block.domain).or_insert(0) += 1;
if let Some(ref status) = block.metadata.status {
*stats.by_status.entry(status.clone()).or_insert(0) += 1;
}
}
stats
}
pub fn create_term_index(&self, blocks: Vec<OntologyBlock>) -> HashMap<String, OntologyBlock> {
blocks.into_iter().map(|b| (b.term_id.clone(), b)).collect()
}
pub fn group_by_domain(
&self,
blocks: Vec<OntologyBlock>,
) -> HashMap<Domain, Vec<OntologyBlock>> {
let mut groups: HashMap<Domain, Vec<OntologyBlock>> = HashMap::new();
for block in blocks {
groups.entry(block.domain).or_default().push(block);
}
groups
}
pub fn clear_cache(&mut self) {
self.cache.clear();
self.cache_hits = 0;
self.cache_misses = 0;
}
pub fn get_cache_stats(&self) -> (usize, usize, usize, f64) {
let total = self.cache_hits + self.cache_misses;
let hit_rate = if total == 0 {
0.0
} else {
self.cache_hits as f64 / total as f64
};
(
self.cache.len(),
self.cache_hits,
self.cache_misses,
hit_rate,
)
}
fn collect_files(&self, path: &Path) -> Result<Vec<PathBuf>> {
if !path.exists() {
return Err(VowlError::ParseError(format!(
"Directory not found: {:?}",
path
)));
}
let mut files = Vec::new();
if self.config.recursive {
self.collect_files_recursive(path, &mut files)?;
} else {
let entries = fs::read_dir(path).map_err(|e| {
VowlError::ParseError(format!("Failed to read directory {:?}: {}", path, e))
})?;
for entry in entries {
let entry = entry.map_err(|e| {
VowlError::ParseError(format!("Failed to read directory entry: {}", e))
})?;
let path = entry.path();
if path.is_file() && self.matches_pattern(&path) {
files.push(path);
}
}
}
files.sort();
Ok(files)
}
fn collect_files_recursive(&self, path: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
let entries = fs::read_dir(path).map_err(|e| {
VowlError::ParseError(format!("Failed to read directory {:?}: {}", path, e))
})?;
for entry in entries {
let entry = entry.map_err(|e| {
VowlError::ParseError(format!("Failed to read directory entry: {}", e))
})?;
let path = entry.path();
if path.is_dir() {
self.collect_files_recursive(&path, files)?;
} else if path.is_file() && self.matches_pattern(&path) {
files.push(path);
}
}
Ok(())
}
fn matches_pattern(&self, path: &Path) -> bool {
if let Some(ext) = path.extension() {
if ext == "md" {
return true;
}
}
false
}
fn load_files_sequential(&mut self, files: &[PathBuf]) -> Result<Vec<OntologyBlock>> {
let mut blocks = Vec::new();
for file in files {
if let Some(block) = self.load_file(file)? {
blocks.push(block);
}
}
Ok(blocks)
}
#[cfg(feature = "parallel")]
fn load_files_parallel(&mut self, files: &[PathBuf]) -> Result<Vec<OntologyBlock>> {
let parser = MarkdownParser::new();
let blocks: Vec<OntologyBlock> = files
.par_iter()
.filter_map(|file| {
let content = fs::read_to_string(file).ok()?;
parser.parse(&content).ok()
})
.collect();
Ok(blocks)
}
#[cfg(not(feature = "parallel"))]
fn load_files_parallel(&mut self, files: &[PathBuf]) -> Result<Vec<OntologyBlock>> {
self.load_files_sequential(files)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)] fn create_test_markdown(term_id: &str, domain: &str) -> String {
format!(
r#"
- ### OntologyBlock
- **Identification**
- term-id:: {}
- preferred-term:: Test Term
- public-access:: true
- ontology:: true
- **Semantic Classification**
- owl:class:: {}:TestClass
- owl:physicality:: VirtualEntity
- owl:role:: Object
"#,
term_id, domain
)
}
#[test]
fn test_loader_creation() {
let config = LoaderConfig::default();
let loader = OntologyLoader::new(config);
assert_eq!(loader.cache.len(), 0);
assert_eq!(loader.cache_hits, 0);
}
#[test]
fn test_filter_by_domain() {
let loader = OntologyLoader::default();
let blocks = Vec::new();
let filtered = loader.filter_by_domain(blocks, Domain::AI);
assert_eq!(filtered.len(), 0);
}
#[test]
fn test_statistics() {
let loader = OntologyLoader::default();
let blocks = Vec::new();
let stats = loader.get_statistics(&blocks);
assert_eq!(stats.total_blocks, 0);
assert_eq!(stats.cache_hit_rate(), 0.0);
}
#[test]
fn test_cache_stats() {
let loader = OntologyLoader::default();
let (size, hits, misses, rate) = loader.get_cache_stats();
assert_eq!(size, 0);
assert_eq!(hits, 0);
assert_eq!(misses, 0);
assert_eq!(rate, 0.0);
}
#[test]
fn test_group_by_domain() {
let loader = OntologyLoader::default();
let blocks = Vec::new();
let groups = loader.group_by_domain(blocks);
assert_eq!(groups.len(), 0);
}
#[test]
fn test_filter_by_pattern() {
let loader = OntologyLoader::default();
let blocks = Vec::new();
let result = loader.filter_by_pattern(blocks, r"AI-\d+");
assert!(result.is_ok());
}
#[test]
fn test_invalid_pattern() {
let loader = OntologyLoader::default();
let blocks = Vec::new();
let result = loader.filter_by_pattern(blocks, "[invalid");
assert!(result.is_err());
}
}