bitemporal_runtime/
types.rs1#[cfg(feature = "schema")]
4use schemars::JsonSchema;
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9pub type RecordId = String;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema", derive(JsonSchema))]
20#[cfg_attr(feature = "schema", schemars(bound = "T: ::schemars::JsonSchema + Default"))]
21pub struct BitemporalRecord<T = ()> {
22 pub id: RecordId,
25
26 pub valid_time: DateTime<Utc>,
29
30 pub recorded_time: DateTime<Utc>,
33
34 #[serde(default)]
39 pub value: T,
40}
41
42impl<T> BitemporalRecord<T> {
43 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> BitemporalRecord<U> {
45 BitemporalRecord {
46 id: self.id,
47 valid_time: self.valid_time,
48 recorded_time: self.recorded_time,
49 value: f(self.value),
50 }
51 }
52
53 pub fn id(&self) -> &str {
55 &self.id
56 }
57
58 pub fn valid_time(&self) -> DateTime<Utc> {
60 self.valid_time
61 }
62
63 pub fn recorded_time(&self) -> DateTime<Utc> {
65 self.recorded_time
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[cfg_attr(feature = "schema", derive(JsonSchema))]
72pub struct SupersessionTarget {
73 pub superseded_id: RecordId,
75 pub superseded_recorded_time: DateTime<Utc>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
84#[cfg_attr(feature = "schema", derive(JsonSchema))]
85pub struct SupersessionReceipt {
86 pub superseding_id: RecordId,
88 pub superseding_recorded_time: DateTime<Utc>,
90 pub superseded: SupersessionTarget,
92 pub superseding_digest: String,
94 pub superseded_digest: String,
96 pub receipt_digest: String,
98}
99
100impl SupersessionReceipt {
101 pub fn new<T>(superseded: BitemporalRecord<T>, superseding: BitemporalRecord<T>) -> Self
110 where
111 T: Serialize,
112 {
113 let superseded_digest = Self::digest_record(&superseded);
114 let superseding_digest = Self::digest_record(&superseding);
115
116 let receipt_content = format!(
117 "supersession:v1:{}:{}:{}:{}:{}:{}",
118 superseding.id,
119 superseding.recorded_time.timestamp(),
120 superseded.id,
121 superseded.recorded_time.timestamp(),
122 superseding_digest,
123 superseded_digest
124 );
125 let receipt_digest = format!("{:x}", Sha256::digest(receipt_content.as_bytes()));
126
127 Self {
128 superseding_id: superseding.id,
129 superseding_recorded_time: superseding.recorded_time,
130 superseded: SupersessionTarget {
131 superseded_id: superseded.id,
132 superseded_recorded_time: superseded.recorded_time,
133 },
134 superseding_digest,
135 superseded_digest,
136 receipt_digest,
137 }
138 }
139
140 fn digest_record<T: Serialize>(record: &BitemporalRecord<T>) -> String {
144 let value_json = serde_json::to_string(&record.value).unwrap_or_default();
145 let content = format!(
146 "record:v1:{}:{}:{}:{}",
147 record.id,
148 record.valid_time.timestamp(),
149 record.recorded_time.timestamp(),
150 value_json
151 );
152 format!("{:x}", Sha256::digest(content.as_bytes()))
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_record_temporal_fields() {
162 let now = Utc::now();
163 let record = BitemporalRecord::<String> {
164 id: "test1".to_string(),
165 valid_time: now,
166 recorded_time: now,
167 value: "hello".to_string(),
168 };
169 assert_eq!(record.id(), "test1");
170 assert_eq!(record.valid_time(), now);
171 assert_eq!(record.recorded_time(), now);
172 }
173
174 #[test]
175 fn test_supersession_receipt_digest() {
176 let now = Utc::now();
177 let superseded = BitemporalRecord {
178 id: "v1".to_string(),
179 valid_time: now,
180 recorded_time: now,
181 value: (),
182 };
183 let superseding = BitemporalRecord {
184 id: "v2".to_string(),
185 valid_time: now,
186 recorded_time: now,
187 value: (),
188 };
189 let receipt = SupersessionReceipt::new(superseded, superseding);
190 assert!(!receipt.receipt_digest.is_empty());
191 assert_eq!(receipt.superseded.superseded_id, "v1");
192 assert_eq!(receipt.superseding_id, "v2");
193 }
194}