fcp_core/
verb_registry.rs1use std::collections::HashMap;
2
3#[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
16pub struct VerbRegistry {
19 specs: HashMap<String, VerbSpec>,
20 categories: Vec<CategoryGroup>,
21 cat_index: HashMap<String, usize>,
22}
23
24impl VerbRegistry {
25 pub fn new() -> Self {
27 VerbRegistry {
28 specs: HashMap::new(),
29 categories: Vec::new(),
30 cat_index: HashMap::new(),
31 }
32 }
33
34 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 pub fn register_many(&mut self, specs: Vec<VerbSpec>) {
51 for spec in specs {
52 self.register(spec);
53 }
54 }
55
56 pub fn lookup(&self, name: &str) -> Option<&VerbSpec> {
58 self.specs.get(name)
59 }
60
61 pub fn verbs(&self) -> Vec<&VerbSpec> {
63 self.specs.values().collect()
64 }
65
66 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 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}