spatialrust_records/
provenance.rs1use crate::{RecordsError, RecordsResult};
4
5pub const RECORD_PROVENANCE_VERSION: u32 = 1;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
15#[cfg_attr(feature = "receipt-json", derive(serde::Serialize, serde::Deserialize))]
16#[cfg_attr(feature = "receipt-json", serde(deny_unknown_fields))]
17pub struct RecordProvenance {
18 pub version: u32,
20 pub source_id: String,
22 #[cfg_attr(feature = "receipt-json", serde(skip_serializing_if = "Option::is_none"))]
24 pub source_uri: Option<String>,
25 #[cfg_attr(feature = "receipt-json", serde(skip_serializing_if = "Option::is_none"))]
27 pub stream_id: Option<String>,
28 #[cfg_attr(feature = "receipt-json", serde(skip_serializing_if = "Option::is_none"))]
30 pub sequence: Option<u64>,
31}
32
33impl RecordProvenance {
34 #[must_use]
36 pub fn unknown() -> Self {
37 Self {
38 version: RECORD_PROVENANCE_VERSION,
39 source_id: "unknown".to_owned(),
40 source_uri: None,
41 stream_id: None,
42 sequence: None,
43 }
44 }
45
46 pub fn try_new(source_id: impl Into<String>) -> RecordsResult<Self> {
48 let source_id = source_id.into();
49 let provenance = Self {
50 version: RECORD_PROVENANCE_VERSION,
51 source_id,
52 source_uri: None,
53 stream_id: None,
54 sequence: None,
55 };
56 provenance.validate()?;
57 Ok(provenance)
58 }
59
60 pub fn validate(&self) -> RecordsResult<()> {
62 if self.version != RECORD_PROVENANCE_VERSION {
63 return Err(RecordsError::InvalidConfiguration(format!(
64 "unsupported record provenance version {}; expected {}",
65 self.version, RECORD_PROVENANCE_VERSION
66 )));
67 }
68 if self.source_id.trim().is_empty() {
69 return Err(RecordsError::InvalidConfiguration(
70 "record provenance source_id must not be empty".into(),
71 ));
72 }
73 if self.source_uri.as_deref().is_some_and(|value| value.trim().is_empty()) {
74 return Err(RecordsError::InvalidConfiguration(
75 "record provenance source_uri must not be empty".into(),
76 ));
77 }
78 if self.stream_id.as_deref().is_some_and(|value| value.trim().is_empty()) {
79 return Err(RecordsError::InvalidConfiguration(
80 "record provenance stream_id must not be empty".into(),
81 ));
82 }
83 Ok(())
84 }
85
86 #[must_use]
88 pub fn with_source_uri(mut self, source_uri: impl Into<String>) -> Self {
89 self.source_uri = Some(source_uri.into());
90 self
91 }
92
93 #[must_use]
95 pub fn with_stream_id(mut self, stream_id: impl Into<String>) -> Self {
96 self.stream_id = Some(stream_id.into());
97 self
98 }
99
100 #[must_use]
102 pub const fn with_sequence(mut self, sequence: Option<u64>) -> Self {
103 self.sequence = sequence;
104 self
105 }
106
107 #[must_use]
109 pub const fn without_sequence(mut self) -> Self {
110 self.sequence = None;
111 self
112 }
113}
114
115impl Default for RecordProvenance {
116 fn default() -> Self {
117 Self::unknown()
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::{RecordProvenance, RECORD_PROVENANCE_VERSION};
124
125 #[test]
126 fn builds_lineage_without_protocol_specific_types() {
127 let provenance = RecordProvenance::try_new("bag-01")
128 .unwrap()
129 .with_source_uri("/media/input/bag.db3")
130 .with_stream_id("/lidar/points")
131 .with_sequence(Some(4));
132 assert_eq!(provenance.version, RECORD_PROVENANCE_VERSION);
133 assert_eq!(provenance.source_id, "bag-01");
134 assert_eq!(provenance.stream_id.as_deref(), Some("/lidar/points"));
135 assert_eq!(provenance.sequence, Some(4));
136 assert_eq!(provenance.without_sequence().sequence, None);
137 }
138
139 #[test]
140 fn rejects_empty_source_identity() {
141 assert!(RecordProvenance::try_new(" ").is_err());
142 }
143
144 #[test]
145 fn rejects_unknown_version_and_empty_optional_identifiers() {
146 let mut provenance = RecordProvenance::unknown();
147 provenance.version = RECORD_PROVENANCE_VERSION + 1;
148 assert!(provenance.validate().is_err());
149
150 let mut provenance = RecordProvenance::unknown();
151 provenance.source_uri = Some(String::new());
152 assert!(provenance.validate().is_err());
153 }
154}