/// Extensions to Workspace for CLI-specific functionality like loading from filesystem use mlf_lang::Workspace; use std::path::Path; /// Load MLF files from a directory into the workspace pub fn load_mlf_directory(workspace: &mut Workspace, dir: &Path) -> Result<(), String> { if !dir.exists() { // Directory doesn't exist, that's ok - just return return Ok(()); } // Recursively find all .mlf files let mlf_files = find_mlf_files(dir)?; for file_path in mlf_files { // Read the file let content = std::fs::read_to_string(&file_path) .map_err(|e| format!("Failed to read {}: {}", file_path.display(), e))?; // Convert file path to namespace // e.g., ".mlf/lexicons/mlf/place/stream/key.mlf" -> "place.stream" let namespace = extract_namespace_from_path(&file_path, dir)?; // Parse the lexicon let lexicon = mlf_lang::parse_lexicon(&content) .map_err(|e| format!("Failed to parse {}: {:?}", file_path.display(), e))?; // Add to workspace (merge if already exists) if workspace.has_module(&namespace) { // Module already exists, we could merge or skip // For now, skip to avoid conflicts continue; } workspace .add_module(namespace.clone(), lexicon) .map_err(|e| format!("Failed to add module {}: {:?}", namespace, e))?; } Ok(()) } /// Find all .mlf files recursively in a directory fn find_mlf_files(dir: &Path) -> Result, String> { let mut files = Vec::new(); let entries = std::fs::read_dir(dir) .map_err(|e| format!("Failed to read directory {}: {}", dir.display(), e))?; for entry in entries { let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; let path = entry.path(); if path.is_dir() { // Recurse into subdirectory files.extend(find_mlf_files(&path)?); } else if path.extension().and_then(|s| s.to_str()) == Some("mlf") { files.push(path); } } Ok(files) } /// Extract namespace from file path relative to base directory /// The namespace includes the full path WITH the filename (minus .mlf extension) /// e.g., base=".mlf/lexicons/mlf", path=".mlf/lexicons/mlf/place/stream/key.mlf" -> "place.stream.key" /// This allows "place.stream.key" to resolve to a definition named "key" in namespace "place.stream.key" fn extract_namespace_from_path(path: &Path, base: &Path) -> Result { let relative = path .strip_prefix(base) .map_err(|e| format!("Failed to strip prefix: {}", e))?; // Convert path to string let path_str = relative .to_str() .ok_or_else(|| "Non-UTF8 path".to_string())?; // Remove .mlf extension let without_ext = path_str .strip_suffix(".mlf") .unwrap_or(path_str); // Replace path separators with dots // e.g., "place/stream/key" -> "place.stream.key" let namespace = without_ext.replace(std::path::MAIN_SEPARATOR, "."); Ok(namespace) } /// Create a workspace with std library AND .mlf cache if it exists pub fn workspace_with_std_and_cache( mlf_cache_dir: Option<&Path>, ) -> Result { // Start with std library let mut workspace = Workspace::with_std() .map_err(|e| format!("Failed to load std library: {:?}", e))?; // Load from .mlf cache if provided if let Some(cache_dir) = mlf_cache_dir { let mlf_dir = cache_dir.join("lexicons/mlf"); load_mlf_directory(&mut workspace, &mlf_dir)?; } Ok(workspace) } /// Extension trait for Workspace to check if a module exists pub trait WorkspaceExt { fn has_module(&self, namespace: &str) -> bool; } impl WorkspaceExt for Workspace { fn has_module(&self, _namespace: &str) -> bool { // This requires exposing the modules field or adding a method to mlf-lang // For now, we'll just try to add and catch the error // TODO: Add a proper has_module method to Workspace false } } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_namespace_nested() { let base = Path::new(".mlf/lexicons/mlf"); let path = Path::new(".mlf/lexicons/mlf/place/stream/key.mlf"); let namespace = extract_namespace_from_path(path, base).unwrap(); // Full path "place/stream/key" becomes "place.stream.key" assert_eq!(namespace, "place.stream.key"); } #[test] fn test_extract_namespace_deep() { let base = Path::new(".mlf/lexicons/mlf"); let path = Path::new(".mlf/lexicons/mlf/com/atproto/admin/defs.mlf"); let namespace = extract_namespace_from_path(path, base).unwrap(); // Full path "com/atproto/admin/defs" becomes "com.atproto.admin.defs" assert_eq!(namespace, "com.atproto.admin.defs"); } #[test] fn test_extract_namespace_root() { let base = Path::new(".mlf/lexicons/mlf"); let path = Path::new(".mlf/lexicons/mlf/simple.mlf"); let namespace = extract_namespace_from_path(path, base).unwrap(); // File "simple" becomes namespace "simple" assert_eq!(namespace, "simple"); } }