1use crate::error::IndexError;
7use crate::io::exit_code::ExitCode;
8use crate::io::format::{JsonResponse, OutputFormat};
9use crate::io::schema::{OutputData, OutputStatus, UnifiedOutput};
10use serde::Serialize;
11use std::fmt::Display;
12use std::io::{self, Write};
13
14pub struct OutputManager {
19 format: OutputFormat,
20 stdout: Box<dyn Write>,
21 stderr: Box<dyn Write>,
22}
23
24impl OutputManager {
25 pub fn new(format: OutputFormat) -> Self {
27 Self {
28 format,
29 stdout: Box::new(io::stdout()),
30 stderr: Box::new(io::stderr()),
31 }
32 }
33
34 fn write_ignoring_broken_pipe(stream: &mut dyn Write, content: &str) -> io::Result<()> {
41 if let Err(e) = writeln!(stream, "{content}") {
42 if e.kind() != io::ErrorKind::BrokenPipe {
44 return Err(e);
45 }
46 }
48 Ok(())
49 }
50
51 #[doc(hidden)]
53 pub fn new_with_writers(
54 format: OutputFormat,
55 stdout: Box<dyn Write>,
56 stderr: Box<dyn Write>,
57 ) -> Self {
58 Self {
59 format,
60 stdout,
61 stderr,
62 }
63 }
64
65 pub fn success<T>(&mut self, data: T) -> io::Result<ExitCode>
71 where
72 T: Serialize + Display,
73 {
74 match self.format {
75 OutputFormat::Json => {
76 let response = JsonResponse::success(&data);
77 let json_str = serde_json::to_string_pretty(&response)?;
78 Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
79 }
80 OutputFormat::Text => {
81 let text = format!("{data}");
82 Self::write_ignoring_broken_pipe(&mut *self.stdout, &text)?;
83 }
84 }
85 Ok(ExitCode::Success)
86 }
87
88 pub fn item<T>(&mut self, item: Option<T>, entity: &str, name: &str) -> io::Result<ExitCode>
93 where
94 T: Serialize + Display,
95 {
96 match item {
97 Some(data) => self.success(data),
98 None => self.not_found(entity, name),
99 }
100 }
101
102 pub fn not_found(&mut self, entity: &str, name: &str) -> io::Result<ExitCode> {
106 match self.format {
107 OutputFormat::Json => {
108 let response = JsonResponse::not_found(entity, name);
109 let json_str = serde_json::to_string_pretty(&response)?;
110 Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
111 }
112 OutputFormat::Text => {
113 let text = format!("{entity} '{name}' not found");
114 Self::write_ignoring_broken_pipe(&mut *self.stderr, &text)?;
115 }
116 }
117 Ok(ExitCode::NotFound)
118 }
119
120 pub fn collection<T, I>(&mut self, items: I, entity_name: &str) -> io::Result<ExitCode>
126 where
127 T: Serialize + Display,
128 I: IntoIterator<Item = T>,
129 {
130 let items: Vec<T> = items.into_iter().collect();
131
132 if items.is_empty() {
133 return self.not_found(entity_name, "any");
134 }
135
136 match self.format {
137 OutputFormat::Json => {
138 let response = JsonResponse::success(&items);
139 let json_str = serde_json::to_string_pretty(&response)?;
140 Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
141 }
142 OutputFormat::Text => {
143 let header = format!("Found {} {entity_name}:", items.len());
144 Self::write_ignoring_broken_pipe(&mut *self.stdout, &header)?;
145 Self::write_ignoring_broken_pipe(&mut *self.stdout, &"=".repeat(40))?;
146 for item in items {
147 let item_str = format!("{item}");
148 Self::write_ignoring_broken_pipe(&mut *self.stdout, &item_str)?;
149 }
150 }
151 }
152 Ok(ExitCode::Success)
153 }
154
155 pub fn error(&mut self, error: &IndexError) -> io::Result<ExitCode> {
159 match self.format {
160 OutputFormat::Json => {
161 let response = JsonResponse::from_error(error);
162 let json_str = serde_json::to_string_pretty(&response)?;
163 Self::write_ignoring_broken_pipe(&mut *self.stderr, &json_str)?;
164 }
165 OutputFormat::Text => {
166 let error_msg = format!("Error: {error}");
167 Self::write_ignoring_broken_pipe(&mut *self.stderr, &error_msg)?;
168 for suggestion in error.recovery_suggestions() {
169 let suggestion_msg = format!(" Suggestion: {suggestion}");
170 Self::write_ignoring_broken_pipe(&mut *self.stderr, &suggestion_msg)?;
171 }
172 }
173 }
174 Ok(ExitCode::from_error(error))
175 }
176
177 pub fn progress(&mut self, message: &str) -> io::Result<()> {
183 if matches!(self.format, OutputFormat::Text) {
184 Self::write_ignoring_broken_pipe(&mut *self.stderr, message)?;
185 }
186 Ok(())
187 }
188
189 pub fn info(&mut self, message: &str) -> io::Result<()> {
192 if matches!(self.format, OutputFormat::Text) {
193 Self::write_ignoring_broken_pipe(&mut *self.stdout, message)?;
194 }
195 Ok(())
196 }
197
198 pub fn symbol_contexts(
211 &mut self,
212 contexts: impl IntoIterator<Item = crate::symbol::context::SymbolContext>,
213 entity_name: &str,
214 ) -> io::Result<ExitCode> {
215 let contexts: Vec<_> = contexts.into_iter().collect();
216
217 if contexts.is_empty() {
218 return self.not_found(entity_name, "any");
219 }
220
221 match self.format {
222 OutputFormat::Json => {
223 let response = JsonResponse::success(&contexts);
224 let json_str = serde_json::to_string_pretty(&response)?;
225 Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
226 }
227 OutputFormat::Text => {
228 let header = format!("Found {} {}:", contexts.len(), entity_name);
229 Self::write_ignoring_broken_pipe(&mut *self.stdout, &header)?;
230 Self::write_ignoring_broken_pipe(&mut *self.stdout, &"=".repeat(40))?;
231
232 for context in contexts {
233 let formatted = format!("{context}");
234 Self::write_ignoring_broken_pipe(&mut *self.stdout, &formatted)?;
235 }
236 }
237 }
238 Ok(ExitCode::Success)
239 }
240
241 pub fn unified<T>(&mut self, output: UnifiedOutput<'_, T>) -> io::Result<ExitCode>
258 where
259 T: Serialize + Display,
260 {
261 let exit_code = output.exit_code;
262
263 match self.format {
264 OutputFormat::Json => {
265 let json_str = serde_json::to_string_pretty(&output)?;
268 Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
269 }
270 OutputFormat::Text => {
271 match (&output.data, &output.status) {
273 (OutputData::Empty, OutputStatus::NotFound) => {
275 let entity = format!("{:?}", output.entity_type);
276 let msg = format!("{} not found", entity.to_lowercase());
277 Self::write_ignoring_broken_pipe(&mut *self.stderr, &msg)?;
278 }
279 (OutputData::Items { items }, OutputStatus::NotFound) if items.is_empty() => {
280 let entity = format!("{:?}", output.entity_type);
281 let msg = format!("{} not found", entity.to_lowercase());
282 Self::write_ignoring_broken_pipe(&mut *self.stderr, &msg)?;
283 }
284 (OutputData::Empty, OutputStatus::Success) => {
286 }
288 (OutputData::Items { items }, OutputStatus::Success) if items.is_empty() => {
289 }
291 _ => {
292 let formatted = format!("{output}");
294 Self::write_ignoring_broken_pipe(&mut *self.stdout, &formatted)?;
295 }
296 }
297
298 if let Some(guidance) = &output.guidance {
300 Self::write_ignoring_broken_pipe(&mut *self.stderr, "")?;
301 Self::write_ignoring_broken_pipe(&mut *self.stderr, guidance)?;
302 }
303 }
304 }
305
306 Ok(exit_code)
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 struct BrokenPipeWriter;
316
317 impl Write for BrokenPipeWriter {
318 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
319 Err(io::Error::new(io::ErrorKind::BrokenPipe, "pipe broken"))
320 }
321
322 fn flush(&mut self) -> io::Result<()> {
323 Err(io::Error::new(io::ErrorKind::BrokenPipe, "pipe broken"))
324 }
325 }
326
327 #[test]
328 fn test_output_manager_text_success() {
329 let stdout = Vec::new();
330 let stderr = Vec::new();
331
332 let mut manager =
333 OutputManager::new_with_writers(OutputFormat::Text, Box::new(stdout), Box::new(stderr));
334
335 let code = manager.success("Test output").unwrap();
336 assert_eq!(code, ExitCode::Success);
337 }
338
339 #[test]
340 fn test_broken_pipe_returns_success_exit_code() {
341 let mut manager = OutputManager::new_with_writers(
342 OutputFormat::Json,
343 Box::new(BrokenPipeWriter),
344 Box::new(Vec::new()),
345 );
346
347 let code = manager.success("test data").unwrap();
349 assert_eq!(code, ExitCode::Success);
350 }
351
352 #[test]
353 fn test_broken_pipe_returns_not_found_exit_code() {
354 let mut manager = OutputManager::new_with_writers(
355 OutputFormat::Json,
356 Box::new(BrokenPipeWriter),
357 Box::new(Vec::new()),
358 );
359
360 let code = manager.not_found("Symbol", "missing").unwrap();
362 assert_eq!(code, ExitCode::NotFound);
363 }
364
365 #[test]
366 fn test_broken_pipe_in_text_mode() {
367 let mut manager = OutputManager::new_with_writers(
368 OutputFormat::Text,
369 Box::new(BrokenPipeWriter),
370 Box::new(Vec::new()),
371 );
372
373 let code = manager.success("test output").unwrap();
375 assert_eq!(code, ExitCode::Success);
376 }
377
378 #[test]
379 fn test_broken_pipe_stderr() {
380 let mut manager = OutputManager::new_with_writers(
381 OutputFormat::Text,
382 Box::new(Vec::new()),
383 Box::new(BrokenPipeWriter),
384 );
385
386 let result = manager.progress("Processing...");
388 assert!(result.is_ok());
389
390 let code = manager.not_found("Entity", "name").unwrap();
392 assert_eq!(code, ExitCode::NotFound);
393 }
394
395 #[test]
396 fn test_output_manager_json_success() {
397 let stdout = Vec::new();
398 let stderr = Vec::new();
399
400 let mut manager =
401 OutputManager::new_with_writers(OutputFormat::Json, Box::new(stdout), Box::new(stderr));
402
403 #[derive(Serialize)]
404 struct TestData {
405 value: i32,
406 }
407
408 impl Display for TestData {
409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 write!(f, "TestData({})", self.value)
411 }
412 }
413
414 let data = TestData { value: 42 };
415 let code = manager.success(data).unwrap();
416 assert_eq!(code, ExitCode::Success);
417 }
418
419 #[test]
420 fn test_symbol_contexts_collection() {
421 use crate::symbol::Symbol;
422 use crate::symbol::context::{SymbolContext, SymbolRelationships};
423 use crate::types::{FileId, Range, SymbolId, SymbolKind};
424
425 fn create_context(id: u32, name: &str) -> SymbolContext {
427 let symbol = Symbol::new(
428 SymbolId::new(id).unwrap(),
429 name,
430 SymbolKind::Function,
431 FileId::new(1).unwrap(),
432 Range::new(10, 0, 20, 0),
433 );
434
435 SymbolContext {
436 symbol,
437 file_path: format!("src/{name}.rs:11"),
438 relationships: SymbolRelationships::default(),
439 }
440 }
441
442 let stdout = Vec::new();
444 let stderr = Vec::new();
445 let mut manager =
446 OutputManager::new_with_writers(OutputFormat::Json, Box::new(stdout), Box::new(stderr));
447
448 let contexts = vec![create_context(1, "main"), create_context(2, "process")];
449
450 let code = manager.symbol_contexts(contexts, "functions").unwrap();
451 assert_eq!(code, ExitCode::Success);
452 }
453
454 #[test]
455 fn test_symbol_contexts_empty() {
456 use crate::symbol::context::SymbolContext;
457
458 let stdout = Vec::new();
459 let stderr = Vec::new();
460 let mut manager =
461 OutputManager::new_with_writers(OutputFormat::Json, Box::new(stdout), Box::new(stderr));
462
463 let contexts: Vec<SymbolContext> = vec![];
464 let code = manager.symbol_contexts(contexts, "symbols").unwrap();
465 assert_eq!(code, ExitCode::NotFound);
466 }
467
468 #[test]
469 fn test_symbol_contexts_text_format() {
470 use crate::symbol::Symbol;
471 use crate::symbol::context::{SymbolContext, SymbolRelationships};
472 use crate::types::{FileId, Range, SymbolId, SymbolKind};
473
474 let symbol = Symbol::new(
475 SymbolId::new(1).unwrap(),
476 "test_function",
477 SymbolKind::Function,
478 FileId::new(1).unwrap(),
479 Range::new(42, 0, 50, 0),
480 );
481
482 let context = SymbolContext {
483 symbol,
484 file_path: "src/test.rs:43".to_string(),
485 relationships: SymbolRelationships::default(),
486 };
487
488 let stdout = Vec::new();
489 let stderr = Vec::new();
490 let mut manager =
491 OutputManager::new_with_writers(OutputFormat::Text, Box::new(stdout), Box::new(stderr));
492
493 let code = manager.symbol_contexts(vec![context], "function").unwrap();
494 assert_eq!(code, ExitCode::Success);
495
496 }
501
502 #[test]
503 fn test_symbol_contexts_broken_pipe() {
504 use crate::symbol::Symbol;
505 use crate::symbol::context::{SymbolContext, SymbolRelationships};
506 use crate::types::{FileId, Range, SymbolId, SymbolKind};
507
508 let symbol = Symbol::new(
509 SymbolId::new(1).unwrap(),
510 "test",
511 SymbolKind::Function,
512 FileId::new(1).unwrap(),
513 Range::new(1, 0, 2, 0),
514 );
515
516 let context = SymbolContext {
517 symbol,
518 file_path: "test.rs:1".to_string(),
519 relationships: SymbolRelationships::default(),
520 };
521
522 let mut manager = OutputManager::new_with_writers(
524 OutputFormat::Json,
525 Box::new(BrokenPipeWriter),
526 Box::new(Vec::new()),
527 );
528
529 let code = manager
531 .symbol_contexts(vec![context.clone()], "symbols")
532 .unwrap();
533 assert_eq!(code, ExitCode::Success);
534
535 let code = manager
537 .symbol_contexts(Vec::<SymbolContext>::new(), "symbols")
538 .unwrap();
539 assert_eq!(code, ExitCode::NotFound);
540 }
541}