Skip to main content

fcp_core/
verb_registry.rs

1use std::collections::HashMap;
2
3/// Defines a single verb in an FCP protocol.
4#[derive(Debug, Clone)]
5pub struct VerbSpec {
6    pub name: String,
7    pub syntax: String,
8    pub category: String,
9}
10
11struct CategoryGroup {
12    name: String,
13    specs: Vec<VerbSpec>,
14}
15
16/// Registry of verb specifications that supports lookup by verb name
17/// and reference card generation grouped by category.
18pub struct VerbRegistry {
19    specs: HashMap<String, VerbSpec>,
20    categories: Vec<CategoryGroup>,
21    cat_index: HashMap<String, usize>,
22}
23
24impl VerbRegistry {
25    /// Creates a new empty VerbRegistry.
26    pub fn new() -> Self {
27        VerbRegistry {
28            specs: HashMap::new(),
29            categories: Vec::new(),
30            cat_index: HashMap::new(),
31        }
32    }
33
34    /// Registers a single verb specification.
35    pub fn register(&mut self, spec: VerbSpec) {
36        self.specs.insert(spec.name.clone(), spec.clone());
37        if let Some(&idx) = self.cat_index.get(&spec.category) {
38            self.categories[idx].specs.push(spec);
39        } else {
40            let idx = self.categories.len();
41            self.cat_index.insert(spec.category.clone(), idx);
42            self.categories.push(CategoryGroup {
43                name: spec.category.clone(),
44                specs: vec![spec],
45            });
46        }
47    }
48
49    /// Registers multiple verb specifications at once.
50    pub fn register_many(&mut self, specs: Vec<VerbSpec>) {
51        for spec in specs {
52            self.register(spec);
53        }
54    }
55
56    /// Looks up a verb specification by name.
57    pub fn lookup(&self, name: &str) -> Option<&VerbSpec> {
58        self.specs.get(name)
59    }
60
61    /// Returns all registered verb specifications.
62    pub fn verbs(&self) -> Vec<&VerbSpec> {
63        self.specs.values().collect()
64    }
65
66    /// Generates a reference card string grouped by category.
67    /// Optional extra_sections adds extra static sections appended after the verb listing.
68    pub fn generate_reference_card(
69        &self,
70        extra_sections: Option<&HashMap<String, String>>,
71    ) -> String {
72        let mut lines: Vec<String> = Vec::new();
73
74        for cat in &self.categories {
75            let name = cat.name.to_uppercase();
76            lines.push(format!("{name}:"));
77            for spec in &cat.specs {
78                let syntax = &spec.syntax;
79                lines.push(format!("  {syntax}"));
80            }
81            lines.push(String::new());
82        }
83
84        if let Some(sections) = extra_sections {
85            for (title, content) in sections {
86                let heading = title.to_uppercase();
87                lines.push(format!("{heading}:"));
88                lines.push(content.clone());
89                lines.push(String::new());
90            }
91        }
92
93        // Remove trailing empty lines
94        while lines.last().is_some_and(|l| l.is_empty()) {
95            lines.pop();
96        }
97
98        lines.join("\n")
99    }
100}
101
102impl Default for VerbRegistry {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    fn create_test_registry() -> VerbRegistry {
113        let mut reg = VerbRegistry::new();
114        reg.register_many(vec![
115            VerbSpec {
116                name: "add".to_string(),
117                syntax: "add TYPE LABEL [key:value]".to_string(),
118                category: "create".to_string(),
119            },
120            VerbSpec {
121                name: "remove".to_string(),
122                syntax: "remove SELECTOR".to_string(),
123                category: "modify".to_string(),
124            },
125            VerbSpec {
126                name: "connect".to_string(),
127                syntax: "connect SRC -> TGT".to_string(),
128                category: "create".to_string(),
129            },
130            VerbSpec {
131                name: "style".to_string(),
132                syntax: "style REF [fill:#HEX]".to_string(),
133                category: "modify".to_string(),
134            },
135        ]);
136        reg
137    }
138
139    #[test]
140    fn test_register_and_lookup() {
141        let mut reg = VerbRegistry::new();
142        reg.register(VerbSpec {
143            name: "add".to_string(),
144            syntax: "add TYPE LABEL".to_string(),
145            category: "create".to_string(),
146        });
147        let spec = reg.lookup("add").expect("expected to find 'add'");
148        assert_eq!(spec.name, "add");
149        assert_eq!(spec.syntax, "add TYPE LABEL");
150        assert_eq!(spec.category, "create");
151    }
152
153    #[test]
154    fn test_lookup_unknown() {
155        let reg = VerbRegistry::new();
156        assert!(reg.lookup("nonexistent").is_none());
157    }
158
159    #[test]
160    fn test_register_many() {
161        let reg = create_test_registry();
162        for name in &["add", "remove", "connect", "style"] {
163            assert!(
164                reg.lookup(name).is_some(),
165                "expected to find {:?}",
166                name
167            );
168        }
169    }
170
171    #[test]
172    fn test_verbs() {
173        let reg = create_test_registry();
174        let verbs = reg.verbs();
175        assert_eq!(verbs.len(), 4);
176        let names: Vec<&str> = verbs.iter().map(|v| v.name.as_str()).collect();
177        for name in &["add", "remove", "connect", "style"] {
178            assert!(names.contains(name), "missing verb {:?}", name);
179        }
180    }
181
182    #[test]
183    fn test_reference_card() {
184        let reg = create_test_registry();
185        let card = reg.generate_reference_card(None);
186        assert!(card.contains("CREATE:"), "card missing CREATE:");
187        assert!(card.contains("MODIFY:"), "card missing MODIFY:");
188        assert!(
189            card.contains("  add TYPE LABEL [key:value]"),
190            "card missing add syntax"
191        );
192        assert!(
193            card.contains("  connect SRC -> TGT"),
194            "card missing connect syntax"
195        );
196        assert!(
197            card.contains("  remove SELECTOR"),
198            "card missing remove syntax"
199        );
200        assert!(
201            card.contains("  style REF [fill:#HEX]"),
202            "card missing style syntax"
203        );
204    }
205
206    #[test]
207    fn test_reference_card_extra_sections() {
208        let reg = create_test_registry();
209        let mut extra = HashMap::new();
210        extra.insert(
211            "Themes".to_string(),
212            "  blue  #dae8fc\n  red   #f8cecc".to_string(),
213        );
214        let card = reg.generate_reference_card(Some(&extra));
215        assert!(card.contains("THEMES:"), "card missing THEMES:");
216        assert!(
217            card.contains("  blue  #dae8fc"),
218            "card missing theme content"
219        );
220    }
221
222    #[test]
223    fn test_reference_card_empty() {
224        let reg = VerbRegistry::new();
225        let card = reg.generate_reference_card(None);
226        assert_eq!(card, "");
227    }
228}