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 264 lines 7.3 kB view raw
1use serde::{Deserialize, Serialize}; 2use std::collections::HashMap; 3use std::path::{Path, PathBuf}; 4use thiserror::Error; 5 6#[derive(Error, Debug)] 7pub enum ConfigError { 8 #[error("Failed to read config file: {0}")] 9 ReadError(#[from] std::io::Error), 10 11 #[error("Failed to parse config file: {0}")] 12 ParseError(#[from] toml::de::Error), 13 14 #[error("No mlf.toml found in current directory or parent directories")] 15 NotFound, 16} 17 18#[derive(Debug, Serialize, Deserialize)] 19pub struct MlfConfig { 20 #[serde(default)] 21 pub source: SourceConfig, 22 23 #[serde(default, skip_serializing_if = "Vec::is_empty")] 24 pub output: Vec<OutputConfig>, 25 26 #[serde(default)] 27 pub dependencies: DependenciesConfig, 28} 29 30#[derive(Debug, Serialize, Deserialize)] 31pub struct SourceConfig { 32 #[serde(default = "default_source_directory", skip_serializing_if = "is_default_source_directory")] 33 pub directory: String, 34} 35 36impl Default for SourceConfig { 37 fn default() -> Self { 38 Self { 39 directory: default_source_directory(), 40 } 41 } 42} 43 44fn default_source_directory() -> String { 45 "./lexicons".to_string() 46} 47 48fn is_default_source_directory(s: &str) -> bool { 49 s == default_source_directory() 50} 51 52#[derive(Debug, Serialize, Deserialize)] 53pub struct OutputConfig { 54 pub r#type: String, 55 pub directory: String, 56} 57 58#[derive(Debug, Serialize, Deserialize)] 59pub struct DependenciesConfig { 60 #[serde(default)] 61 pub dependencies: Vec<String>, 62 63 #[serde(default = "default_allow_transitive_deps", skip_serializing_if = "is_default_allow_transitive_deps")] 64 pub allow_transitive_deps: bool, 65 66 #[serde(default = "default_optimize_transitive_fetches", skip_serializing_if = "is_default_optimize_transitive_fetches")] 67 pub optimize_transitive_fetches: bool, 68} 69 70fn default_allow_transitive_deps() -> bool { 71 true 72} 73 74fn default_optimize_transitive_fetches() -> bool { 75 false 76} 77 78fn is_default_allow_transitive_deps(b: &bool) -> bool { 79 *b == default_allow_transitive_deps() 80} 81 82fn is_default_optimize_transitive_fetches(b: &bool) -> bool { 83 *b == default_optimize_transitive_fetches() 84} 85 86impl Default for DependenciesConfig { 87 fn default() -> Self { 88 Self { 89 dependencies: vec![], 90 allow_transitive_deps: default_allow_transitive_deps(), 91 optimize_transitive_fetches: default_optimize_transitive_fetches(), 92 } 93 } 94} 95 96impl Default for MlfConfig { 97 fn default() -> Self { 98 Self { 99 source: SourceConfig::default(), 100 output: vec![], 101 dependencies: DependenciesConfig::default(), 102 } 103 } 104} 105 106impl MlfConfig { 107 pub fn load(path: &Path) -> Result<Self, ConfigError> { 108 let content = std::fs::read_to_string(path)?; 109 let config: MlfConfig = toml::from_str(&content)?; 110 Ok(config) 111 } 112 113 pub fn save(&self, path: &Path) -> Result<(), ConfigError> { 114 let content = toml::to_string_pretty(self) 115 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 116 std::fs::write(path, content)?; 117 Ok(()) 118 } 119 120 pub fn create_default(path: &Path) -> Result<Self, ConfigError> { 121 let config = MlfConfig::default(); 122 config.save(path)?; 123 Ok(config) 124 } 125} 126 127/// Find the project root by looking for mlf.toml 128/// Searches upward from the given directory 129pub fn find_project_root(start_dir: &Path) -> Result<PathBuf, ConfigError> { 130 let mut current = start_dir.to_path_buf(); 131 const MAX_DEPTH: usize = 10; 132 133 for _ in 0..MAX_DEPTH { 134 let config_path = current.join("mlf.toml"); 135 if config_path.exists() { 136 return Ok(current); 137 } 138 139 match current.parent() { 140 Some(parent) => current = parent.to_path_buf(), 141 None => break, 142 } 143 } 144 145 Err(ConfigError::NotFound) 146} 147 148/// Get or create the .mlf cache directory 149pub fn get_mlf_cache_dir(project_root: &Path) -> PathBuf { 150 project_root.join(".mlf") 151} 152 153/// Initialize the .mlf directory structure 154pub fn init_mlf_cache(project_root: &Path) -> std::io::Result<()> { 155 let mlf_dir = get_mlf_cache_dir(project_root); 156 157 // Create directory structure 158 std::fs::create_dir_all(&mlf_dir)?; 159 std::fs::create_dir_all(mlf_dir.join("lexicons/json"))?; 160 std::fs::create_dir_all(mlf_dir.join("lexicons/mlf"))?; 161 162 // Create .gitignore 163 let gitignore_path = mlf_dir.join(".gitignore"); 164 if !gitignore_path.exists() { 165 std::fs::write(&gitignore_path, "*\n!.gitignore\n")?; 166 } 167 168 Ok(()) 169} 170 171/// Lock file format for tracking resolved lexicons 172#[derive(Debug, Serialize, Deserialize, Default)] 173pub struct LockFile { 174 /// Lock file format version 175 pub version: u32, 176 177 /// All resolved lexicons (both direct and transitive dependencies) 178 #[serde(default)] 179 pub lexicons: HashMap<String, LockedLexicon>, 180} 181 182/// A single locked lexicon entry 183#[derive(Debug, Clone, Serialize, Deserialize)] 184pub struct LockedLexicon { 185 /// The NSID of this lexicon 186 pub nsid: String, 187 188 /// The DID of the repository this was fetched from 189 pub did: String, 190 191 /// SHA-256 checksum of the JSON content 192 pub checksum: String, 193 194 /// List of NSIDs this lexicon depends on (external references) 195 #[serde(default, skip_serializing_if = "Vec::is_empty")] 196 pub dependencies: Vec<String>, 197} 198 199impl LockFile { 200 pub fn new() -> Self { 201 Self { 202 version: 1, 203 lexicons: HashMap::new(), 204 } 205 } 206 207 pub fn load(path: &Path) -> Result<Self, ConfigError> { 208 if !path.exists() { 209 return Ok(Self::new()); 210 } 211 212 let content = std::fs::read_to_string(path)?; 213 toml::from_str(&content).map_err(|e| ConfigError::ParseError(e)) 214 } 215 216 pub fn save(&self, path: &Path) -> Result<(), ConfigError> { 217 let content = toml::to_string_pretty(self) 218 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 219 std::fs::write(path, content)?; 220 Ok(()) 221 } 222 223 pub fn add_lexicon(&mut self, nsid: String, did: String, checksum: String, dependencies: Vec<String>) { 224 self.lexicons.insert(nsid.clone(), LockedLexicon { 225 nsid, 226 did, 227 checksum, 228 dependencies, 229 }); 230 } 231} 232 233#[cfg(test)] 234mod tests { 235 use super::*; 236 237 #[test] 238 fn test_default_config() { 239 let config = MlfConfig::default(); 240 assert_eq!(config.source.directory, "./lexicons"); 241 assert!(config.output.is_empty()); 242 assert!(config.dependencies.dependencies.is_empty()); 243 } 244 245 #[test] 246 fn test_lockfile_basic() { 247 let mut lockfile = LockFile::new(); 248 assert_eq!(lockfile.version, 1); 249 assert!(lockfile.lexicons.is_empty()); 250 251 lockfile.add_lexicon( 252 "app.bsky.actor.profile".to_string(), 253 "did:plc:test".to_string(), 254 "sha256:abc123".to_string(), 255 vec![], 256 ); 257 258 assert_eq!(lockfile.lexicons.len(), 1); 259 let locked = lockfile.lexicons.get("app.bsky.actor.profile").unwrap(); 260 assert_eq!(locked.nsid, "app.bsky.actor.profile"); 261 assert_eq!(locked.did, "did:plc:test"); 262 assert_eq!(locked.checksum, "sha256:abc123"); 263 } 264}