A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 152 lines 5.3 kB view raw
1/// Extensions to Workspace for CLI-specific functionality like loading from filesystem 2use mlf_lang::Workspace; 3use std::path::Path; 4 5/// Load MLF files from a directory into the workspace 6pub fn load_mlf_directory(workspace: &mut Workspace, dir: &Path) -> Result<(), String> { 7 if !dir.exists() { 8 // Directory doesn't exist, that's ok - just return 9 return Ok(()); 10 } 11 12 // Recursively find all .mlf files 13 let mlf_files = find_mlf_files(dir)?; 14 15 for file_path in mlf_files { 16 // Read the file 17 let content = std::fs::read_to_string(&file_path) 18 .map_err(|e| format!("Failed to read {}: {}", file_path.display(), e))?; 19 20 // Convert file path to namespace 21 // e.g., ".mlf/lexicons/mlf/place/stream/key.mlf" -> "place.stream" 22 let namespace = extract_namespace_from_path(&file_path, dir)?; 23 24 // Parse the lexicon 25 let lexicon = mlf_lang::parse_lexicon(&content) 26 .map_err(|e| format!("Failed to parse {}: {:?}", file_path.display(), e))?; 27 28 // Add to workspace (merge if already exists) 29 if workspace.has_module(&namespace) { 30 // Module already exists, we could merge or skip 31 // For now, skip to avoid conflicts 32 continue; 33 } 34 35 workspace 36 .add_module(namespace.clone(), lexicon) 37 .map_err(|e| format!("Failed to add module {}: {:?}", namespace, e))?; 38 } 39 40 Ok(()) 41} 42 43/// Find all .mlf files recursively in a directory 44fn find_mlf_files(dir: &Path) -> Result<Vec<std::path::PathBuf>, String> { 45 let mut files = Vec::new(); 46 47 let entries = std::fs::read_dir(dir) 48 .map_err(|e| format!("Failed to read directory {}: {}", dir.display(), e))?; 49 50 for entry in entries { 51 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; 52 let path = entry.path(); 53 54 if path.is_dir() { 55 // Recurse into subdirectory 56 files.extend(find_mlf_files(&path)?); 57 } else if path.extension().and_then(|s| s.to_str()) == Some("mlf") { 58 files.push(path); 59 } 60 } 61 62 Ok(files) 63} 64 65/// Extract namespace from file path relative to base directory 66/// The namespace includes the full path WITH the filename (minus .mlf extension) 67/// e.g., base=".mlf/lexicons/mlf", path=".mlf/lexicons/mlf/place/stream/key.mlf" -> "place.stream.key" 68/// This allows "place.stream.key" to resolve to a definition named "key" in namespace "place.stream.key" 69fn extract_namespace_from_path(path: &Path, base: &Path) -> Result<String, String> { 70 let relative = path 71 .strip_prefix(base) 72 .map_err(|e| format!("Failed to strip prefix: {}", e))?; 73 74 // Convert path to string 75 let path_str = relative 76 .to_str() 77 .ok_or_else(|| "Non-UTF8 path".to_string())?; 78 79 // Remove .mlf extension 80 let without_ext = path_str 81 .strip_suffix(".mlf") 82 .unwrap_or(path_str); 83 84 // Replace path separators with dots 85 // e.g., "place/stream/key" -> "place.stream.key" 86 let namespace = without_ext.replace(std::path::MAIN_SEPARATOR, "."); 87 88 Ok(namespace) 89} 90 91/// Create a workspace with std library AND .mlf cache if it exists 92pub fn workspace_with_std_and_cache( 93 mlf_cache_dir: Option<&Path>, 94) -> Result<Workspace, String> { 95 // Start with std library 96 let mut workspace = Workspace::with_std() 97 .map_err(|e| format!("Failed to load std library: {:?}", e))?; 98 99 // Load from .mlf cache if provided 100 if let Some(cache_dir) = mlf_cache_dir { 101 let mlf_dir = cache_dir.join("lexicons/mlf"); 102 load_mlf_directory(&mut workspace, &mlf_dir)?; 103 } 104 105 Ok(workspace) 106} 107 108/// Extension trait for Workspace to check if a module exists 109pub trait WorkspaceExt { 110 fn has_module(&self, namespace: &str) -> bool; 111} 112 113impl WorkspaceExt for Workspace { 114 fn has_module(&self, _namespace: &str) -> bool { 115 // This requires exposing the modules field or adding a method to mlf-lang 116 // For now, we'll just try to add and catch the error 117 // TODO: Add a proper has_module method to Workspace 118 false 119 } 120} 121 122#[cfg(test)] 123mod tests { 124 use super::*; 125 126 #[test] 127 fn test_extract_namespace_nested() { 128 let base = Path::new(".mlf/lexicons/mlf"); 129 let path = Path::new(".mlf/lexicons/mlf/place/stream/key.mlf"); 130 let namespace = extract_namespace_from_path(path, base).unwrap(); 131 // Full path "place/stream/key" becomes "place.stream.key" 132 assert_eq!(namespace, "place.stream.key"); 133 } 134 135 #[test] 136 fn test_extract_namespace_deep() { 137 let base = Path::new(".mlf/lexicons/mlf"); 138 let path = Path::new(".mlf/lexicons/mlf/com/atproto/admin/defs.mlf"); 139 let namespace = extract_namespace_from_path(path, base).unwrap(); 140 // Full path "com/atproto/admin/defs" becomes "com.atproto.admin.defs" 141 assert_eq!(namespace, "com.atproto.admin.defs"); 142 } 143 144 #[test] 145 fn test_extract_namespace_root() { 146 let base = Path::new(".mlf/lexicons/mlf"); 147 let path = Path::new(".mlf/lexicons/mlf/simple.mlf"); 148 let namespace = extract_namespace_from_path(path, base).unwrap(); 149 // File "simple" becomes namespace "simple" 150 assert_eq!(namespace, "simple"); 151 } 152}