#![deny(missing_docs)]
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Decision {
pub id: String,
#[serde(with = "system_time_serde")]
pub timestamp: SystemTime,
pub options: Vec<String>,
pub chosen: String,
pub rationale: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<String>,
#[serde(default)]
pub meta: serde_json::Value,
}
impl Decision {
pub fn chose_listed_option(&self) -> bool {
self.options.iter().any(|o| o == &self.chosen)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DecisionLog {
pub decisions: Vec<Decision>,
}
impl DecisionLog {
pub fn new() -> Self {
Self::default()
}
pub fn add<O, C, R>(
&mut self,
options: Vec<O>,
chosen: C,
rationale: R,
meta: serde_json::Value,
) -> String
where
O: Into<String>,
C: Into<String>,
R: Into<String>,
{
let id = new_id();
let decision = Decision {
id: id.clone(),
timestamp: SystemTime::now(),
options: options.into_iter().map(Into::into).collect(),
chosen: chosen.into(),
rationale: rationale.into(),
outcome: None,
meta,
};
self.decisions.push(decision);
id
}
pub fn set_outcome<S: Into<String>>(&mut self, id: &str, outcome: S) -> bool {
if let Some(d) = self.decisions.iter_mut().find(|d| d.id == id) {
d.outcome = Some(outcome.into());
true
} else {
false
}
}
pub fn find_by_id(&self, id: &str) -> Option<&Decision> {
self.decisions.iter().find(|d| d.id == id)
}
pub fn last(&self) -> Option<&Decision> {
self.decisions.last()
}
pub fn len(&self) -> usize {
self.decisions.len()
}
pub fn is_empty(&self) -> bool {
self.decisions.is_empty()
}
pub fn to_jsonl<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
let file = File::create(path)?;
let mut w = BufWriter::new(file);
for d in &self.decisions {
let line = serde_json::to_string(d).map_err(io_invalid_data)?;
w.write_all(line.as_bytes())?;
w.write_all(b"\n")?;
}
w.flush()
}
pub fn from_jsonl<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut decisions = Vec::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let d: Decision = serde_json::from_str(trimmed).map_err(io_invalid_data)?;
decisions.push(d);
}
Ok(Self { decisions })
}
}
fn io_invalid_data(e: serde_json::Error) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
}
fn new_id() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("dec_{:016x}{:08x}", nanos, seq)
}
mod system_time_serde {
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(t: &SystemTime, s: S) -> Result<S::Ok, S::Error> {
let nanos = t
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
nanos.to_string().serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SystemTime, D::Error> {
let raw = String::deserialize(d)?;
let nanos: u128 = raw.parse().map_err(serde::de::Error::custom)?;
let secs = (nanos / 1_000_000_000) as u64;
let sub = (nanos % 1_000_000_000) as u32;
Ok(UNIX_EPOCH + Duration::new(secs, sub))
}
}