wordpress_audit/
output.rs

1//! Output formatting for WordPress scan results
2
3use crate::analyze::{Analysis, ComponentAnalysis, ComponentStatus, ComponentType};
4use crate::error::{Error, Result};
5use comfy_table::{
6    Attribute, Cell, CellAlignment, Color, ContentArrangement, Table, presets::UTF8_FULL,
7};
8use std::io::Write;
9use std::str::FromStr;
10
11/// Output format for results
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum OutputFormat {
14    /// Human-readable table output
15    #[default]
16    Human,
17    /// JSON output
18    Json,
19    /// No output (silent mode)
20    None,
21}
22
23impl FromStr for OutputFormat {
24    type Err = Error;
25
26    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
27        match s.to_lowercase().as_str() {
28            "human" => Ok(Self::Human),
29            "json" => Ok(Self::Json),
30            "none" => Ok(Self::None),
31            _ => Err(Error::InvalidOutputFormat(s.to_string())),
32        }
33    }
34}
35
36/// Sort order for output
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum OutputSort {
39    /// Sort by type (Core, Theme, Plugin), then by name (default)
40    #[default]
41    Type,
42    /// Sort alphabetically by name only
43    Name,
44    /// Sort by status, then by type, then by name
45    Status,
46}
47
48impl FromStr for OutputSort {
49    type Err = Error;
50
51    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
52        match s.to_lowercase().as_str() {
53            "type" => Ok(Self::Type),
54            "name" => Ok(Self::Name),
55            "status" => Ok(Self::Status),
56            _ => Err(Error::InvalidOutputSort(s.to_string())),
57        }
58    }
59}
60
61/// Configuration for output formatting
62#[derive(Debug, Clone, Default)]
63pub struct OutputConfig {
64    /// Output format
65    pub format: OutputFormat,
66    /// Sort order
67    pub sort: OutputSort,
68}
69
70impl OutputConfig {
71    /// Create a new output config
72    pub fn new(format: OutputFormat, sort: OutputSort) -> Self {
73        Self { format, sort }
74    }
75}
76
77/// Output the analysis results
78pub fn output_analysis<W: Write>(
79    analysis: &Analysis,
80    config: &OutputConfig,
81    writer: &mut W,
82) -> Result<()> {
83    match config.format {
84        OutputFormat::Human => output_human(analysis, config, writer),
85        OutputFormat::Json => output_json(analysis, writer),
86        OutputFormat::None => Ok(()),
87    }
88}
89
90/// Output JSON format
91fn output_json<W: Write>(analysis: &Analysis, writer: &mut W) -> Result<()> {
92    serde_json::to_writer_pretty(&mut *writer, analysis)?;
93    writeln!(writer).map_err(Error::OutputFailed)?;
94    Ok(())
95}
96
97/// Output human-readable table format
98fn output_human<W: Write>(
99    analysis: &Analysis,
100    config: &OutputConfig,
101    writer: &mut W,
102) -> Result<()> {
103    let mut table = Table::new();
104    table
105        .load_preset(UTF8_FULL)
106        .set_content_arrangement(ContentArrangement::Dynamic)
107        .set_header(vec![
108            Cell::new("Type").add_attribute(Attribute::Bold),
109            Cell::new("Name").add_attribute(Attribute::Bold),
110            Cell::new("Version").add_attribute(Attribute::Bold),
111            Cell::new("Latest").add_attribute(Attribute::Bold),
112            Cell::new("Status").add_attribute(Attribute::Bold),
113        ]);
114
115    // Placeholder for when no plugins detected
116    let no_plugins = ComponentAnalysis {
117        component_type: ComponentType::Plugin,
118        name: "-".to_string(),
119        version: "-".to_string(),
120        latest_version: "-".to_string(),
121        status: ComponentStatus::NotDetected,
122    };
123
124    // Collect all components
125    let mut components: Vec<&ComponentAnalysis> = Vec::new();
126    components.push(&analysis.wordpress);
127    components.push(&analysis.theme);
128    if analysis.plugins.is_empty() {
129        components.push(&no_plugins);
130    } else {
131        for component in analysis.plugins.values() {
132            components.push(component);
133        }
134    }
135
136    // Helper to get sort priority by type (Core=0, Theme=1, Plugin=2)
137    let type_order = |t: ComponentType| -> u8 {
138        match t {
139            ComponentType::Core => 0,
140            ComponentType::Theme => 1,
141            ComponentType::Plugin => 2,
142        }
143    };
144
145    // Sort based on config
146    match config.sort {
147        // Default: by type (Core, Theme, Plugin), then by name
148        OutputSort::Type => {
149            components.sort_by(|a, b| {
150                type_order(a.component_type)
151                    .cmp(&type_order(b.component_type))
152                    .then_with(|| a.name.cmp(&b.name))
153            });
154        }
155        // By name only (alphabetically)
156        OutputSort::Name => {
157            components.sort_by(|a, b| a.name.cmp(&b.name));
158        }
159        // By status first, then type, then name
160        OutputSort::Status => {
161            components.sort_by(|a, b| {
162                b.status
163                    .cmp(&a.status)
164                    .then_with(|| type_order(a.component_type).cmp(&type_order(b.component_type)))
165                    .then_with(|| a.name.cmp(&b.name))
166            });
167        }
168    }
169
170    // Add rows
171    for component in components {
172        add_component_row(&mut table, component);
173    }
174
175    writeln!(writer, "{}", table).map_err(Error::OutputFailed)
176}
177
178/// Add a row for a component to the table
179fn add_component_row(table: &mut Table, component: &ComponentAnalysis) {
180    let status_cell = match component.status {
181        ComponentStatus::Ok => Cell::new("Ok")
182            .fg(Color::Green)
183            .set_alignment(CellAlignment::Center),
184        ComponentStatus::Outdated => Cell::new("Outdated")
185            .fg(Color::Yellow)
186            .set_alignment(CellAlignment::Center),
187        ComponentStatus::Unknown => Cell::new("Unknown")
188            .fg(Color::DarkGrey)
189            .set_alignment(CellAlignment::Center),
190        ComponentStatus::NotDetected => Cell::new("Not Found")
191            .fg(Color::DarkGrey)
192            .set_alignment(CellAlignment::Center),
193    };
194
195    table.add_row(vec![
196        Cell::new(component.component_type.to_string()),
197        Cell::new(&component.name),
198        Cell::new(&component.version),
199        Cell::new(&component.latest_version),
200        status_cell,
201    ]);
202}