use sha2::{Digest, Sha256};
use zenith_core::{KdlAdapter, KdlSource};
use crate::commands::serialize_pretty;
use crate::json_types::FmtOutput;
#[derive(Debug)]
pub struct FmtErr {
pub message: String,
pub exit_code: u8,
}
#[derive(Debug)]
pub struct FmtResult {
pub formatted: Vec<u8>,
pub changed: bool,
pub hash: String,
}
pub fn run(src: &str) -> Result<FmtResult, FmtErr> {
let doc = KdlAdapter.parse(src.as_bytes()).map_err(|e| FmtErr {
message: format!("parse error: {}", e.message),
exit_code: 2,
})?;
let formatted = KdlAdapter.format(&doc).map_err(|e| FmtErr {
message: format!("format error: {}", e.message),
exit_code: 2,
})?;
let changed = formatted != src.as_bytes();
let hash = hex_hash(&formatted);
Ok(FmtResult {
formatted,
changed,
hash,
})
}
pub fn render_stdout(result: &FmtResult, json: bool) -> String {
if json {
let out = FmtOutput {
schema: "zenith-fmt-v1",
changed: result.changed,
hash: result.hash.clone(),
};
serialize_pretty(&out)
} else if result.changed {
format!("formatted (hash: {})", result.hash)
} else {
format!("already canonical (hash: {})", result.hash)
}
}
fn hex_hash(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const FMT_INPUT: &str = r##"zenith version=1 {
project id="proj.f" name="Fmt Test"
tokens format="zenith-token-v1" {
token id="color.bg" type="color" value="#f8fafc"
}
styles {
}
document id="doc.f" title="Fmt Test" {
page id="page.f" w=(px)320 h=(px)200 {
rect id="rect.f" x=(px)0 y=(px)0 w=(px)320 h=(px)200 fill=(token)"color.bg"
}
}
}
"##;
#[test]
fn already_formatted_doc_reports_not_changed() {
let first = run(FMT_INPUT).expect("must succeed");
let canonical = std::str::from_utf8(&first.formatted).expect("utf8");
let second = run(canonical).expect("second run");
assert!(
!second.changed,
"fmt on already-canonical doc must report changed=false"
);
}
#[test]
fn fmt_is_idempotent() {
let first = run(FMT_INPUT).expect("first fmt");
let second = run(std::str::from_utf8(&first.formatted).expect("utf8")).expect("second fmt");
assert_eq!(
first.formatted, second.formatted,
"fmt must be idempotent: fmt(fmt(x)) == fmt(x)"
);
}
#[test]
fn parse_error_returns_err() {
let result = run("not valid kdl {{{");
assert!(result.is_err(), "parse error must return Err");
assert_eq!(result.unwrap_err().exit_code, 2);
}
#[test]
fn hash_is_stable() {
let r1 = run(FMT_INPUT).expect("r1");
let r2 = run(FMT_INPUT).expect("r2");
assert_eq!(r1.hash, r2.hash, "hash must be stable across runs");
}
}