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(())
}