use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Record {
pub v: u32,
pub bench: String,
pub tool: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
pub runner: String,
pub ts: String,
pub metrics: BTreeMap<String, f64>,
}
impl Record {
pub fn to_line(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string(self)?)
}
pub fn from_line(line: &str) -> anyhow::Result<Option<Self>> {
let line = line.trim();
if line.is_empty() {
return Ok(None);
}
let rec: Record = serde_json::from_str(line)?;
if rec.v > SCHEMA_VERSION {
return Ok(None);
}
Ok(Some(rec))
}
}
pub fn parse_note(body: &str) -> Vec<Record> {
body.lines()
.filter_map(|l| Record::from_line(l).ok().flatten())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn rec() -> Record {
Record {
v: SCHEMA_VERSION,
bench: "hook-env".into(),
tool: "mise".into(),
version: None,
runner: "linux-amd64".into(),
ts: "2026-07-24T09:00:00Z".into(),
metrics: BTreeMap::from([
("instructions".to_string(), 29_413_235.0),
("wall_min_ms".to_string(), 11.28),
]),
}
}
#[test]
fn line_roundtrips() {
let line = rec().to_line().unwrap();
let back = Record::from_line(&line).unwrap().unwrap();
assert_eq!(back.bench, "hook-env");
assert_eq!(back.metrics["instructions"], 29_413_235.0);
}
#[test]
fn serialisation_is_byte_stable() {
assert_eq!(rec().to_line().unwrap(), rec().to_line().unwrap());
}
#[test]
fn blank_lines_are_skipped_not_errors() {
assert!(Record::from_line(" ").unwrap().is_none());
}
#[test]
fn future_schema_versions_are_skipped() {
let mut r = rec();
r.v = SCHEMA_VERSION + 1;
let line = r.to_line().unwrap();
assert!(Record::from_line(&line).unwrap().is_none());
}
#[test]
fn one_bad_line_does_not_poison_the_note() {
let body = format!(
"{}\nnot json at all\n{}",
rec().to_line().unwrap(),
rec().to_line().unwrap()
);
assert_eq!(parse_note(&body).len(), 2);
}
}