ricecoder_hooks/cli/
formatter.rs

1//! Output formatting for hook commands
2
3use crate::error::{HooksError, Result};
4use crate::types::{Action, Hook};
5
6/// Format a single hook as a table
7pub fn format_hook_table(hook: &Hook) -> String {
8    let status = if hook.enabled {
9        "✓ Enabled"
10    } else {
11        "✗ Disabled"
12    };
13    let action_type = match &hook.action {
14        Action::Command(_) => "Command",
15        Action::ToolCall(_) => "Tool Call",
16        Action::AiPrompt(_) => "AI Prompt",
17        Action::Chain(_) => "Chain",
18    };
19
20    let mut output = String::new();
21    output.push_str(&format!("ID:          {}\n", hook.id));
22    output.push_str(&format!("Name:        {}\n", hook.name));
23    if let Some(desc) = &hook.description {
24        output.push_str(&format!("Description: {}\n", desc));
25    }
26    output.push_str(&format!("Event:       {}\n", hook.event));
27    output.push_str(&format!("Action:      {}\n", action_type));
28    output.push_str(&format!("Status:      {}\n", status));
29    if !hook.tags.is_empty() {
30        output.push_str(&format!("Tags:        {}\n", hook.tags.join(", ")));
31    }
32
33    output
34}
35
36/// Format multiple hooks as a table
37pub fn format_hooks_table(hooks: &[Hook]) -> String {
38    if hooks.is_empty() {
39        return "No hooks found".to_string();
40    }
41
42    let mut output = String::new();
43    output.push_str("ID                                   | Name                     | Event              | Status   | Action\n");
44    output.push_str("-------------------------------------|--------------------------|--------------------|---------|-----------\n");
45
46    for hook in hooks {
47        let status = if hook.enabled { "Enabled" } else { "Disabled" };
48        let action_type = match &hook.action {
49            Action::Command(_) => "Command",
50            Action::ToolCall(_) => "Tool Call",
51            Action::AiPrompt(_) => "AI Prompt",
52            Action::Chain(_) => "Chain",
53        };
54
55        let id = if hook.id.len() > 36 {
56            format!("{}...", &hook.id[..33])
57        } else {
58            hook.id.clone()
59        };
60
61        let name = if hook.name.len() > 24 {
62            format!("{}...", &hook.name[..21])
63        } else {
64            hook.name.clone()
65        };
66
67        let event = if hook.event.len() > 18 {
68            format!("{}...", &hook.event[..15])
69        } else {
70            hook.event.clone()
71        };
72
73        output.push_str(&format!(
74            "{:<36} | {:<24} | {:<18} | {:<8} | {}\n",
75            id, name, event, status, action_type
76        ));
77    }
78
79    output
80}
81
82/// Format a single hook as JSON
83pub fn format_hook_json(hook: &Hook) -> Result<String> {
84    serde_json::to_string_pretty(hook)
85        .map_err(|e| HooksError::InvalidConfiguration(format!("Failed to serialize hook: {}", e)))
86}
87
88/// Format multiple hooks as JSON
89pub fn format_hooks_json(hooks: &[Hook]) -> Result<String> {
90    serde_json::to_string_pretty(hooks)
91        .map_err(|e| HooksError::InvalidConfiguration(format!("Failed to serialize hooks: {}", e)))
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::types::CommandAction;
98
99    fn create_test_hook(id: &str, name: &str) -> Hook {
100        Hook {
101            id: id.to_string(),
102            name: name.to_string(),
103            description: Some("Test hook".to_string()),
104            event: "test_event".to_string(),
105            action: Action::Command(CommandAction {
106                command: "echo".to_string(),
107                args: vec!["test".to_string()],
108                timeout_ms: Some(5000),
109                capture_output: true,
110            }),
111            enabled: true,
112            tags: vec!["test".to_string()],
113            metadata: serde_json::json!({}),
114            condition: None,
115        }
116    }
117
118    #[test]
119    fn test_format_hook_table() {
120        let hook = create_test_hook("hook1", "Test Hook");
121        let output = format_hook_table(&hook);
122
123        assert!(output.contains("hook1"));
124        assert!(output.contains("Test Hook"));
125        assert!(output.contains("test_event"));
126        assert!(output.contains("Enabled"));
127        assert!(output.contains("Command"));
128    }
129
130    #[test]
131    fn test_format_hooks_table_empty() {
132        let hooks: Vec<Hook> = vec![];
133        let output = format_hooks_table(&hooks);
134
135        assert_eq!(output, "No hooks found");
136    }
137
138    #[test]
139    fn test_format_hooks_table() {
140        let hook1 = create_test_hook("hook1", "Hook 1");
141        let hook2 = create_test_hook("hook2", "Hook 2");
142        let hooks = vec![hook1, hook2];
143
144        let output = format_hooks_table(&hooks);
145
146        assert!(output.contains("hook1"));
147        assert!(output.contains("hook2"));
148        assert!(output.contains("Hook 1"));
149        assert!(output.contains("Hook 2"));
150    }
151
152    #[test]
153    fn test_format_hook_json() {
154        let hook = create_test_hook("hook1", "Test Hook");
155        let json = format_hook_json(&hook).unwrap();
156
157        assert!(json.contains("\"id\":\"hook1\"") || json.contains("\"id\": \"hook1\""));
158        assert!(json.contains("Test Hook"));
159    }
160
161    #[test]
162    fn test_format_hooks_json() {
163        let hook1 = create_test_hook("hook1", "Hook 1");
164        let hook2 = create_test_hook("hook2", "Hook 2");
165        let hooks = vec![hook1, hook2];
166
167        let json = format_hooks_json(&hooks).unwrap();
168
169        assert!(json.contains("hook1"));
170        assert!(json.contains("hook2"));
171    }
172
173    #[test]
174    fn test_format_hook_table_disabled() {
175        let mut hook = create_test_hook("hook1", "Test Hook");
176        hook.enabled = false;
177        let output = format_hook_table(&hook);
178
179        assert!(output.contains("Disabled"));
180    }
181
182    #[test]
183    fn test_format_hooks_table_truncation() {
184        let mut hook = create_test_hook("a".repeat(50).as_str(), "b".repeat(50).as_str());
185        hook.event = "c".repeat(50);
186        let output = format_hooks_table(&[hook]);
187
188        // Should contain truncated versions
189        assert!(output.contains("..."));
190    }
191}