text-grid 0.4.1

A library to create formatted plain-text tables.
Documentation
use std::{borrow::Borrow, io::Write};

use csv::{StringRecord, Writer};

use crate::{CellsFormatter, CellsSchema, CellsWrite, RawCell};

pub fn write_csv<T>(
    csv_writer: &mut Writer<impl Write>,
    source: impl IntoIterator<Item = impl Borrow<T>>,
    schema: impl CellsSchema<Source = T>,
    separator: &str,
) -> csv::Result<()> {
    let source = source.into_iter();
    let mut w = CsvHeaderWriter::new(separator);
    schema.fmt(&mut CellsFormatter::new(&mut w, None));
    csv_writer.write_record(&w.record)?;

    let mut w = CsvBodyWriter::new();
    for item in source {
        schema.fmt(&mut CellsFormatter::new(&mut w, Some(item.borrow())));
        csv_writer.write_record(&w.record)?;
        w.record.clear();
    }
    Ok(())
}

struct CsvHeaderWriter<'a> {
    pub record: StringRecord,
    value: String,
    lens: Vec<usize>,
    has_content: bool,
    separator: &'a str,
}
impl<'a> CsvHeaderWriter<'a> {
    fn new(separator: &'a str) -> Self {
        Self {
            record: StringRecord::new(),
            value: String::new(),
            lens: Vec::new(),
            has_content: false,
            separator,
        }
    }
}

impl<'a> CellsWrite for CsvHeaderWriter<'a> {
    fn content(&mut self, _cell: Option<&dyn RawCell>, _stretch: bool) {
        self.has_content = true;
    }

    fn content_start(&mut self, _cell: &dyn RawCell) {}
    fn content_end(&mut self, _cell: &dyn RawCell) {}

    fn column_start(&mut self, header: &dyn RawCell) {
        self.lens.push(self.value.len());
        if !self.value.is_empty() {
            self.value.push_str(self.separator);
        }
        header.fmt(&mut self.value);
    }

    fn column_end(&mut self, _header: &dyn RawCell) {
        if self.has_content {
            self.record.push_field(&self.value);
            self.has_content = false;
        }
        self.value.truncate(self.lens.pop().unwrap());
    }
}

struct CsvBodyWriter {
    pub record: StringRecord,
    value: String,
    is_merged: bool,
    has_content: bool,
}

impl CsvBodyWriter {
    fn new() -> Self {
        Self {
            record: StringRecord::new(),
            value: String::new(),
            is_merged: false,
            has_content: false,
        }
    }
}

impl CellsWrite for CsvBodyWriter {
    fn content(&mut self, cell: Option<&dyn RawCell>, _stretch: bool) {
        if let Some(cell) = cell {
            cell.fmt(&mut self.value);
        }
        self.has_content = true;
    }

    fn content_start(&mut self, cell: &dyn RawCell) {
        self.is_merged = true;
        cell.fmt(&mut self.value);
    }

    fn content_end(&mut self, _cell: &dyn RawCell) {
        self.is_merged = false;
    }

    fn column_start(&mut self, _header: &dyn RawCell) {}

    fn column_end(&mut self, _header: &dyn RawCell) {
        if self.has_content {
            self.record.push_field(&self.value);
            self.value.clear();
            self.has_content = false;
        }
    }
}