1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt;
9use std::path::Path;
10
11pub mod loader;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct Config {
16 #[serde(default)]
18 pub metadata: Metadata,
19 pub tools: Vec<Tool>,
21 #[serde(default)]
23 pub categories: Vec<Category>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28pub struct Metadata {
29 #[serde(default = "default_title")]
31 pub title: String,
32 #[serde(default)]
34 pub description: Option<String>,
35 #[serde(default = "default_version")]
37 pub version: String,
38 #[serde(default)]
40 pub author: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45pub struct Tool {
46 pub id: String,
48 pub name: String,
50 #[serde(default)]
52 pub description: Option<String>,
53 #[serde(default)]
55 pub command: Option<String>,
56 #[serde(default)]
58 pub shortcut: Option<String>,
59 #[serde(default)]
61 pub category: Option<String>,
62 #[serde(default)]
64 pub tags: Vec<String>,
65 #[serde(default)]
67 pub metadata: HashMap<String, String>,
68 #[serde(default = "default_enabled")]
70 pub enabled: bool,
71 #[serde(default)]
73 pub priority: i32,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub struct Category {
79 pub id: String,
81 pub name: String,
83 #[serde(default)]
85 pub description: Option<String>,
86 #[serde(default)]
88 pub icon: Option<String>,
89 #[serde(default)]
91 pub color: Option<String>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
96pub enum ConfigFormat {
97 Toml,
99 Json,
101}
102
103impl ConfigFormat {
104 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
106 path.as_ref()
107 .extension()
108 .and_then(|ext| ext.to_str())
109 .map(|ext| match ext.to_lowercase().as_str() {
110 "toml" => ConfigFormat::Toml,
111 "json" => ConfigFormat::Json,
112 _ => ConfigFormat::Toml, })
114 }
115}
116
117impl fmt::Display for ConfigFormat {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 match self {
120 ConfigFormat::Toml => write!(f, "TOML"),
121 ConfigFormat::Json => write!(f, "JSON"),
122 }
123 }
124}
125
126impl Default for Metadata {
127 fn default() -> Self {
128 Self {
129 title: default_title(),
130 description: None,
131 version: default_version(),
132 author: None,
133 }
134 }
135}
136
137impl Default for Config {
138 fn default() -> Self {
139 Self {
140 metadata: Metadata::default(),
141 tools: Vec::new(),
142 categories: Vec::new(),
143 }
144 }
145}
146
147impl Config {
148 pub fn new() -> Self {
150 Self::default()
151 }
152
153 pub fn add_tool(&mut self, tool: Tool) {
155 self.tools.push(tool);
156 }
157
158 pub fn add_category(&mut self, category: Category) {
160 self.categories.push(category);
161 }
162
163 pub fn tools_by_category(&self, category_id: &str) -> Vec<&Tool> {
165 self.tools
166 .iter()
167 .filter(|tool| {
168 tool.category
169 .as_ref()
170 .map_or(false, |cat| cat == category_id)
171 })
172 .collect()
173 }
174
175 pub fn tools_by_tag(&self, tag: &str) -> Vec<&Tool> {
177 self.tools
178 .iter()
179 .filter(|tool| tool.tags.contains(&tag.to_string()))
180 .collect()
181 }
182
183 pub fn enabled_tools(&self) -> Vec<&Tool> {
185 let mut tools: Vec<&Tool> = self.tools.iter().filter(|tool| tool.enabled).collect();
186 tools.sort_by(|a, b| b.priority.cmp(&a.priority));
187 tools
188 }
189
190 pub fn validate(&self) -> Result<(), Vec<String>> {
192 let mut errors = Vec::new();
193
194 let mut tool_ids = std::collections::HashSet::new();
196 for tool in &self.tools {
197 if !tool_ids.insert(&tool.id) {
198 errors.push(format!("Duplicate tool ID: {}", tool.id));
199 }
200 }
201
202 let mut category_ids = std::collections::HashSet::new();
204 for category in &self.categories {
205 if !category_ids.insert(&category.id) {
206 errors.push(format!("Duplicate category ID: {}", category.id));
207 }
208 }
209
210 let valid_categories: std::collections::HashSet<_> =
212 self.categories.iter().map(|c| &c.id).collect();
213 for tool in &self.tools {
214 if let Some(ref category) = tool.category {
215 if !valid_categories.contains(category) {
216 errors.push(format!(
217 "Tool '{}' references non-existent category '{}'",
218 tool.id, category
219 ));
220 }
221 }
222 }
223
224 if errors.is_empty() {
225 Ok(())
226 } else {
227 Err(errors)
228 }
229 }
230}
231
232impl Tool {
233 pub fn new<S: Into<String>>(id: S, name: S) -> Self {
235 Self {
236 id: id.into(),
237 name: name.into(),
238 description: None,
239 command: None,
240 shortcut: None,
241 category: None,
242 tags: Vec::new(),
243 metadata: HashMap::new(),
244 enabled: true,
245 priority: 0,
246 }
247 }
248
249 pub fn with_description<S: Into<String>>(mut self, description: S) -> Self {
251 self.description = Some(description.into());
252 self
253 }
254
255 pub fn with_command<S: Into<String>>(mut self, command: S) -> Self {
257 self.command = Some(command.into());
258 self
259 }
260
261 pub fn with_shortcut<S: Into<String>>(mut self, shortcut: S) -> Self {
263 self.shortcut = Some(shortcut.into());
264 self
265 }
266
267 pub fn with_category<S: Into<String>>(mut self, category: S) -> Self {
269 self.category = Some(category.into());
270 self
271 }
272
273 pub fn with_tag<S: Into<String>>(mut self, tag: S) -> Self {
275 self.tags.push(tag.into());
276 self
277 }
278
279 pub fn with_priority(mut self, priority: i32) -> Self {
281 self.priority = priority;
282 self
283 }
284
285 pub fn with_enabled(mut self, enabled: bool) -> Self {
287 self.enabled = enabled;
288 self
289 }
290
291 pub fn add_metadata<K, V>(&mut self, key: K, value: V)
293 where
294 K: Into<String>,
295 V: Into<String>,
296 {
297 self.metadata.insert(key.into(), value.into());
298 }
299}
300
301impl Category {
302 pub fn new<S: Into<String>>(id: S, name: S) -> Self {
304 Self {
305 id: id.into(),
306 name: name.into(),
307 description: None,
308 icon: None,
309 color: None,
310 }
311 }
312
313 pub fn with_description<S: Into<String>>(mut self, description: S) -> Self {
315 self.description = Some(description.into());
316 self
317 }
318
319 pub fn with_icon<S: Into<String>>(mut self, icon: S) -> Self {
321 self.icon = Some(icon.into());
322 self
323 }
324
325 pub fn with_color<S: Into<String>>(mut self, color: S) -> Self {
327 self.color = Some(color.into());
328 self
329 }
330}
331
332fn default_title() -> String {
333 "Tools and Shortcuts".to_string()
334}
335
336fn default_version() -> String {
337 "1.0".to_string()
338}
339
340fn default_enabled() -> bool {
341 true
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_config_default() {
350 let config = Config::default();
351 assert_eq!(config.metadata.title, "Tools and Shortcuts");
352 assert_eq!(config.metadata.version, "1.0");
353 assert!(config.tools.is_empty());
354 assert!(config.categories.is_empty());
355 }
356
357 #[test]
358 fn test_tool_builder() {
359 let tool = Tool::new("test", "Test Tool")
360 .with_description("A test tool")
361 .with_command("test-cmd")
362 .with_shortcut("Ctrl+T")
363 .with_category("testing")
364 .with_tag("test")
365 .with_tag("development")
366 .with_priority(10);
367
368 assert_eq!(tool.id, "test");
369 assert_eq!(tool.name, "Test Tool");
370 assert_eq!(tool.description, Some("A test tool".to_string()));
371 assert_eq!(tool.command, Some("test-cmd".to_string()));
372 assert_eq!(tool.shortcut, Some("Ctrl+T".to_string()));
373 assert_eq!(tool.category, Some("testing".to_string()));
374 assert_eq!(tool.tags, vec!["test", "development"]);
375 assert_eq!(tool.priority, 10);
376 assert!(tool.enabled);
377 }
378
379 #[test]
380 fn test_category_builder() {
381 let category = Category::new("dev", "Development")
382 .with_description("Development tools")
383 .with_icon("🔧")
384 .with_color("blue");
385
386 assert_eq!(category.id, "dev");
387 assert_eq!(category.name, "Development");
388 assert_eq!(category.description, Some("Development tools".to_string()));
389 assert_eq!(category.icon, Some("🔧".to_string()));
390 assert_eq!(category.color, Some("blue".to_string()));
391 }
392
393 #[test]
394 fn test_config_validation() {
395 let mut config = Config::new();
396
397 config.add_category(Category::new("dev", "Development"));
399 config.add_category(Category::new("utils", "Utilities"));
400
401 config.add_tool(Tool::new("git", "Git").with_category("dev"));
403 config.add_tool(Tool::new("ls", "List Files").with_category("utils"));
404
405 assert!(config.validate().is_ok());
406
407 config.add_tool(Tool::new("git", "Another Git Tool"));
409 let errors = config.validate().unwrap_err();
410 assert!(errors.iter().any(|e| e.contains("Duplicate tool ID: git")));
411
412 config.tools.pop();
414 config.add_tool(Tool::new("vim", "Vim Editor").with_category("invalid"));
415 let errors = config.validate().unwrap_err();
416 assert!(errors.iter().any(|e| e.contains("non-existent category 'invalid'")));
417 }
418
419 #[test]
420 fn test_config_format_detection() {
421 assert_eq!(ConfigFormat::from_path("config.toml"), Some(ConfigFormat::Toml));
422 assert_eq!(ConfigFormat::from_path("config.json"), Some(ConfigFormat::Json));
423 assert_eq!(ConfigFormat::from_path("config.txt"), Some(ConfigFormat::Toml)); }
425
426 #[test]
427 fn test_tools_by_category() {
428 let mut config = Config::new();
429 config.add_tool(Tool::new("git", "Git").with_category("dev"));
430 config.add_tool(Tool::new("vim", "Vim").with_category("dev"));
431 config.add_tool(Tool::new("ls", "List").with_category("utils"));
432
433 let dev_tools = config.tools_by_category("dev");
434 assert_eq!(dev_tools.len(), 2);
435 assert!(dev_tools.iter().any(|t| t.id == "git"));
436 assert!(dev_tools.iter().any(|t| t.id == "vim"));
437 }
438
439 #[test]
440 fn test_enabled_tools_sorting() {
441 let mut config = Config::new();
442 config.add_tool(Tool::new("low", "Low Priority").with_priority(1));
443 config.add_tool(Tool::new("high", "High Priority").with_priority(10));
444 config.add_tool(Tool::new("disabled", "Disabled").with_enabled(false));
445 config.add_tool(Tool::new("medium", "Medium Priority").with_priority(5));
446
447 let enabled = config.enabled_tools();
448 assert_eq!(enabled.len(), 3);
449 assert_eq!(enabled[0].id, "high");
450 assert_eq!(enabled[1].id, "medium");
451 assert_eq!(enabled[2].id, "low");
452 }
453}