use crate::event::{EventKind, EventPayload};
use batpak_macros::Error;
use serde::de::DeserializeOwned;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
pub trait Upcast {
const KIND: EventKind;
const FROM_VERSION: u16;
fn upcast(value: rmpv::Value) -> Result<rmpv::Value, UpcastError>;
}
#[derive(Debug, Error)]
pub enum UpcastError {
#[error(
"no registered upcast step for kind {kind:?} from version {from_version} \
(need to reach version {to_version}); register an Upcast for this hop"
)]
MissingStep {
kind: EventKind,
from_version: u16,
to_version: u16,
},
#[error("duplicate upcast step registered for kind {kind:?} from version {from_version}")]
DuplicateStep {
kind: EventKind,
from_version: u16,
},
#[error("upcast step for kind {kind:?} from version {from_version} failed: {source}")]
Step {
kind: EventKind,
from_version: u16,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("upcast value codec error: {0}")]
ValueCodec(String),
}
#[macro_export]
macro_rules! register_upcast {
($ty:ty) => {
$crate::__private::inventory::submit! {
$crate::__private::UpcastRegistration {
kind_bits: <$ty as $crate::event::Upcast>::KIND.as_raw_u16(),
from_version: <$ty as $crate::event::Upcast>::FROM_VERSION,
step: |value| {
<$ty as $crate::event::Upcast>::upcast(value)
.map_err(|e| -> ::std::boxed::Box<
dyn ::std::error::Error + ::std::marker::Send + ::std::marker::Sync,
> { ::std::boxed::Box::new(e) })
},
}
}
};
}
pub(crate) fn upcast_and_decode<T: EventPayload>(
value: rmpv::Value,
from_version: u16,
to_version: u16,
) -> Result<T, UpcastError> {
let lifted = run_chain(T::KIND, value, from_version, to_version)?;
decode_value::<T>(&lifted)
}
pub(crate) fn run_chain(
kind: EventKind,
mut value: rmpv::Value,
from_version: u16,
to_version: u16,
) -> Result<rmpv::Value, UpcastError> {
let registry = crate::__private::upcast_steps_for(kind.as_raw_u16());
let mut current = from_version;
while current < to_version {
let mut matches = registry.iter().filter(|reg| reg.from_version == current);
let Some(step) = matches.next() else {
return Err(UpcastError::MissingStep {
kind,
from_version: current,
to_version,
});
};
if matches.next().is_some() {
return Err(UpcastError::DuplicateStep {
kind,
from_version: current,
});
}
value = (step.step)(value).map_err(|source| UpcastError::Step {
kind,
from_version: current,
source,
})?;
current += 1;
}
Ok(value)
}
fn decode_value<T: DeserializeOwned>(value: &rmpv::Value) -> Result<T, UpcastError> {
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, value)
.map_err(|e| UpcastError::ValueCodec(format!("re-encode upcasted value: {e}")))?;
crate::encoding::from_bytes::<T>(&buf)
.map_err(|e| UpcastError::ValueCodec(format!("decode upcasted value into target: {e}")))
}
pub(crate) fn value_from_msgpack(bytes: &[u8]) -> Result<rmpv::Value, UpcastError> {
let mut cursor = bytes;
let value = rmpv::decode::read_value(&mut cursor)
.map_err(|e| UpcastError::ValueCodec(format!("read stored msgpack as value: {e}")))?;
if !cursor.is_empty() {
return Err(UpcastError::ValueCodec(format!(
"{} trailing byte(s) after stored msgpack value",
cursor.len()
)));
}
Ok(value)
}
pub(crate) fn value_from_json(value: &serde_json::Value) -> Result<rmpv::Value, UpcastError> {
let bytes = crate::encoding::to_bytes(value)
.map_err(|e| UpcastError::ValueCodec(format!("encode json payload to msgpack: {e}")))?;
value_from_msgpack(&bytes)
}
static UPCAST_CHAIN_OPEN_CACHE: Mutex<Option<Result<(), UpcastChainRegistryError>>> =
Mutex::new(None);
static UPCAST_CHAIN_WARNED: AtomicBool = AtomicBool::new(false);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IncompleteUpcastChain {
pub kind: EventKind,
pub current_version: u16,
pub missing_from_versions: Vec<u16>,
pub type_name: &'static str,
}
impl IncompleteUpcastChain {
fn from_support(chain: batpak_macros_support::IncompleteUpcastChain) -> Self {
Self {
kind: EventKind::from_raw_u16(chain.kind_bits),
current_version: chain.current_version,
missing_from_versions: chain.missing_from_versions,
type_name: chain.type_name,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UpcastChainRegistryError {
incomplete: Vec<IncompleteUpcastChain>,
}
impl UpcastChainRegistryError {
pub fn new(incomplete: Vec<IncompleteUpcastChain>) -> Self {
Self { incomplete }
}
pub fn incomplete_chains(&self) -> &[IncompleteUpcastChain] {
&self.incomplete
}
}
impl std::fmt::Display for UpcastChainRegistryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"linked EventPayload registry has {} kind(s) declaring version > 1 \
without a complete upcast chain",
self.incomplete.len()
)?;
for chain in &self.incomplete {
write!(
f,
"; kind category=0x{:X} type_id=0x{:03X} (`{}`) declares version {} \
but is missing upcast step(s) from version(s) {:?} — register an Upcast \
for each missing hop (1 -> 2 -> ... -> {})",
chain.kind.category(),
chain.kind.type_id(),
chain.type_name,
chain.current_version,
chain.missing_from_versions,
chain.current_version,
)?;
}
Ok(())
}
}
impl std::error::Error for UpcastChainRegistryError {}
pub fn validate_upcast_chain_registry() -> Result<(), UpcastChainRegistryError> {
let incomplete = batpak_macros_support::find_incomplete_upcast_chains()
.into_iter()
.map(IncompleteUpcastChain::from_support)
.collect::<Vec<_>>();
if incomplete.is_empty() {
Ok(())
} else {
Err(UpcastChainRegistryError::new(incomplete))
}
}
pub fn revalidate_upcast_chain_registry() -> Result<(), UpcastChainRegistryError> {
let result = validate_upcast_chain_registry();
UPCAST_CHAIN_WARNED.store(false, Ordering::SeqCst);
let Ok(mut cached) = UPCAST_CHAIN_OPEN_CACHE.lock() else {
return result;
};
*cached = Some(result.clone());
result
}
pub(crate) fn cached_upcast_chain_registry_validation() -> Result<(), UpcastChainRegistryError> {
let Ok(mut cached) = UPCAST_CHAIN_OPEN_CACHE.lock() else {
return validate_upcast_chain_registry();
};
if let Some(result) = cached.as_ref() {
return result.clone();
}
let result = validate_upcast_chain_registry();
*cached = Some(result.clone());
result
}
pub(crate) fn mark_upcast_chain_registry_warning_emitted() -> bool {
!UPCAST_CHAIN_WARNED.swap(true, Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
use serde_json::{Map, Value};
fn arb_json_value() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
any::<i64>().prop_map(|n| Value::Number(n.into())),
"[a-zA-Z0-9 _:-]{0,24}".prop_map(Value::String),
];
leaf.prop_recursive(3, 24, 4, |inner| {
prop_oneof![
proptest::collection::vec(inner.clone(), 0..4).prop_map(Value::Array),
proptest::collection::btree_map("[a-zA-Z0-9_:-]{1,12}", inner, 0..4).prop_map(
|items| {
let mut map = Map::new();
for (key, value) in items {
map.insert(key, value);
}
Value::Object(map)
}
),
]
})
}
fn prop_result<T, E: std::fmt::Display>(
result: Result<T, E>,
context: &'static str,
) -> Result<T, TestCaseError> {
result.map_err(|err| TestCaseError::fail(format!("{context}: {err}")))
}
#[test]
fn value_from_msgpack_rejects_trailing_bytes() {
let mut bytes = Vec::new();
rmpv::encode::write_value(&mut bytes, &rmpv::Value::from(42u8)).expect("encode test value");
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let err = value_from_msgpack(&bytes)
.expect_err("PROPERTY: trailing bytes after a msgpack value must be rejected");
assert!(
matches!(&err, UpcastError::ValueCodec(_)),
"expected ValueCodec error, got {err:?}"
);
let UpcastError::ValueCodec(msg) = err else {
unreachable!("matches! above already asserted the ValueCodec variant")
};
assert!(
msg.contains("4 trailing byte(s)"),
"error must report the trailing byte count, got: {msg}"
);
}
#[test]
fn value_from_msgpack_accepts_a_clean_single_value() {
let mut bytes = Vec::new();
rmpv::encode::write_value(&mut bytes, &rmpv::Value::from(7u8)).expect("encode test value");
let value = value_from_msgpack(&bytes).expect("clean single value decodes");
assert_eq!(value.as_u64(), Some(7));
}
proptest! {
#[test]
fn value_from_json_and_msgpack_agree(value in arb_json_value()) {
let bytes = prop_result(crate::encoding::to_bytes(&value), "encode json value")?;
let from_json = prop_result(value_from_json(&value), "value from json")?;
let from_msgpack = prop_result(value_from_msgpack(&bytes), "value from msgpack")?;
prop_assert_eq!(from_json, from_msgpack);
}
}
}