ryo_suggest/generator/
store.rs1use ryo_pattern::{GeneratorLoader, GeneratorTemplate};
8use std::path::Path;
9use thiserror::Error;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum GeneratorScope {
14 Global,
16 Project,
18}
19
20impl std::fmt::Display for GeneratorScope {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 GeneratorScope::Global => write!(f, "global"),
24 GeneratorScope::Project => write!(f, "project"),
25 }
26 }
27}
28
29#[derive(Debug, Error)]
31pub enum GeneratorStoreError {
32 #[error("Failed to load generator: {0}")]
33 Load(#[from] ryo_pattern::GeneratorLoadError),
34
35 #[error("IO error: {0}")]
36 Io(#[from] std::io::Error),
37
38 #[error("Home directory not found")]
39 NoHomeDir,
40}
41
42#[derive(Debug, Clone)]
44pub struct GeneratorEntry {
45 pub template: GeneratorTemplate,
47 pub scope: GeneratorScope,
49}
50
51#[derive(Debug, Default)]
59pub struct GeneratorStore {
60 global: Vec<GeneratorEntry>,
61 project: Vec<GeneratorEntry>,
62}
63
64impl GeneratorStore {
65 pub const GLOBAL_GENERATORS_DIR: &'static str = "generators";
67
68 pub const PROJECT_GENERATORS_DIR: &'static str = ".ryo/generators";
70
71 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn load(project_path: &Path) -> Result<Self, GeneratorStoreError> {
81 let global = Self::load_global()?;
82 let project = Self::load_project(project_path)?;
83
84 Ok(Self { global, project })
85 }
86
87 pub fn global_only() -> Result<Self, GeneratorStoreError> {
89 Ok(Self {
90 global: Self::load_global()?,
91 project: Vec::new(),
92 })
93 }
94
95 fn load_global() -> Result<Vec<GeneratorEntry>, GeneratorStoreError> {
97 let home = dirs::home_dir().ok_or(GeneratorStoreError::NoHomeDir)?;
98 let global_dir = home.join(".ryo").join(Self::GLOBAL_GENERATORS_DIR);
99
100 if !global_dir.exists() {
101 return Ok(Vec::new());
102 }
103
104 Self::load_from_dir(&global_dir, GeneratorScope::Global)
105 }
106
107 fn load_project(project_path: &Path) -> Result<Vec<GeneratorEntry>, GeneratorStoreError> {
109 let project_dir = project_path.join(Self::PROJECT_GENERATORS_DIR);
110
111 if !project_dir.exists() {
112 return Ok(Vec::new());
113 }
114
115 Self::load_from_dir(&project_dir, GeneratorScope::Project)
116 }
117
118 fn load_from_dir(
120 dir: &Path,
121 scope: GeneratorScope,
122 ) -> Result<Vec<GeneratorEntry>, GeneratorStoreError> {
123 let templates = GeneratorLoader::load_dir(dir)?;
124 Ok(templates
125 .into_iter()
126 .map(|template| GeneratorEntry { template, scope })
127 .collect())
128 }
129
130 pub fn find_by_id(&self, id: &str) -> Option<&GeneratorEntry> {
132 if let Some(entry) = self.project.iter().find(|e| e.template.id() == id) {
134 return Some(entry);
135 }
136
137 self.global.iter().find(|e| e.template.id() == id)
139 }
140
141 pub fn find_by_name(&self, name: &str) -> Option<&GeneratorEntry> {
143 if let Some(entry) = self.project.iter().find(|e| e.template.name() == name) {
145 return Some(entry);
146 }
147
148 self.global.iter().find(|e| e.template.name() == name)
150 }
151
152 pub fn all_generators(&self) -> impl Iterator<Item = &GeneratorEntry> {
154 let project_ids: std::collections::HashSet<_> =
156 self.project.iter().map(|e| e.template.id()).collect();
157
158 self.project.iter().chain(
160 self.global
161 .iter()
162 .filter(move |e| !project_ids.contains(e.template.id())),
163 )
164 }
165
166 pub fn len(&self) -> usize {
168 let project_ids: std::collections::HashSet<_> =
170 self.project.iter().map(|e| e.template.id()).collect();
171
172 let global_unique = self
173 .global
174 .iter()
175 .filter(|e| !project_ids.contains(e.template.id()))
176 .count();
177
178 self.project.len() + global_unique
179 }
180
181 pub fn is_empty(&self) -> bool {
183 self.global.is_empty() && self.project.is_empty()
184 }
185
186 pub fn list_names(&self) -> Vec<(&str, GeneratorScope)> {
188 self.all_generators()
189 .map(|e| (e.template.name(), e.scope))
190 .collect()
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use std::io::Write;
198 use tempfile::TempDir;
199
200 fn create_test_generator(dir: &Path, name: &str) {
201 let content = format!(
202 r#"
203generator:
204 id: "{name}"
205 name: "{name}"
206 description: Test generator
207
208params:
209 - name: value
210 description: Test param
211 required: true
212
213template:
214 code: "// Generated: {{{{value}}}}"
215"#
216 );
217
218 let path = dir.join(format!("{}.yaml", name));
219 let mut file = std::fs::File::create(path).unwrap();
220 file.write_all(content.as_bytes()).unwrap();
221 }
222
223 #[test]
224 fn test_load_from_dir() {
225 let temp = TempDir::new().unwrap();
226 create_test_generator(temp.path(), "test_gen");
227
228 let entries = GeneratorStore::load_from_dir(temp.path(), GeneratorScope::Project).unwrap();
229 assert_eq!(entries.len(), 1);
230 assert_eq!(entries[0].template.name(), "test_gen");
231 }
232
233 #[test]
234 fn test_find_by_name() {
235 let temp = TempDir::new().unwrap();
236 let gen_dir = temp.path().join(".ryo/generators");
237 std::fs::create_dir_all(&gen_dir).unwrap();
238 create_test_generator(&gen_dir, "my_generator");
239
240 let store = GeneratorStore::load(temp.path()).unwrap();
241 let entry = store.find_by_name("my_generator");
242 assert!(entry.is_some());
243 assert_eq!(entry.unwrap().scope, GeneratorScope::Project);
244 }
245
246 #[test]
247 fn test_empty_store() {
248 let store = GeneratorStore::new();
249 assert!(store.is_empty());
250 assert_eq!(store.len(), 0);
251 }
252}