use crate::__compat::wire::{DescribeWire, WireSchema};
use crate::identity::{ParticipantId, ProducerId};
use serde::{Deserialize, Serialize};
use crate::bus::abi::CodecId;
use crate::bus::time::{RobotInstant, TimeWindow};
const MAX_METADATA_BYTES: usize = 4 * 1024;
pub(crate) const MAX_SOURCE_LABEL_BYTES: usize = 512;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct SourceLabel(String);
impl SourceLabel {
pub fn new(value: impl Into<String>) -> Result<Self, SourceLabelError> {
let value = value.into();
if value.is_empty() || value.len() > MAX_SOURCE_LABEL_BYTES {
return Err(SourceLabelError(value));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for SourceLabel {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for SourceLabel {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
impl DescribeWire for SourceLabel {
fn wire_schema() -> WireSchema {
WireSchema::opaque("SourceLabel", WireSchema::String)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("source label must be non-empty and at most {MAX_SOURCE_LABEL_BYTES} bytes, got {0:?}")]
pub struct SourceLabelError(String);
#[derive(
phoxal_macros::DescribeWire, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize,
)]
pub struct ParticipantSourceIdentity {
pub participant: ParticipantId,
pub producer: ProducerId,
}
impl ParticipantSourceIdentity {
#[must_use]
pub fn new(participant: ParticipantId, producer: ProducerId) -> Self {
Self {
participant,
producer,
}
}
}
#[derive(phoxal_macros::DescribeWire, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SourceAttribution {
Participant(ParticipantSourceIdentity),
External {
producer: ProducerId,
label: Option<SourceLabel>,
},
}
impl SourceAttribution {
pub fn participant_source(&self) -> Option<&ParticipantSourceIdentity> {
match self {
Self::Participant(source) => Some(source),
Self::External { .. } => None,
}
}
pub fn participant(&self) -> Option<&ParticipantId> {
self.participant_source().map(|source| &source.participant)
}
pub fn producer(&self) -> ProducerId {
match self {
Self::Participant(source) => source.producer,
Self::External { producer, .. } => *producer,
}
}
pub fn label(&self) -> Option<&SourceLabel> {
match self {
Self::Participant(_) => None,
Self::External { label, .. } => label.as_ref(),
}
}
}
#[derive(phoxal_macros::DescribeWire, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StreamPosition {
pub sequence: u64,
}
#[derive(phoxal_macros::DescribeWire, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusMetadata {
pub codec: u8,
pub sequence: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream_position: Option<StreamPosition>,
pub produced_at: Option<TimeWindow>,
pub source: SourceAttribution,
}
impl BusMetadata {
pub fn encode(&self) -> std::result::Result<Vec<u8>, rmp_serde::encode::Error> {
let encoded = rmp_serde::to_vec_named(self)?;
debug_assert!(encoded.len() <= MAX_METADATA_BYTES);
Ok(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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::TimelineId;
use crate::bus::test_support::producer;
#[test]
fn the_declared_label_shape_is_the_shape_its_serializer_writes() {
let label = SourceLabel::new("external-bridge").expect("a bounded label");
let json = serde_json::to_value(&label).expect("a label serializes");
assert_eq!(SourceLabel::wire_schema().conforms(&json), Ok(()));
}
fn metadata(produced_at: Option<TimeWindow>) -> BusMetadata {
BusMetadata {
codec: CodecId::MessagePack.as_u8(),
sequence: 7,
stream_position: None,
produced_at,
source: SourceAttribution::Participant(ParticipantSourceIdentity::new(
ParticipantId::new("unit").expect("test participant"),
producer(1),
)),
}
}
fn encoded(metadata: &BusMetadata) -> Vec<u8> {
metadata.encode().expect("test metadata encodes")
}
#[test]
fn the_bootstrap_reply_attachment_is_pinned_to_its_literal_fields() {
let names = |attachment: &BusMetadata| -> Vec<String> {
serde_json::to_value(attachment)
.expect("the attachment serializes")
.as_object()
.expect("the attachment is a map")
.keys()
.cloned()
.collect()
};
let reply = metadata(None);
assert_eq!(
names(&reply),
["codec", "produced_at", "sequence", "source"]
);
let mut streamed = metadata(None);
streamed.stream_position = Some(StreamPosition { sequence: 0 });
assert_eq!(
names(&streamed),
[
"codec",
"produced_at",
"sequence",
"source",
"stream_position"
]
);
let bytes = encoded(&reply);
for name in ["codec", "produced_at", "sequence", "source"] {
assert!(
bytes
.windows(name.len())
.any(|window| window == name.as_bytes()),
"the encoded attachment must spell '{name}'"
);
}
}
#[test]
fn provenance_round_trips_through_the_attachment() {
let original = metadata(Some(TimeWindow::exact(RobotInstant::new(
TimelineId::mint(),
42,
))));
assert_eq!(BusMetadata::decode(&encoded(&original)).unwrap(), original);
}
#[test]
fn absence_of_a_production_instant_round_trips_as_absence() {
let original = metadata(None);
let decoded = BusMetadata::decode(&encoded(&original)).unwrap();
assert_eq!(decoded, original);
assert_eq!(decoded.produced_at, None);
assert_eq!(decoded.produced_exactly_at(), None);
}
#[test]
fn stream_position_round_trips_without_changing_ordinary_sequence() {
let mut original = metadata(None);
original.stream_position = Some(StreamPosition { sequence: 41 });
let decoded = BusMetadata::decode(&encoded(&original)).unwrap();
assert_eq!(decoded, original);
assert_eq!(decoded.sequence, 7);
assert_eq!(decoded.stream_position.unwrap().sequence, 41);
}
#[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(&encoded(&exact)).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(&encoded(&metadata(Some(window)))).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_external_label_is_rejected_at_construction() {
let mut long = metadata(None);
assert!(SourceLabel::new("\u{e9}".repeat(MAX_SOURCE_LABEL_BYTES)).is_err());
long.source = SourceAttribution::External {
producer: producer(1),
label: Some(SourceLabel::new("diagnostic").expect("label")),
};
assert!(BusMetadata::decode(&encoded(&long)).is_ok());
}
#[test]
fn the_attachment_stays_inside_its_wire_limit_in_both_directions() {
let mut oversized = metadata(None);
oversized.source = SourceAttribution::External {
producer: producer(1),
label: Some(SourceLabel::new("diagnostic").expect("label")),
};
let bytes = encoded(&oversized);
assert!(bytes.len() <= MAX_METADATA_BYTES);
let decoded = BusMetadata::decode(&bytes).expect("bounded metadata decodes");
assert!(decoded.source.label().is_some());
let error = BusMetadata::decode(&vec![0_u8; MAX_METADATA_BYTES + 1]).unwrap_err();
assert!(error.to_string().contains("4096-byte limit"));
}
}