univert 0.4.0

Universal file converter (library and CLI)
Documentation
use std::fs;
use std::io::Write;
use std::path::Path;

use crate::{Error, Job, Result};

pub fn csv_to_json(job: &Job) -> Result<()> {
  if job.options.split {
    return split_rows(job, b',');
  }
  sv_to_json(job, b',')
}

pub fn tsv_to_json(job: &Job) -> Result<()> {
  if job.options.split {
    return split_rows(job, b'\t');
  }
  sv_to_json(job, b'\t')
}

pub fn csv_to_tsv(job: &Job) -> Result<()> {
  sv_to_sv(job.input, job.output, b',', b'\t')
}

pub fn tsv_to_csv(job: &Job) -> Result<()> {
  sv_to_sv(job.input, job.output, b'\t', b',')
}

pub fn csv_to_html(job: &Job) -> Result<()> {
  sv_to_html(job, b',')
}

pub fn tsv_to_html(job: &Job) -> Result<()> {
  sv_to_html(job, b'\t')
}

fn sv_to_json(job: &Job, delimiter: u8) -> Result<()> {
  let mut reader = csv::ReaderBuilder::new()
    .delimiter(delimiter)
    .has_headers(true)
    .from_path(job.input)?;

  let headers = reader.headers()?.clone();
  let mut file = fs::File::create(job.output)?;

  writeln!(file, "[")?;
  let mut first = true;
  for record in reader.records() {
    let record = record?;
    let mut obj = serde_json::Map::with_capacity(headers.len());
    for (header, value) in headers.iter().zip(record.iter()) {
      obj.insert(
        header.to_string(),
        serde_json::Value::String(value.to_string()),
      );
    }
    if !first {
      writeln!(file, ",")?;
    }
    first = false;
    let line = serde_json::to_string(&serde_json::Value::Object(obj))
      .map_err(|e| Error::other(format!("json serialize: {e}")))?;
    write!(file, "{line}")?;
  }
  writeln!(file)?;
  writeln!(file, "]")?;
  Ok(())
}

fn sv_to_sv(input: &Path, output: &Path, from: u8, to: u8) -> Result<()> {
  let mut reader = csv::ReaderBuilder::new()
    .delimiter(from)
    .has_headers(false)
    .from_path(input)?;
  let mut writer = csv::WriterBuilder::new().delimiter(to).from_path(output)?;

  for record in reader.records() {
    let record = record?;
    writer.write_record(&record)?;
  }
  writer.flush()?;
  Ok(())
}

fn sv_to_html(job: &Job, delimiter: u8) -> Result<()> {
  let mut reader = csv::ReaderBuilder::new()
    .delimiter(delimiter)
    .has_headers(false)
    .from_path(job.input)?;

  let mut out = String::from(
    "<!doctype html><html><head><title>csv2</title></head><body><table>",
  );
  for record in reader.records() {
    let record = record?;
    out.push_str("<tr>");
    for cell in record.iter() {
      out.push_str("<td>");
      push_html_escaped(&mut out, cell);
      out.push_str("</td>");
    }
    out.push_str("</tr>");
  }
  out.push_str("</table></body></html>");
  fs::write(job.output, out)?;
  Ok(())
}

fn push_html_escaped(out: &mut String, s: &str) {
  for c in s.chars() {
    match c {
      '&' => out.push_str("&amp;"),
      '<' => out.push_str("&lt;"),
      '>' => out.push_str("&gt;"),
      '"' => out.push_str("&quot;"),
      '\'' => out.push_str("&#39;"),
      c => out.push(c),
    }
  }
}

fn split_rows(job: &Job, delimiter: u8) -> Result<()> {
  // Matches javascript/src/modules/sv2json.js --split semantics:
  // create a directory named after the output's stem and write 0.json, 1.json, ...
  let dir = job
    .output
    .file_stem()
    .map(|s| job.output.with_file_name(s))
    .ok_or_else(|| Error::other("output has no file stem"))?;

  fs::create_dir_all(&dir)?;

  let mut reader = csv::ReaderBuilder::new()
    .delimiter(delimiter)
    .has_headers(true)
    .from_path(job.input)?;
  let headers = reader.headers()?.clone();

  for (i, record) in reader.records().enumerate() {
    let record = record?;
    let mut obj = serde_json::Map::with_capacity(headers.len());
    for (header, value) in headers.iter().zip(record.iter()) {
      obj.insert(
        header.to_string(),
        serde_json::Value::String(value.to_string()),
      );
    }
    let path = dir.join(format!("{i}.json"));
    let json = serde_json::to_string_pretty(&serde_json::Value::Object(obj))
      .map_err(|e| Error::other(format!("json serialize: {e}")))?;
    fs::write(path, json)?;
  }
  Ok(())
}