ricecoder_commands/
output_injection.rs1use crate::error::Result;
2use crate::types::CommandExecutionResult;
3
4#[derive(Debug, Clone)]
6pub struct OutputInjectionConfig {
7 pub inject_stdout: bool,
9
10 pub inject_stderr: bool,
12
13 pub max_length: usize,
15
16 pub include_exit_code: bool,
18
19 pub include_duration: bool,
21
22 pub format: OutputFormat,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum OutputFormat {
29 Plain,
31
32 Markdown,
34
35 Json,
37}
38
39pub struct OutputInjector;
41
42impl OutputInjector {
43 pub fn inject(
45 result: &CommandExecutionResult,
46 config: &OutputInjectionConfig,
47 ) -> Result<String> {
48 match config.format {
49 OutputFormat::Plain => Self::format_plain(result, config),
50 OutputFormat::Markdown => Self::format_markdown(result, config),
51 OutputFormat::Json => Self::format_json(result, config),
52 }
53 }
54
55 fn format_plain(
57 result: &CommandExecutionResult,
58 config: &OutputInjectionConfig,
59 ) -> Result<String> {
60 let mut output = String::new();
61
62 if config.include_exit_code {
63 output.push_str(&format!("Exit Code: {}\n", result.exit_code));
64 }
65
66 if config.include_duration {
67 output.push_str(&format!("Duration: {}ms\n", result.duration_ms));
68 }
69
70 if config.inject_stdout && !result.stdout.is_empty() {
71 let stdout = if config.max_length > 0 && result.stdout.len() > config.max_length {
72 format!("{}... (truncated)", &result.stdout[..config.max_length])
73 } else {
74 result.stdout.clone()
75 };
76 output.push_str(&format!("Output:\n{}\n", stdout));
77 }
78
79 if config.inject_stderr && !result.stderr.is_empty() {
80 let stderr = if config.max_length > 0 && result.stderr.len() > config.max_length {
81 format!("{}... (truncated)", &result.stderr[..config.max_length])
82 } else {
83 result.stderr.clone()
84 };
85 output.push_str(&format!("Error:\n{}\n", stderr));
86 }
87
88 Ok(output.trim().to_string())
89 }
90
91 fn format_markdown(
93 result: &CommandExecutionResult,
94 config: &OutputInjectionConfig,
95 ) -> Result<String> {
96 let mut output = String::new();
97
98 if config.include_exit_code {
99 output.push_str(&format!("**Exit Code:** {}\n", result.exit_code));
100 }
101
102 if config.include_duration {
103 output.push_str(&format!("**Duration:** {}ms\n", result.duration_ms));
104 }
105
106 if config.inject_stdout && !result.stdout.is_empty() {
107 let stdout = if config.max_length > 0 && result.stdout.len() > config.max_length {
108 format!("{}... (truncated)", &result.stdout[..config.max_length])
109 } else {
110 result.stdout.clone()
111 };
112 output.push_str(&format!("```\n{}\n```\n", stdout));
113 }
114
115 if config.inject_stderr && !result.stderr.is_empty() {
116 let stderr = if config.max_length > 0 && result.stderr.len() > config.max_length {
117 format!("{}... (truncated)", &result.stderr[..config.max_length])
118 } else {
119 result.stderr.clone()
120 };
121 output.push_str(&format!("**Error:**\n```\n{}\n```\n", stderr));
122 }
123
124 Ok(output.trim().to_string())
125 }
126
127 fn format_json(
129 result: &CommandExecutionResult,
130 config: &OutputInjectionConfig,
131 ) -> Result<String> {
132 let mut json = serde_json::json!({
133 "command_id": result.command_id,
134 "exit_code": result.exit_code,
135 "success": result.success,
136 });
137
138 if config.include_duration {
139 json["duration_ms"] = serde_json::json!(result.duration_ms);
140 }
141
142 if config.inject_stdout {
143 let stdout = if config.max_length > 0 && result.stdout.len() > config.max_length {
144 format!("{}... (truncated)", &result.stdout[..config.max_length])
145 } else {
146 result.stdout.clone()
147 };
148 json["stdout"] = serde_json::json!(stdout);
149 }
150
151 if config.inject_stderr {
152 let stderr = if config.max_length > 0 && result.stderr.len() > config.max_length {
153 format!("{}... (truncated)", &result.stderr[..config.max_length])
154 } else {
155 result.stderr.clone()
156 };
157 json["stderr"] = serde_json::json!(stderr);
158 }
159
160 Ok(serde_json::to_string_pretty(&json)?)
161 }
162}
163
164impl Default for OutputInjectionConfig {
165 fn default() -> Self {
166 Self {
167 inject_stdout: true,
168 inject_stderr: true,
169 max_length: 5000,
170 include_exit_code: true,
171 include_duration: true,
172 format: OutputFormat::Markdown,
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 fn create_test_result() -> CommandExecutionResult {
182 CommandExecutionResult::new("test-cmd", 0)
183 .with_stdout("Hello World")
184 .with_stderr("")
185 .with_duration(100)
186 }
187
188 #[test]
189 fn test_format_plain() {
190 let result = create_test_result();
191 let config = OutputInjectionConfig {
192 format: OutputFormat::Plain,
193 ..Default::default()
194 };
195
196 let output = OutputInjector::inject(&result, &config).unwrap();
197 assert!(output.contains("Exit Code: 0"));
198 assert!(output.contains("Duration: 100ms"));
199 assert!(output.contains("Hello World"));
200 }
201
202 #[test]
203 fn test_format_markdown() {
204 let result = create_test_result();
205 let config = OutputInjectionConfig {
206 format: OutputFormat::Markdown,
207 ..Default::default()
208 };
209
210 let output = OutputInjector::inject(&result, &config).unwrap();
211 assert!(output.contains("**Exit Code:** 0"));
212 assert!(output.contains("**Duration:** 100ms"));
213 assert!(output.contains("```"));
214 assert!(output.contains("Hello World"));
215 }
216
217 #[test]
218 fn test_format_json() {
219 let result = create_test_result();
220 let config = OutputInjectionConfig {
221 format: OutputFormat::Json,
222 ..Default::default()
223 };
224
225 let output = OutputInjector::inject(&result, &config).unwrap();
226 let json: serde_json::Value = serde_json::from_str(&output).unwrap();
227 assert_eq!(json["command_id"], "test-cmd");
228 assert_eq!(json["exit_code"], 0);
229 assert!(json["success"].as_bool().unwrap());
230 }
231
232 #[test]
233 fn test_truncate_output() {
234 let mut result = create_test_result();
235 result.stdout = "a".repeat(10000);
236
237 let config = OutputInjectionConfig {
238 format: OutputFormat::Plain,
239 max_length: 100,
240 ..Default::default()
241 };
242
243 let output = OutputInjector::inject(&result, &config).unwrap();
244 assert!(output.contains("(truncated)"));
245 assert!(!output.contains(&"a".repeat(10000)));
246 }
247
248 #[test]
249 fn test_exclude_stderr() {
250 let mut result = create_test_result();
251 result.stderr = "Error message".to_string();
252
253 let config = OutputInjectionConfig {
254 format: OutputFormat::Plain,
255 inject_stderr: false,
256 ..Default::default()
257 };
258
259 let output = OutputInjector::inject(&result, &config).unwrap();
260 assert!(!output.contains("Error message"));
261 }
262
263 #[test]
264 fn test_exclude_exit_code() {
265 let result = create_test_result();
266 let config = OutputInjectionConfig {
267 format: OutputFormat::Plain,
268 include_exit_code: false,
269 ..Default::default()
270 };
271
272 let output = OutputInjector::inject(&result, &config).unwrap();
273 assert!(!output.contains("Exit Code"));
274 }
275}