1use serde::{Deserialize, Serialize};
20
21use crate::abi::CodecId;
22use crate::identity::ProducerId;
23use crate::time::{RobotInstant, TimeWindow};
24
25const MAX_METADATA_BYTES: usize = 4 * 1024;
26pub(crate) const MAX_SOURCE_PARTICIPANT_BYTES: usize = 512;
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub struct BusMetadata {
31 pub codec: u8,
33 pub producer: ProducerId,
35 pub sequence: u64,
38 pub produced_at: Option<TimeWindow>,
41 pub participant: String,
44}
45
46impl BusMetadata {
47 pub fn encode(&self) -> Vec<u8> {
49 let mut bounded = self.clone();
50 if bounded.participant.len() > MAX_SOURCE_PARTICIPANT_BYTES {
51 bounded.participant = truncate_utf8(&bounded.participant, MAX_SOURCE_PARTICIPANT_BYTES);
52 }
53 let encoded =
54 rmp_serde::to_vec_named(&bounded).expect("BusMetadata is always serializable");
55 debug_assert!(encoded.len() <= MAX_METADATA_BYTES);
56 encoded
57 }
58
59 pub fn decode(bytes: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
61 if bytes.len() > MAX_METADATA_BYTES {
62 return Err(rmp_serde::decode::Error::Syntax(format!(
63 "BusMetadata exceeds the {MAX_METADATA_BYTES}-byte limit"
64 )));
65 }
66 rmp_serde::from_slice(bytes)
67 }
68
69 pub fn codec_id(&self) -> Option<CodecId> {
71 CodecId::from_u8(self.codec)
72 }
73
74 pub fn produced_exactly_at(&self) -> Option<RobotInstant> {
80 self.produced_at.and_then(TimeWindow::as_exact)
81 }
82}
83
84fn truncate_utf8(value: &str, max_bytes: usize) -> String {
85 if value.len() <= max_bytes {
86 return value.to_string();
87 }
88 let mut end = max_bytes;
89 while !value.is_char_boundary(end) {
90 end -= 1;
91 }
92 value[..end].to_string()
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::identity::TimelineId;
99
100 fn metadata(produced_at: Option<TimeWindow>) -> BusMetadata {
101 BusMetadata {
102 codec: CodecId::MessagePack.as_u8(),
103 producer: ProducerId::mint(),
104 sequence: 7,
105 produced_at,
106 participant: "unit".to_string(),
107 }
108 }
109
110 #[test]
111 fn absence_of_a_production_instant_round_trips_as_absence() {
112 let original = metadata(None);
113 let decoded = BusMetadata::decode(&original.encode()).unwrap();
114 assert_eq!(decoded, original);
115 assert_eq!(decoded.produced_at, None);
116 assert_eq!(decoded.produced_exactly_at(), None);
117 }
118
119 #[test]
120 fn an_exact_production_instant_round_trips_without_collapsing_a_window() {
121 let timeline = TimelineId::mint();
122 let exact = metadata(Some(TimeWindow::exact(RobotInstant::new(timeline, 42))));
123 let decoded = BusMetadata::decode(&exact.encode()).unwrap();
124 assert_eq!(
125 decoded.produced_exactly_at(),
126 Some(RobotInstant::new(timeline, 42))
127 );
128
129 let window = TimeWindow::bounded(
130 RobotInstant::new(timeline, 40),
131 RobotInstant::new(timeline, 44),
132 )
133 .unwrap();
134 let bounded = BusMetadata::decode(&metadata(Some(window)).encode()).unwrap();
135 assert_eq!(bounded.produced_at, Some(window));
136 assert_eq!(
137 bounded.produced_exactly_at(),
138 None,
139 "a bounded estimate must not present itself as exact"
140 );
141 }
142
143 #[test]
144 fn an_over_long_participant_label_is_truncated_at_a_char_boundary() {
145 let mut long = metadata(None);
146 long.participant = "é".repeat(MAX_SOURCE_PARTICIPANT_BYTES);
147 let decoded = BusMetadata::decode(&long.encode()).unwrap();
148 assert!(decoded.participant.len() <= MAX_SOURCE_PARTICIPANT_BYTES);
149 assert!(decoded.participant.chars().all(|c| c == 'é'));
150 }
151}