use serde_json::Value;
use crate::core::StoreError;
pub trait Upcaster: Send + Sync + std::fmt::Debug {
fn current_version(&self, kind: &str) -> u16;
fn upcast(&self, kind: &str, version: u16, payload: Value) -> Result<Value, StoreError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Identity;
impl Upcaster for Identity {
fn current_version(&self, _kind: &str) -> u16 {
1
}
fn upcast(&self, kind: &str, version: u16, payload: Value) -> Result<Value, StoreError> {
match version {
1 => Ok(payload),
_ => Err(StoreError::Corrupt {
seq: 0,
detail: format!(
"record {kind} is v{version}, and this build writes and reads v1 only. \
Record shapes change by hard cut until the format freeze, so a journal \
at another version is refused rather than read with fields quietly \
defaulted — a false answer to an audit question is worse than a \
refusal to answer. Start a fresh journal; if v{version} is the newer \
one, deploy readers before writers"
),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_journal_from_an_older_cut_is_refused_rather_than_misread() {
let err = Identity
.upcast("RunAdmitted", 0, json!({ "agent": "a", "input": null }))
.unwrap_err();
let text = err.to_string();
assert!(
text.contains("fresh journal"),
"the refusal must say what to do about it: {text}"
);
}
#[test]
fn future_versions_are_refused_not_guessed() {
let err = Identity.upcast("EffectDone", 7, json!({})).unwrap_err();
assert!(matches!(err, StoreError::Corrupt { .. }));
}
#[test]
fn current_version_passes_through() {
let v = Identity.current_version("EffectDone");
assert_eq!(
Identity.upcast("EffectDone", v, json!({"a": 1})).unwrap(),
json!({"a": 1})
);
}
#[test]
fn upcasting_is_pure() {
let v = Identity.current_version("StepStarted");
let a = Identity
.upcast("StepStarted", v, json!({"skill": "x"}))
.unwrap();
let b = Identity
.upcast("StepStarted", v, json!({"skill": "x"}))
.unwrap();
assert_eq!(a, b);
}
}