miyabi_modes/
loader.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use tracing::{debug, info, warn};
4
5use crate::error::{ModeError, ModeResult};
6use crate::mode::MiyabiMode;
7
8/// Mode loader for loading modes from .miyabi/modes directory
9pub struct ModeLoader {
10    modes_dir: PathBuf,
11}
12
13impl ModeLoader {
14    /// Create a new mode loader for the given project root
15    pub fn new(project_root: &Path) -> Self {
16        Self {
17            modes_dir: project_root.join(".miyabi/modes"),
18        }
19    }
20
21    /// Load all modes (system + custom)
22    pub fn load_all(&self) -> ModeResult<Vec<MiyabiMode>> {
23        let mut modes = Vec::new();
24
25        // Load system modes
26        match self.load_system_modes() {
27            Ok(system_modes) => {
28                info!("Loaded {} system modes", system_modes.len());
29                modes.extend(system_modes);
30            },
31            Err(e) => {
32                warn!("Failed to load system modes: {}", e);
33            },
34        }
35
36        // Load custom modes
37        match self.load_custom_modes() {
38            Ok(custom_modes) => {
39                info!("Loaded {} custom modes", custom_modes.len());
40                modes.extend(custom_modes);
41            },
42            Err(e) => {
43                debug!("No custom modes loaded: {}", e);
44            },
45        }
46
47        if modes.is_empty() {
48            return Err(ModeError::InvalidDefinition(
49                "No modes loaded. Check .miyabi/modes directory.".into(),
50            ));
51        }
52
53        Ok(modes)
54    }
55
56    /// Load system modes from .miyabi/modes/system/
57    fn load_system_modes(&self) -> ModeResult<Vec<MiyabiMode>> {
58        let system_dir = self.modes_dir.join("system");
59        if !system_dir.exists() {
60            return Err(ModeError::InvalidDefinition(format!(
61                "System modes directory not found: {}",
62                system_dir.display()
63            )));
64        }
65        self.load_from_dir(&system_dir)
66    }
67
68    /// Load custom modes from .miyabi/modes/custom/
69    fn load_custom_modes(&self) -> ModeResult<Vec<MiyabiMode>> {
70        let custom_dir = self.modes_dir.join("custom");
71        if !custom_dir.exists() {
72            return Ok(Vec::new());
73        }
74        self.load_from_dir(&custom_dir)
75    }
76
77    /// Load all YAML files from a directory
78    fn load_from_dir(&self, dir: &Path) -> ModeResult<Vec<MiyabiMode>> {
79        let mut modes = Vec::new();
80
81        for entry in fs::read_dir(dir)? {
82            let entry = entry?;
83            let path = entry.path();
84
85            if path.extension().and_then(|s| s.to_str()) == Some("yaml") {
86                match self.load_file(&path) {
87                    Ok(mode) => {
88                        debug!("Loaded mode '{}' from {:?}", mode.slug, path);
89                        modes.push(mode);
90                    },
91                    Err(e) => {
92                        warn!("Failed to load mode from {:?}: {}", path, e);
93                    },
94                }
95            }
96        }
97
98        Ok(modes)
99    }
100
101    /// Load a single mode from a YAML file
102    fn load_file(&self, path: &Path) -> ModeResult<MiyabiMode> {
103        let yaml = fs::read_to_string(path)?;
104        let mode: MiyabiMode = serde_yaml::from_str(&yaml)?;
105
106        // Validate required fields
107        if mode.slug.is_empty() {
108            return Err(ModeError::MissingField("slug".into()));
109        }
110        if mode.name.is_empty() {
111            return Err(ModeError::MissingField("name".into()));
112        }
113
114        // Validate file regex if present
115        if let Some(ref regex) = mode.file_regex {
116            regex::Regex::new(regex)?;
117        }
118
119        Ok(mode)
120    }
121
122    /// Get the modes directory path
123    pub fn modes_dir(&self) -> &Path {
124        &self.modes_dir
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use tempfile::TempDir;
132
133    fn create_test_mode_yaml(dir: &Path, slug: &str) -> std::io::Result<()> {
134        let yaml = format!(
135            r#"
136slug: {}
137name: "Test Mode"
138character: "てすとん"
139roleDefinition: "Test role"
140whenToUse: "Test usage"
141groups:
142  - read
143  - edit
144customInstructions: "Test instructions"
145source: "user"
146"#,
147            slug
148        );
149        fs::write(dir.join(format!("{}.yaml", slug)), yaml)
150    }
151
152    #[test]
153    fn test_load_from_dir() {
154        let temp_dir = TempDir::new().unwrap();
155        let modes_dir = temp_dir.path().join(".miyabi/modes/custom");
156        fs::create_dir_all(&modes_dir).unwrap();
157
158        create_test_mode_yaml(&modes_dir, "test1").unwrap();
159        create_test_mode_yaml(&modes_dir, "test2").unwrap();
160
161        let loader = ModeLoader::new(temp_dir.path());
162        let modes = loader.load_from_dir(&modes_dir).unwrap();
163
164        assert_eq!(modes.len(), 2);
165        assert!(modes.iter().any(|m| m.slug == "test1"));
166        assert!(modes.iter().any(|m| m.slug == "test2"));
167    }
168
169    #[test]
170    fn test_missing_slug() {
171        let temp_dir = TempDir::new().unwrap();
172        let modes_dir = temp_dir.path().join(".miyabi/modes/custom");
173        fs::create_dir_all(&modes_dir).unwrap();
174
175        let invalid_yaml = r#"
176name: "Test Mode"
177character: "てすとん"
178roleDefinition: "Test"
179whenToUse: "Test"
180groups: [read]
181customInstructions: "Test"
182source: "user"
183"#;
184        fs::write(modes_dir.join("invalid.yaml"), invalid_yaml).unwrap();
185
186        let loader = ModeLoader::new(temp_dir.path());
187        let result = loader.load_file(&modes_dir.join("invalid.yaml"));
188
189        assert!(result.is_err());
190    }
191}