ggen_cli_validation/
noun_verb_validator.rs1use crate::error::{Result, ValidationError};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Debug, Default)]
13pub struct NounVerbValidator {
14 dependencies: HashMap<String, Vec<String>>,
16 audit_trail: Vec<AuditEntry>,
18}
19
20#[derive(Debug, Clone, serde::Serialize)]
22pub struct AuditEntry {
23 pub command: String,
25 pub timestamp: String,
27 pub dependencies: Vec<String>,
29 pub success: bool,
31}
32
33impl NounVerbValidator {
34 #[must_use]
36 pub fn new() -> Self {
37 Self {
38 dependencies: HashMap::new(),
39 audit_trail: Vec::new(),
40 }
41 }
42
43 pub fn register_command(&mut self, command: String, deps: Vec<String>) {
45 self.dependencies.insert(command, deps);
46 }
47
48 pub fn validate_sequence(&self, commands: &[String]) -> Result<()> {
50 for command in commands {
51 self.check_circular_dependencies(command, &mut HashSet::new())?;
52 }
53 Ok(())
54 }
55
56 fn check_circular_dependencies(
58 &self, command: &str, visited: &mut HashSet<String>,
59 ) -> Result<()> {
60 if visited.contains(command) {
61 return Err(ValidationError::CircularDependency);
62 }
63
64 visited.insert(command.to_string());
65
66 if let Some(deps) = self.dependencies.get(command) {
67 for dep in deps {
68 self.check_circular_dependencies(dep, visited)?;
69 }
70 }
71
72 visited.remove(command);
73 Ok(())
74 }
75
76 pub fn validate_command_structure(&self, noun: &str, verb: &str) -> Result<()> {
78 if noun.is_empty() {
80 return Err(ValidationError::InvalidCommandStructure {
81 reason: "Noun cannot be empty".to_string(),
82 });
83 }
84
85 if verb.is_empty() {
87 return Err(ValidationError::InvalidCommandStructure {
88 reason: "Verb cannot be empty".to_string(),
89 });
90 }
91
92 let valid_chars = |s: &str| {
94 s.chars()
95 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
96 };
97
98 if !valid_chars(noun) {
99 return Err(ValidationError::InvalidCommandStructure {
100 reason: format!("Noun '{noun}' contains invalid characters"),
101 });
102 }
103
104 if !valid_chars(verb) {
105 return Err(ValidationError::InvalidCommandStructure {
106 reason: format!("Verb '{verb}' contains invalid characters"),
107 });
108 }
109
110 Ok(())
111 }
112
113 pub fn record_execution(&mut self, command: String, dependencies: Vec<String>, success: bool) {
115 let entry = AuditEntry {
116 command,
117 timestamp: chrono::Utc::now().to_rfc3339(),
118 dependencies,
119 success,
120 };
121 self.audit_trail.push(entry);
122 }
123
124 #[must_use]
126 pub fn get_audit_trail(&self) -> &[AuditEntry] {
127 &self.audit_trail
128 }
129
130 pub fn export_audit_trail(&self) -> Result<String> {
135 serde_json::to_string_pretty(&self.audit_trail).map_err(|e| {
136 ValidationError::InvalidCommandStructure {
137 reason: format!("Failed to serialize audit trail: {e}"),
138 }
139 })
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_circular_dependency_detection() {
149 let mut validator = NounVerbValidator::new();
150
151 validator.register_command("A".to_string(), vec!["B".to_string()]);
153 validator.register_command("B".to_string(), vec!["C".to_string()]);
154 validator.register_command("C".to_string(), vec!["A".to_string()]);
155
156 let result = validator.validate_sequence(&["A".to_string()]);
157 assert!(result.is_err());
158 assert!(matches!(result, Err(ValidationError::CircularDependency)));
159 }
160
161 #[test]
162 fn test_valid_command_structure() {
163 let validator = NounVerbValidator::new();
164
165 assert!(validator
166 .validate_command_structure("template", "generate")
167 .is_ok());
168 assert!(validator
169 .validate_command_structure("ontology", "extract")
170 .is_ok());
171 assert!(validator
172 .validate_command_structure("hook", "create")
173 .is_ok());
174 }
175
176 #[test]
177 fn test_invalid_command_structure() {
178 let validator = NounVerbValidator::new();
179
180 assert!(validator.validate_command_structure("", "verb").is_err());
182 assert!(validator.validate_command_structure("noun", "").is_err());
183
184 assert!(validator
186 .validate_command_structure("noun!", "verb")
187 .is_err());
188 assert!(validator
189 .validate_command_structure("noun", "verb*")
190 .is_err());
191 }
192
193 #[allow(clippy::expect_used)]
194 #[test]
195 fn test_audit_trail_recording() {
196 let mut validator = NounVerbValidator::new();
197
198 validator.record_execution(
199 "template generate".to_string(),
200 vec!["template load".to_string()],
201 true,
202 );
203
204 let trail = validator.get_audit_trail();
205 assert_eq!(trail.len(), 1);
206 assert_eq!(trail[0].command, "template generate");
207 assert!(trail[0].success);
208 }
209
210 #[allow(clippy::expect_used)]
211 #[test]
212 fn test_audit_trail_export() {
213 let mut validator = NounVerbValidator::new();
214
215 validator.record_execution("test command".to_string(), vec![], true);
216
217 let json = validator.export_audit_trail();
218 assert!(json.is_ok());
219 assert!(json.expect("JSON should be valid").contains("test command"));
220 }
221
222 #[test]
223 fn test_linear_dependency_chain() {
224 let mut validator = NounVerbValidator::new();
225
226 validator.register_command("A".to_string(), vec!["B".to_string()]);
228 validator.register_command("B".to_string(), vec!["C".to_string()]);
229 validator.register_command("C".to_string(), vec![]);
230
231 let result = validator.validate_sequence(&["A".to_string()]);
232 assert!(result.is_ok());
233 }
234}