gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! Output formatting utilities for different formats.
//!
//! Supports JSON, JSONL, CSV, Table, and Markdown output.

// These formatters are available for future use in custom output scenarios
#[allow(dead_code)]
mod csv;
#[allow(dead_code)]
mod json;
#[allow(dead_code)]
mod markdown;
#[allow(dead_code)]
mod table;

#[allow(unused_imports)]
pub use self::csv::CsvOutput;
#[allow(unused_imports)]
pub use self::json::JsonOutput;
#[allow(unused_imports)]
pub use self::markdown::MarkdownOutput;
#[allow(unused_imports)]
pub use self::table::TableOutput;

use crate::cli::args::OutputFormat;
use crate::error::Result;
use serde::Serialize;

/// Trait for types that can be rendered in multiple output formats
#[allow(dead_code)]
pub trait Render {
    /// Render the data according to the specified format
    fn render(&self, format: OutputFormat, pretty: bool) -> Result<String>;
}

/// Generic output helper
#[allow(dead_code)]
pub struct Output {
    format: OutputFormat,
    pretty: bool,
}

#[allow(dead_code)]
impl Output {
    pub fn new(format: OutputFormat, pretty: bool) -> Self {
        Self { format, pretty }
    }

    /// Auto-detect format based on TTY
    pub fn auto_detect(pretty: bool) -> Self {
        let format = if atty::is(atty::Stream::Stdout) {
            OutputFormat::Table
        } else {
            OutputFormat::Json
        };
        Self { format, pretty }
    }

    /// Get effective format, resolving Auto
    pub fn effective_format(&self) -> OutputFormat {
        match self.format {
            OutputFormat::Auto => {
                if atty::is(atty::Stream::Stdout) {
                    OutputFormat::Table
                } else {
                    OutputFormat::Json
                }
            }
            other => other,
        }
    }

    /// Print data in the configured format
    pub fn print<T: Serialize + Tabular>(&self, data: &T) -> Result<()> {
        match self.effective_format() {
            OutputFormat::Json => {
                JsonOutput::new(self.pretty).print(data)?;
            }
            OutputFormat::Jsonl => {
                JsonOutput::new(false).print_jsonl(data)?;
            }
            OutputFormat::Csv => {
                CsvOutput::new().print(data)?;
            }
            OutputFormat::Table => {
                TableOutput::new().print(data)?;
            }
            OutputFormat::Markdown => {
                MarkdownOutput::new().print(data)?;
            }
            OutputFormat::Auto => unreachable!(),
        }
        Ok(())
    }

    /// Print a list of items, one per line for JSONL
    pub fn print_list<T: Serialize + Tabular>(&self, items: &[T]) -> Result<()> {
        match self.effective_format() {
            OutputFormat::Jsonl => {
                for item in items {
                    JsonOutput::new(false).print(item)?;
                }
            }
            OutputFormat::Json => {
                JsonOutput::new(self.pretty).print(items)?;
            }
            OutputFormat::Csv => {
                CsvOutput::new().print_rows(items)?;
            }
            OutputFormat::Table => {
                TableOutput::new().print_rows(items)?;
            }
            OutputFormat::Markdown => {
                MarkdownOutput::new().print_rows(items)?;
            }
            OutputFormat::Auto => {
                if atty::is(atty::Stream::Stdout) {
                    TableOutput::new().print_rows(items)?;
                } else {
                    JsonOutput::new(false).print(items)?;
                }
            }
        }
        Ok(())
    }
}

/// Trait for types that can be rendered as a table
#[allow(dead_code)]
pub trait Tabular {
    /// Get column headers
    fn headers() -> Vec<String>;

    /// Get row values as strings
    fn row(&self) -> Vec<String>;
}

/// Implement Tabular for Vec<T> where T: Tabular
impl<T: Tabular> Tabular for Vec<T> {
    fn headers() -> Vec<String> {
        T::headers()
    }

    fn row(&self) -> Vec<String> {
        // For a Vec, we don't have a single row
        // This is handled specially in the output methods
        vec![]
    }
}

use crate::cli::args::GlobalArgs;

/// Output writer that uses GlobalArgs for configuration
pub struct OutputWriter<'a> {
    global: &'a GlobalArgs,
}

impl<'a> OutputWriter<'a> {
    /// Create a new output writer from global args
    pub fn new(global: &'a GlobalArgs) -> Self {
        Self { global }
    }

    /// Get the effective output format
    pub fn format(&self) -> OutputFormat {
        self.global.effective_output_format()
    }

    /// Check if pretty printing is enabled
    pub fn pretty(&self) -> bool {
        self.global.should_pretty_print()
    }

    /// Write a serializable value to stdout
    pub fn write_value<T: Serialize>(&self, value: &T) -> Result<()> {
        let format = self.format();
        let pretty = self.pretty();

        match format {
            OutputFormat::Auto => {
                if atty::is(atty::Stream::Stdout) {
                    // For TTY, try to format as table if possible, else pretty JSON
                    let json = if pretty {
                        serde_json::to_string_pretty(value)?
                    } else {
                        serde_json::to_string(value)?
                    };
                    println!("{}", json);
                } else {
                    let json = serde_json::to_string(value)?;
                    println!("{}", json);
                }
            }
            OutputFormat::Json => {
                let json = if pretty {
                    serde_json::to_string_pretty(value)?
                } else {
                    serde_json::to_string(value)?
                };
                println!("{}", json);
            }
            OutputFormat::Jsonl => {
                // For JSONL, each array element gets its own line
                let json_value = serde_json::to_value(value)?;
                if let serde_json::Value::Array(arr) = json_value {
                    for item in arr {
                        println!("{}", serde_json::to_string(&item)?);
                    }
                } else {
                    println!("{}", serde_json::to_string(&json_value)?);
                }
            }
            OutputFormat::Csv => {
                let json_value = serde_json::to_value(value)?;
                self.write_csv(&json_value)?;
            }
            OutputFormat::Table => {
                let json = serde_json::to_string_pretty(value)?;
                println!("{}", json);
            }
            OutputFormat::Markdown => {
                let json = serde_json::to_string_pretty(value)?;
                println!("```json\n{}\n```", json);
            }
        }

        Ok(())
    }

    /// Write JSON value as CSV
    fn write_csv(&self, value: &serde_json::Value) -> Result<()> {
        #[allow(unused_imports)]
        use std::io::Write;

        let mut wtr = ::csv::Writer::from_writer(std::io::stdout());

        match value {
            serde_json::Value::Array(arr) => {
                if let Some(first) = arr.first() {
                    if let serde_json::Value::Object(obj) = first {
                        // Write headers
                        let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
                        wtr.write_record(&headers)?;

                        // Write rows
                        for item in arr {
                            if let serde_json::Value::Object(row) = item {
                                let values: Vec<String> = headers.iter()
                                    .map(|h| {
                                        row.get(*h)
                                            .map(|v| match v {
                                                serde_json::Value::String(s) => s.clone(),
                                                serde_json::Value::Null => String::new(),
                                                other => other.to_string(),
                                            })
                                            .unwrap_or_default()
                                    })
                                    .collect();
                                wtr.write_record(&values)?;
                            }
                        }
                    }
                }
            }
            serde_json::Value::Object(obj) => {
                // Single object - write as key,value pairs
                wtr.write_record(&["key", "value"])?;
                for (k, v) in obj {
                    let value_str = match v {
                        serde_json::Value::String(s) => s.clone(),
                        serde_json::Value::Null => String::new(),
                        other => other.to_string(),
                    };
                    wtr.write_record(&[k.as_str(), &value_str])?;
                }
            }
            _ => {
                println!("{}", value);
            }
        }

        wtr.flush()?;
        Ok(())
    }
}