use serde::{Deserialize, Serialize};
use crate::abi::CodecId;
use crate::identity::ProducerId;
use crate::time::{RobotInstant, TimeWindow};
const MAX_METADATA_BYTES: usize = 4 * 1024;
pub(crate) const MAX_SOURCE_PARTICIPANT_BYTES: usize = 512;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusMetadata {
pub codec: u8,
pub producer: ProducerId,
pub sequence: u64,
pub produced_at: Option<TimeWindow>,
pub participant: String,
}
impl BusMetadata {
pub fn encode(&self) -> Vec<u8> {
let mut bounded = self.clone();
if bounded.participant.len() > MAX_SOURCE_PARTICIPANT_BYTES {
bounded.participant = truncate_utf8(&bounded.participant, MAX_SOURCE_PARTICIPANT_BYTES);
}
let encoded =
rmp_serde::to_vec_named(&bounded).expect("BusMetadata is always serializable");
debug_assert!(encoded.len() <= MAX_METADATA_BYTES);
encoded
}
pub fn decode(bytes: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
if bytes.len() > MAX_METADATA_BYTES {
return Err(rmp_serde::decode::Error::Syntax(format!(
"BusMetadata exceeds the {MAX_METADATA_BYTES}-byte limit"
)));
}
rmp_serde::from_slice(bytes)
}
pub fn codec_id(&self) -> Option<CodecId> {
CodecId::from_u8(self.codec)
}
pub fn produced_exactly_at(&self) -> Option<RobotInstant> {
self.produced_at.and_then(TimeWindow::as_exact)
}
}
fn truncate_utf8(value: &str, max_bytes: usize) -> String {
if value.len() <= max_bytes {
return value.to_string();
}
let mut end = max_bytes;
while !value.is_char_boundary(end) {
end -= 1;
}
value[..end].to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::TimelineId;
fn metadata(produced_at: Option<TimeWindow>) -> BusMetadata {
BusMetadata {
codec: CodecId::MessagePack.as_u8(),
producer: ProducerId::mint(),
sequence: 7,
produced_at,
participant: "unit".to_string(),
}
}
#[test]
fn absence_of_a_production_instant_round_trips_as_absence() {
let original = metadata(None);
let decoded = BusMetadata::decode(&original.encode()).unwrap();
assert_eq!(decoded, original);
assert_eq!(decoded.produced_at, None);
assert_eq!(decoded.produced_exactly_at(), None);
}
#[test]
fn an_exact_production_instant_round_trips_without_collapsing_a_window() {
let timeline = TimelineId::mint();
let exact = metadata(Some(TimeWindow::exact(RobotInstant::new(timeline, 42))));
let decoded = BusMetadata::decode(&exact.encode()).unwrap();
assert_eq!(
decoded.produced_exactly_at(),
Some(RobotInstant::new(timeline, 42))
);
let window = TimeWindow::bounded(
RobotInstant::new(timeline, 40),
RobotInstant::new(timeline, 44),
)
.unwrap();
let bounded = BusMetadata::decode(&metadata(Some(window)).encode()).unwrap();
assert_eq!(bounded.produced_at, Some(window));
assert_eq!(
bounded.produced_exactly_at(),
None,
"a bounded estimate must not present itself as exact"
);
}
#[test]
fn an_over_long_participant_label_is_truncated_at_a_char_boundary() {
let mut long = metadata(None);
long.participant = "é".repeat(MAX_SOURCE_PARTICIPANT_BYTES);
let decoded = BusMetadata::decode(&long.encode()).unwrap();
assert!(decoded.participant.len() <= MAX_SOURCE_PARTICIPANT_BYTES);
assert!(decoded.participant.chars().all(|c| c == 'é'));
}
}