use std::fs;
use crate::{Error, Job, Result};
pub fn json_to_yaml(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 yaml = serde_yaml_ng::to_string(&value)
.map_err(|e| Error::other(format!("yaml serialize: {e}")))?;
fs::write(job.output, yaml)?;
Ok(())
}
pub fn yaml_to_json(job: &Job) -> Result<()> {
let raw = fs::read_to_string(job.input)?;
let mut value: serde_json::Value = serde_yaml_ng::from_str(&raw)
.map_err(|e| Error::other(format!("invalid YAML: {e}")))?;
normalize_yaml_timestamps(&mut value);
let json = serde_json::to_string(&value)
.map_err(|e| Error::other(format!("json serialize: {e}")))?;
fs::write(job.output, json + "\n")?;
Ok(())
}
fn normalize_yaml_timestamps(value: &mut serde_json::Value) {
match value {
serde_json::Value::String(s) => {
if let Some(iso) = yaml_ts_to_iso(s) {
*s = iso;
}
}
serde_json::Value::Array(arr) => {
for item in arr {
normalize_yaml_timestamps(item);
}
}
serde_json::Value::Object(map) => {
for (_, v) in map.iter_mut() {
normalize_yaml_timestamps(v);
}
}
_ => {}
}
}
fn yaml_ts_to_iso(s: &str) -> Option<String> {
let bytes = s.as_bytes();
if s.len() == 10
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& s[..4].chars().all(|c| c.is_ascii_digit())
&& s[5..7].chars().all(|c| c.is_ascii_digit())
&& s[8..10].chars().all(|c| c.is_ascii_digit())
{
return Some(format!("{s}T00:00:00.000Z"));
}
None
}
pub fn json_to_json5(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 out = json5_writer::write(&value);
fs::write(job.output, out)?;
Ok(())
}
pub fn json5_to_json(job: &Job) -> Result<()> {
let raw = fs::read_to_string(job.input)?;
let value: serde_json::Value = json5::from_str(&raw)
.map_err(|e| Error::other(format!("invalid JSON5: {e}")))?;
let json = serde_json::to_string(&value)
.map_err(|e| Error::other(format!("json serialize: {e}")))?;
fs::write(job.output, json + "\n")?;
Ok(())
}
mod json5_writer {
use serde_json::Value;
use std::fmt::Write;
pub fn write(value: &Value) -> String {
let mut out = String::new();
write_value(&mut out, value, 0);
out.push('\n');
out
}
fn write_value(out: &mut String, value: &Value, indent: usize) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => {
let _ = write!(out, "{n}");
}
Value::String(s) => write_string(out, s),
Value::Array(arr) => write_array(out, arr, indent),
Value::Object(map) => write_object(out, map, indent),
}
}
fn write_array(out: &mut String, arr: &[Value], indent: usize) {
if arr.is_empty() {
out.push_str("[]");
return;
}
out.push('[');
out.push('\n');
for item in arr {
pad(out, indent + 1);
write_value(out, item, indent + 1);
out.push(',');
out.push('\n');
}
pad(out, indent);
out.push(']');
}
fn write_object(
out: &mut String,
map: &serde_json::Map<String, Value>,
indent: usize,
) {
if map.is_empty() {
out.push_str("{}");
return;
}
out.push('{');
out.push('\n');
for (k, v) in map {
pad(out, indent + 1);
write_key(out, k);
out.push_str(": ");
write_value(out, v, indent + 1);
out.push(',');
out.push('\n');
}
pad(out, indent);
out.push('}');
}
fn pad(out: &mut String, indent: usize) {
for _ in 0..indent {
out.push_str(" ");
}
}
fn write_key(out: &mut String, key: &str) {
if is_bare_identifier(key) {
out.push_str(key);
} else {
write_string(out, key);
}
}
fn is_bare_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
None => return false,
Some(c) if !(c.is_ascii_alphabetic() || c == '_' || c == '$') => {
return false;
}
_ => {}
}
for c in chars {
if !(c.is_ascii_alphanumeric() || c == '_' || c == '$') {
return false;
}
}
true
}
fn write_string(out: &mut String, s: &str) {
out.push('\'');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0C' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('\'');
}
}