forked from
stavola.xyz/mlf
A human-friendly DSL for ATProto Lexicons
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}