use crate::error::IndexError;
use crate::io::exit_code::ExitCode;
use crate::io::format::{JsonResponse, OutputFormat};
use crate::io::schema::{OutputData, OutputStatus, UnifiedOutput};
use serde::Serialize;
use std::fmt::Display;
use std::io::{self, Write};
pub struct OutputManager {
format: OutputFormat,
stdout: Box<dyn Write>,
stderr: Box<dyn Write>,
}
impl OutputManager {
pub fn new(format: OutputFormat) -> Self {
Self {
format,
stdout: Box::new(io::stdout()),
stderr: Box::new(io::stderr()),
}
}
fn write_ignoring_broken_pipe(stream: &mut dyn Write, content: &str) -> io::Result<()> {
if let Err(e) = writeln!(stream, "{content}") {
if e.kind() != io::ErrorKind::BrokenPipe {
return Err(e);
}
}
Ok(())
}
#[doc(hidden)]
pub fn new_with_writers(
format: OutputFormat,
stdout: Box<dyn Write>,
stderr: Box<dyn Write>,
) -> Self {
Self {
format,
stdout,
stderr,
}
}
pub fn success<T>(&mut self, data: T) -> io::Result<ExitCode>
where
T: Serialize + Display,
{
match self.format {
OutputFormat::Json => {
let response = JsonResponse::success(&data);
let json_str = serde_json::to_string_pretty(&response)?;
Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
}
OutputFormat::Text => {
let text = format!("{data}");
Self::write_ignoring_broken_pipe(&mut *self.stdout, &text)?;
}
}
Ok(ExitCode::Success)
}
pub fn item<T>(&mut self, item: Option<T>, entity: &str, name: &str) -> io::Result<ExitCode>
where
T: Serialize + Display,
{
match item {
Some(data) => self.success(data),
None => self.not_found(entity, name),
}
}
pub fn not_found(&mut self, entity: &str, name: &str) -> io::Result<ExitCode> {
match self.format {
OutputFormat::Json => {
let response = JsonResponse::not_found(entity, name);
let json_str = serde_json::to_string_pretty(&response)?;
Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
}
OutputFormat::Text => {
let text = format!("{entity} '{name}' not found");
Self::write_ignoring_broken_pipe(&mut *self.stderr, &text)?;
}
}
Ok(ExitCode::NotFound)
}
pub fn collection<T, I>(&mut self, items: I, entity_name: &str) -> io::Result<ExitCode>
where
T: Serialize + Display,
I: IntoIterator<Item = T>,
{
let items: Vec<T> = items.into_iter().collect();
if items.is_empty() {
return self.not_found(entity_name, "any");
}
match self.format {
OutputFormat::Json => {
let response = JsonResponse::success(&items);
let json_str = serde_json::to_string_pretty(&response)?;
Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
}
OutputFormat::Text => {
let header = format!("Found {} {entity_name}:", items.len());
Self::write_ignoring_broken_pipe(&mut *self.stdout, &header)?;
Self::write_ignoring_broken_pipe(&mut *self.stdout, &"=".repeat(40))?;
for item in items {
let item_str = format!("{item}");
Self::write_ignoring_broken_pipe(&mut *self.stdout, &item_str)?;
}
}
}
Ok(ExitCode::Success)
}
pub fn error(&mut self, error: &IndexError) -> io::Result<ExitCode> {
match self.format {
OutputFormat::Json => {
let response = JsonResponse::from_error(error);
let json_str = serde_json::to_string_pretty(&response)?;
Self::write_ignoring_broken_pipe(&mut *self.stderr, &json_str)?;
}
OutputFormat::Text => {
let error_msg = format!("Error: {error}");
Self::write_ignoring_broken_pipe(&mut *self.stderr, &error_msg)?;
for suggestion in error.recovery_suggestions() {
let suggestion_msg = format!(" Suggestion: {suggestion}");
Self::write_ignoring_broken_pipe(&mut *self.stderr, &suggestion_msg)?;
}
}
}
Ok(ExitCode::from_error(error))
}
pub fn progress(&mut self, message: &str) -> io::Result<()> {
if matches!(self.format, OutputFormat::Text) {
Self::write_ignoring_broken_pipe(&mut *self.stderr, message)?;
}
Ok(())
}
pub fn info(&mut self, message: &str) -> io::Result<()> {
if matches!(self.format, OutputFormat::Text) {
Self::write_ignoring_broken_pipe(&mut *self.stdout, message)?;
}
Ok(())
}
pub fn symbol_contexts(
&mut self,
contexts: impl IntoIterator<Item = crate::symbol::context::SymbolContext>,
entity_name: &str,
) -> io::Result<ExitCode> {
let contexts: Vec<_> = contexts.into_iter().collect();
if contexts.is_empty() {
return self.not_found(entity_name, "any");
}
match self.format {
OutputFormat::Json => {
let response = JsonResponse::success(&contexts);
let json_str = serde_json::to_string_pretty(&response)?;
Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
}
OutputFormat::Text => {
let header = format!("Found {} {}:", contexts.len(), entity_name);
Self::write_ignoring_broken_pipe(&mut *self.stdout, &header)?;
Self::write_ignoring_broken_pipe(&mut *self.stdout, &"=".repeat(40))?;
for context in contexts {
let formatted = format!("{context}");
Self::write_ignoring_broken_pipe(&mut *self.stdout, &formatted)?;
}
}
}
Ok(ExitCode::Success)
}
pub fn unified<T>(&mut self, output: UnifiedOutput<'_, T>) -> io::Result<ExitCode>
where
T: Serialize + Display,
{
let exit_code = output.exit_code;
match self.format {
OutputFormat::Json => {
let json_str = serde_json::to_string_pretty(&output)?;
Self::write_ignoring_broken_pipe(&mut *self.stdout, &json_str)?;
}
OutputFormat::Text => {
match (&output.data, &output.status) {
(OutputData::Empty, OutputStatus::NotFound) => {
let entity = format!("{:?}", output.entity_type);
let msg = format!("{} not found", entity.to_lowercase());
Self::write_ignoring_broken_pipe(&mut *self.stderr, &msg)?;
}
(OutputData::Items { items }, OutputStatus::NotFound) if items.is_empty() => {
let entity = format!("{:?}", output.entity_type);
let msg = format!("{} not found", entity.to_lowercase());
Self::write_ignoring_broken_pipe(&mut *self.stderr, &msg)?;
}
(OutputData::Empty, OutputStatus::Success) => {
}
(OutputData::Items { items }, OutputStatus::Success) if items.is_empty() => {
}
_ => {
let formatted = format!("{output}");
Self::write_ignoring_broken_pipe(&mut *self.stdout, &formatted)?;
}
}
if let Some(guidance) = &output.guidance {
Self::write_ignoring_broken_pipe(&mut *self.stderr, "")?;
Self::write_ignoring_broken_pipe(&mut *self.stderr, guidance)?;
}
}
}
Ok(exit_code)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct BrokenPipeWriter;
impl Write for BrokenPipeWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "pipe broken"))
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "pipe broken"))
}
}
#[test]
fn test_output_manager_text_success() {
let stdout = Vec::new();
let stderr = Vec::new();
let mut manager =
OutputManager::new_with_writers(OutputFormat::Text, Box::new(stdout), Box::new(stderr));
let code = manager.success("Test output").unwrap();
assert_eq!(code, ExitCode::Success);
}
#[test]
fn test_broken_pipe_returns_success_exit_code() {
let mut manager = OutputManager::new_with_writers(
OutputFormat::Json,
Box::new(BrokenPipeWriter),
Box::new(Vec::new()),
);
let code = manager.success("test data").unwrap();
assert_eq!(code, ExitCode::Success);
}
#[test]
fn test_broken_pipe_returns_not_found_exit_code() {
let mut manager = OutputManager::new_with_writers(
OutputFormat::Json,
Box::new(BrokenPipeWriter),
Box::new(Vec::new()),
);
let code = manager.not_found("Symbol", "missing").unwrap();
assert_eq!(code, ExitCode::NotFound);
}
#[test]
fn test_broken_pipe_in_text_mode() {
let mut manager = OutputManager::new_with_writers(
OutputFormat::Text,
Box::new(BrokenPipeWriter),
Box::new(Vec::new()),
);
let code = manager.success("test output").unwrap();
assert_eq!(code, ExitCode::Success);
}
#[test]
fn test_broken_pipe_stderr() {
let mut manager = OutputManager::new_with_writers(
OutputFormat::Text,
Box::new(Vec::new()),
Box::new(BrokenPipeWriter),
);
let result = manager.progress("Processing...");
assert!(result.is_ok());
let code = manager.not_found("Entity", "name").unwrap();
assert_eq!(code, ExitCode::NotFound);
}
#[test]
fn test_output_manager_json_success() {
let stdout = Vec::new();
let stderr = Vec::new();
let mut manager =
OutputManager::new_with_writers(OutputFormat::Json, Box::new(stdout), Box::new(stderr));
#[derive(Serialize)]
struct TestData {
value: i32,
}
impl Display for TestData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TestData({})", self.value)
}
}
let data = TestData { value: 42 };
let code = manager.success(data).unwrap();
assert_eq!(code, ExitCode::Success);
}
#[test]
fn test_symbol_contexts_collection() {
use crate::symbol::Symbol;
use crate::symbol::context::{SymbolContext, SymbolRelationships};
use crate::types::{FileId, Range, SymbolId, SymbolKind};
fn create_context(id: u32, name: &str) -> SymbolContext {
let symbol = Symbol::new(
SymbolId::new(id).unwrap(),
name,
SymbolKind::Function,
FileId::new(1).unwrap(),
Range::new(10, 0, 20, 0),
);
SymbolContext {
symbol,
file_path: format!("src/{name}.rs:11"),
relationships: SymbolRelationships::default(),
}
}
let stdout = Vec::new();
let stderr = Vec::new();
let mut manager =
OutputManager::new_with_writers(OutputFormat::Json, Box::new(stdout), Box::new(stderr));
let contexts = vec![create_context(1, "main"), create_context(2, "process")];
let code = manager.symbol_contexts(contexts, "functions").unwrap();
assert_eq!(code, ExitCode::Success);
}
#[test]
fn test_symbol_contexts_empty() {
use crate::symbol::context::SymbolContext;
let stdout = Vec::new();
let stderr = Vec::new();
let mut manager =
OutputManager::new_with_writers(OutputFormat::Json, Box::new(stdout), Box::new(stderr));
let contexts: Vec<SymbolContext> = vec![];
let code = manager.symbol_contexts(contexts, "symbols").unwrap();
assert_eq!(code, ExitCode::NotFound);
}
#[test]
fn test_symbol_contexts_text_format() {
use crate::symbol::Symbol;
use crate::symbol::context::{SymbolContext, SymbolRelationships};
use crate::types::{FileId, Range, SymbolId, SymbolKind};
let symbol = Symbol::new(
SymbolId::new(1).unwrap(),
"test_function",
SymbolKind::Function,
FileId::new(1).unwrap(),
Range::new(42, 0, 50, 0),
);
let context = SymbolContext {
symbol,
file_path: "src/test.rs:43".to_string(),
relationships: SymbolRelationships::default(),
};
let stdout = Vec::new();
let stderr = Vec::new();
let mut manager =
OutputManager::new_with_writers(OutputFormat::Text, Box::new(stdout), Box::new(stderr));
let code = manager.symbol_contexts(vec![context], "function").unwrap();
assert_eq!(code, ExitCode::Success);
}
#[test]
fn test_symbol_contexts_broken_pipe() {
use crate::symbol::Symbol;
use crate::symbol::context::{SymbolContext, SymbolRelationships};
use crate::types::{FileId, Range, SymbolId, SymbolKind};
let symbol = Symbol::new(
SymbolId::new(1).unwrap(),
"test",
SymbolKind::Function,
FileId::new(1).unwrap(),
Range::new(1, 0, 2, 0),
);
let context = SymbolContext {
symbol,
file_path: "test.rs:1".to_string(),
relationships: SymbolRelationships::default(),
};
let mut manager = OutputManager::new_with_writers(
OutputFormat::Json,
Box::new(BrokenPipeWriter),
Box::new(Vec::new()),
);
let code = manager
.symbol_contexts(vec![context.clone()], "symbols")
.unwrap();
assert_eq!(code, ExitCode::Success);
let code = manager
.symbol_contexts(Vec::<SymbolContext>::new(), "symbols")
.unwrap();
assert_eq!(code, ExitCode::NotFound);
}
}