diffo 0.2.0

Semantic diffing for Rust structs via serde
Documentation
use super::Formatter;
use crate::{Change, Diff, FormatError};

/// Markdown table formatter for PR-friendly output.
#[derive(Debug)]
pub struct MarkdownFormatter {
    max_value_length: usize,
}

impl MarkdownFormatter {
    /// Create a new Markdown formatter.
    pub fn new() -> Self {
        Self {
            max_value_length: 100,
        }
    }

    /// Set the maximum value length to display.
    pub fn max_value_length(mut self, length: usize) -> Self {
        self.max_value_length = length;
        self
    }

    fn truncate(&self, s: String) -> String {
        if s.len() > self.max_value_length {
            format!("{}...", &s[..self.max_value_length])
        } else {
            s
        }
    }
}

impl Default for MarkdownFormatter {
    fn default() -> Self {
        Self::new()
    }
}

impl Formatter for MarkdownFormatter {
    fn format(&self, diff: &Diff) -> Result<String, FormatError> {
        if diff.is_empty() {
            return Ok(String::from("No changes"));
        }

        let mut output = String::new();

        // Header
        output.push_str("| Path | Change | Old Value | New Value |\n");
        output.push_str("|------|--------|-----------|-----------|");
        output.push('\n');

        // Rows
        for (path, change) in diff.changes() {
            match change {
                Change::Added(val) => {
                    let val_str = self.truncate(format!("{:?}", val));
                    output.push_str(&format!("| {} | Added | - | {} |\n", path, val_str));
                }
                Change::Removed(val) => {
                    let val_str = self.truncate(format!("{:?}", val));
                    output.push_str(&format!("| {} | Removed | {} | - |\n", path, val_str));
                }
                Change::Modified { from, to } => {
                    let from_str = self.truncate(format!("{:?}", from));
                    let to_str = self.truncate(format!("{:?}", to));
                    output.push_str(&format!(
                        "| {} | Modified | {} | {} |\n",
                        path, from_str, to_str
                    ));
                }
                Change::Elided { reason, count } => {
                    output.push_str(&format!(
                        "| {} | Elided | {} ({} items) | - |\n",
                        path, reason, count
                    ));
                }
            }
        }

        Ok(output)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Path;
    use serde_value::Value;

    #[test]
    fn test_markdown_format() {
        let mut diff = Diff::new();
        diff.insert(Path::root().field("x"), Change::Added(Value::I64(42)));

        let formatter = MarkdownFormatter::new();
        let output = formatter.format(&diff).unwrap();
        assert!(output.contains("| Path | Change |"));
        assert!(output.contains("| x | Added |"));
    }
}