univert 0.4.0

Universal file converter (library and CLI)
Documentation
//! JSON Lines (a.k.a. NDJSON) — one JSON value per line.

use std::fs;
use std::io::{BufRead, BufReader, Write};

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

pub fn jsonl_to_json(job: &Job) -> Result<()> {
  let values = read_values(job)?;
  let json = serde_json::to_string(&serde_json::Value::Array(values))
    .map_err(|e| Error::other(format!("json serialize: {e}")))?;
  fs::write(job.output, json + "\n")?;
  Ok(())
}

pub fn json_to_jsonl(job: &Job) -> Result<()> {
  let raw = fs::read_to_string(job.input)?;
  let value: serde_json::Value = serde_json::from_str(&raw)
    .map_err(|e| Error::other(format!("invalid JSON: {e}")))?;
  let items = match value {
    serde_json::Value::Array(items) => items,
    other => vec![other],
  };
  let mut file = fs::File::create(job.output)?;
  for item in items {
    let line = serde_json::to_string(&item)
      .map_err(|e| Error::other(format!("json serialize: {e}")))?;
    writeln!(file, "{line}")?;
  }
  Ok(())
}

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

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

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

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

fn read_values(job: &Job) -> Result<Vec<serde_json::Value>> {
  let file = fs::File::open(job.input)?;
  let reader = BufReader::new(file);
  let mut out = Vec::new();
  for (i, line) in reader.lines().enumerate() {
    let line = line?;
    let trimmed = line.trim();
    if trimmed.is_empty() {
      continue;
    }
    let v: serde_json::Value = serde_json::from_str(trimmed).map_err(|e| {
      Error::other(format!("invalid JSON on line {}: {e}", i + 1))
    })?;
    out.push(v);
  }
  Ok(out)
}

fn jsonl_to_sv(job: &Job, delimiter: u8) -> Result<()> {
  let values = read_values(job)?;
  let mut headers: Vec<String> = Vec::new();
  let mut seen = std::collections::BTreeSet::new();
  for v in &values {
    if let serde_json::Value::Object(map) = v {
      for k in map.keys() {
        if seen.insert(k.clone()) {
          headers.push(k.clone());
        }
      }
    }
  }

  let mut writer = csv::WriterBuilder::new()
    .delimiter(delimiter)
    .from_path(job.output)?;
  writer.write_record(&headers)?;
  for v in &values {
    let obj = match v {
      serde_json::Value::Object(map) => map,
      _ => {
        return Err(Error::other(
          "jsonl: rows must be JSON objects to convert to csv/tsv",
        ));
      }
    };
    let row: Vec<String> = headers
      .iter()
      .map(|h| match obj.get(h) {
        None | Some(serde_json::Value::Null) => String::new(),
        Some(serde_json::Value::String(s)) => s.clone(),
        Some(v) => v.to_string(),
      })
      .collect();
    writer.write_record(&row)?;
  }
  writer.flush()?;
  Ok(())
}

fn sv_to_jsonl(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)?;
  for record in reader.records() {
    let record = record?;
    let mut obj = serde_json::Map::with_capacity(headers.len());
    for (h, v) in headers.iter().zip(record.iter()) {
      obj.insert(h.to_string(), serde_json::Value::String(v.to_string()));
    }
    let line = serde_json::to_string(&serde_json::Value::Object(obj))
      .map_err(|e| Error::other(format!("json serialize: {e}")))?;
    writeln!(file, "{line}")?;
  }
  Ok(())
}