#[cfg(feature = "schema")]
use schemars::JsonSchema;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub type RecordId = String;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(bound = "T: ::schemars::JsonSchema + Default"))]
pub struct BitemporalRecord<T = ()> {
pub id: RecordId,
pub valid_time: DateTime<Utc>,
pub recorded_time: DateTime<Utc>,
#[serde(default)]
pub value: T,
}
impl<T> BitemporalRecord<T> {
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> BitemporalRecord<U> {
BitemporalRecord {
id: self.id,
valid_time: self.valid_time,
recorded_time: self.recorded_time,
value: f(self.value),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn valid_time(&self) -> DateTime<Utc> {
self.valid_time
}
pub fn recorded_time(&self) -> DateTime<Utc> {
self.recorded_time
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct SupersessionTarget {
pub superseded_id: RecordId,
pub superseded_recorded_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct SupersessionReceipt {
pub superseding_id: RecordId,
pub superseding_recorded_time: DateTime<Utc>,
pub superseded: SupersessionTarget,
pub superseding_digest: String,
pub superseded_digest: String,
pub receipt_digest: String,
}
impl SupersessionReceipt {
pub fn new<T>(superseded: BitemporalRecord<T>, superseding: BitemporalRecord<T>) -> Self
where
T: Serialize,
{
let superseded_digest = Self::digest_record(&superseded);
let superseding_digest = Self::digest_record(&superseding);
let receipt_content = format!(
"supersession:v1:{}:{}:{}:{}:{}:{}",
superseding.id,
superseding.recorded_time.timestamp(),
superseded.id,
superseded.recorded_time.timestamp(),
superseding_digest,
superseded_digest
);
let receipt_digest = format!("{:x}", Sha256::digest(receipt_content.as_bytes()));
Self {
superseding_id: superseding.id,
superseding_recorded_time: superseding.recorded_time,
superseded: SupersessionTarget {
superseded_id: superseded.id,
superseded_recorded_time: superseded.recorded_time,
},
superseding_digest,
superseded_digest,
receipt_digest,
}
}
fn digest_record<T: Serialize>(record: &BitemporalRecord<T>) -> String {
let value_json = serde_json::to_string(&record.value).unwrap_or_default();
let content = format!(
"record:v1:{}:{}:{}:{}",
record.id,
record.valid_time.timestamp(),
record.recorded_time.timestamp(),
value_json
);
format!("{:x}", Sha256::digest(content.as_bytes()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_record_temporal_fields() {
let now = Utc::now();
let record = BitemporalRecord::<String> {
id: "test1".to_string(),
valid_time: now,
recorded_time: now,
value: "hello".to_string(),
};
assert_eq!(record.id(), "test1");
assert_eq!(record.valid_time(), now);
assert_eq!(record.recorded_time(), now);
}
#[test]
fn test_supersession_receipt_digest() {
let now = Utc::now();
let superseded = BitemporalRecord {
id: "v1".to_string(),
valid_time: now,
recorded_time: now,
value: (),
};
let superseding = BitemporalRecord {
id: "v2".to_string(),
valid_time: now,
recorded_time: now,
value: (),
};
let receipt = SupersessionReceipt::new(superseded, superseding);
assert!(!receipt.receipt_digest.is_empty());
assert_eq!(receipt.superseded.superseded_id, "v1");
assert_eq!(receipt.superseding_id, "v2");
}
}