From 314f50df3a9d42abb8b882a828d70bdf8e176898 Mon Sep 17 00:00:00 2001 From: Matt Stavola Date: Wed, 8 Oct 2025 22:55:03 -0400 Subject: [PATCH] Allow .mlf/lexicons to be part of the workspace --- mlf-cli/src/generate/code.rs | 10 ++- mlf-cli/src/generate/lexicon.rs | 10 ++- mlf-cli/src/main.rs | 1 + mlf-cli/src/workspace_ext.rs | 141 ++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 mlf-cli/src/workspace_ext.rs diff --git a/mlf-cli/src/generate/code.rs b/mlf-cli/src/generate/code.rs index 91e6dd2..90dc514 100644 --- a/mlf-cli/src/generate/code.rs +++ b/mlf-cli/src/generate/code.rs @@ -118,13 +118,17 @@ pub fn run( let namespace = extract_namespace(&file_path); - // Create workspace with standard library - let mut workspace = match mlf_lang::Workspace::with_std() { + // Create workspace with standard library and .mlf cache + let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap()) + .ok() + .map(|root| crate::config::get_mlf_cache_dir(&root)); + + let mut workspace = match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) { Ok(ws) => ws, Err(e) => { errors.push(( file_path.display().to_string(), - format!("Failed to load standard library: {:?}", e), + format!("Failed to load workspace: {}", e), )); continue; } diff --git a/mlf-cli/src/generate/lexicon.rs b/mlf-cli/src/generate/lexicon.rs index 964baaa..4ce503d 100644 --- a/mlf-cli/src/generate/lexicon.rs +++ b/mlf-cli/src/generate/lexicon.rs @@ -89,11 +89,15 @@ pub fn run(input_patterns: Vec, output_dir: PathBuf, flat: bool) -> Resu let namespace = extract_namespace(&file_path); - // Create workspace with standard library for inline type resolution - let mut workspace = match mlf_lang::Workspace::with_std() { + // Create workspace with standard library and .mlf cache for inline type resolution + let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap()) + .ok() + .map(|root| crate::config::get_mlf_cache_dir(&root)); + + let mut workspace = match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) { Ok(ws) => ws, Err(e) => { - errors.push((file_path.display().to_string(), format!("Failed to load standard library: {:?}", e))); + errors.push((file_path.display().to_string(), format!("Failed to load workspace: {}", e))); continue; } }; diff --git a/mlf-cli/src/main.rs b/mlf-cli/src/main.rs index 03d32c2..9da9d2c 100644 --- a/mlf-cli/src/main.rs +++ b/mlf-cli/src/main.rs @@ -7,6 +7,7 @@ mod check; mod config; mod fetch; mod generate; +mod workspace_ext; // Import optional code generator plugins // These are automatically registered via inventory when the feature is enabled diff --git a/mlf-cli/src/workspace_ext.rs b/mlf-cli/src/workspace_ext.rs new file mode 100644 index 0000000..f81d7d0 --- /dev/null +++ b/mlf-cli/src/workspace_ext.rs @@ -0,0 +1,141 @@ +/// 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/stream.place.mlf" -> "stream.place" + 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 +/// e.g., base=".mlf/lexicons/mlf", path=".mlf/lexicons/mlf/stream.place.mlf" -> "stream.place" +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., "stream/place/foo.mlf" -> "stream.place.foo" + 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() { + let base = Path::new(".mlf/lexicons/mlf"); + let path = Path::new(".mlf/lexicons/mlf/stream.place.mlf"); + let namespace = extract_namespace_from_path(path, base).unwrap(); + assert_eq!(namespace, "stream.place"); + } + + #[test] + fn test_extract_namespace_nested() { + 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(); + // On Unix: "com/atproto/admin/defs" -> "com.atproto.admin.defs" + // Note: This depends on the directory structure + assert!(namespace.contains("com")); + } +} -- 2.51.2