use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::manifest::Lineage;
use crate::{DocumentId, HashAlgorithm};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProvenanceRecord {
pub version: String,
pub document_id: DocumentId,
pub created: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creator: Option<CreatorInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lineage: Option<Lineage>,
pub merkle: MerkleInfo,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub timestamps: Vec<TimestampRecord>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub derived_from: Vec<DerivationRecord>,
}
impl ProvenanceRecord {
pub const VERSION: &'static str = "0.1";
#[must_use]
pub fn new(document_id: DocumentId, merkle: MerkleInfo) -> Self {
Self {
version: Self::VERSION.to_string(),
document_id,
created: Utc::now(),
creator: None,
lineage: None,
merkle,
timestamps: Vec::new(),
derived_from: Vec::new(),
}
}
#[must_use]
pub fn with_creator(mut self, creator: CreatorInfo) -> Self {
self.creator = Some(creator);
self
}
#[must_use]
pub fn with_lineage(mut self, lineage: Lineage) -> Self {
self.lineage = Some(lineage);
self
}
#[must_use]
pub fn with_timestamp(mut self, timestamp: TimestampRecord) -> Self {
self.timestamps.push(timestamp);
self
}
#[must_use]
pub fn with_derivation(mut self, derivation: DerivationRecord) -> Self {
self.derived_from.push(derivation);
self
}
pub fn to_json(&self) -> crate::Result<String> {
serde_json::to_string_pretty(self).map_err(Into::into)
}
pub fn from_json(json: &str) -> crate::Result<Self> {
serde_json::from_str(json).map_err(Into::into)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatorInfo {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub organization: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
}
impl CreatorInfo {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
email: None,
organization: None,
uri: None,
}
}
#[must_use]
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
#[must_use]
pub fn with_organization(mut self, org: impl Into<String>) -> Self {
self.organization = Some(org.into());
self
}
#[must_use]
pub fn with_uri(mut self, uri: impl Into<String>) -> Self {
self.uri = Some(uri.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MerkleInfo {
pub root: DocumentId,
pub block_count: usize,
pub algorithm: HashAlgorithm,
}
impl MerkleInfo {
#[must_use]
pub fn new(root: DocumentId, block_count: usize, algorithm: HashAlgorithm) -> Self {
Self {
root,
block_count,
algorithm,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimestampRecord {
pub method: TimestampMethod,
pub authority: String,
pub time: DateTime<Utc>,
pub token: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transaction_id: Option<String>,
}
impl TimestampRecord {
#[must_use]
pub fn rfc3161(
authority: impl Into<String>,
time: DateTime<Utc>,
token: impl Into<String>,
) -> Self {
Self {
method: TimestampMethod::Rfc3161,
authority: authority.into(),
time,
token: token.into(),
transaction_id: None,
}
}
#[must_use]
pub fn bitcoin(
time: DateTime<Utc>,
token: impl Into<String>,
tx_id: impl Into<String>,
) -> Self {
Self {
method: TimestampMethod::Bitcoin,
authority: "Bitcoin Mainnet".to_string(),
time,
token: token.into(),
transaction_id: Some(tx_id.into()),
}
}
#[must_use]
pub fn ethereum(
time: DateTime<Utc>,
token: impl Into<String>,
tx_id: impl Into<String>,
) -> Self {
Self {
method: TimestampMethod::Ethereum,
authority: "Ethereum Mainnet".to_string(),
time,
token: token.into(),
transaction_id: Some(tx_id.into()),
}
}
#[must_use]
pub fn open_timestamps(time: DateTime<Utc>, token: impl Into<String>) -> Self {
Self {
method: TimestampMethod::OpenTimestamps,
authority: "OpenTimestamps".to_string(),
time,
token: token.into(),
transaction_id: None,
}
}
#[must_use]
pub fn matches_document(&self, _document_id: &DocumentId) -> bool {
!self.token.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "lowercase")]
pub enum TimestampMethod {
#[strum(serialize = "RFC 3161")]
Rfc3161,
Bitcoin,
Ethereum,
OpenTimestamps,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DerivationRecord {
pub source: String,
pub derivation_type: DerivationType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
}
impl DerivationRecord {
#[must_use]
pub fn new(source: impl Into<String>, derivation_type: DerivationType) -> Self {
Self {
source: source.into(),
derivation_type,
description: None,
timestamp: None,
license: None,
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = Some(timestamp);
self
}
#[must_use]
pub fn with_license(mut self, license: impl Into<String>) -> Self {
self.license = Some(license.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "camelCase")]
pub enum DerivationType {
Quotation,
Paraphrase,
Translation,
Adaptation,
#[strum(serialize = "Based On")]
BasedOn,
Import,
}
#[cfg(test)]
mod tests {
use super::*;
fn test_hash() -> DocumentId {
"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
.parse()
.unwrap()
}
#[test]
fn test_provenance_record_creation() {
let merkle = MerkleInfo::new(test_hash(), 10, HashAlgorithm::Sha256);
let record = ProvenanceRecord::new(test_hash(), merkle);
assert_eq!(record.version, "0.1");
assert_eq!(record.merkle.block_count, 10);
assert!(record.timestamps.is_empty());
}
#[test]
fn test_provenance_record_with_creator() {
let merkle = MerkleInfo::new(test_hash(), 5, HashAlgorithm::Sha256);
let creator = CreatorInfo::new("Jane Doe")
.with_email("jane@example.com")
.with_organization("Acme Corp");
let record = ProvenanceRecord::new(test_hash(), merkle).with_creator(creator);
assert!(record.creator.is_some());
assert_eq!(record.creator.as_ref().unwrap().name, "Jane Doe");
}
#[test]
fn test_timestamp_record_rfc3161() {
let timestamp =
TimestampRecord::rfc3161("https://timestamp.example.com", Utc::now(), "base64token");
assert_eq!(timestamp.method, TimestampMethod::Rfc3161);
assert_eq!(timestamp.authority, "https://timestamp.example.com");
}
#[test]
fn test_timestamp_record_bitcoin() {
let timestamp = TimestampRecord::bitcoin(Utc::now(), "opreturn_data", "abc123def456");
assert_eq!(timestamp.method, TimestampMethod::Bitcoin);
assert!(timestamp.transaction_id.is_some());
}
#[test]
fn test_derivation_record() {
let derivation =
DerivationRecord::new("https://example.com/source", DerivationType::Quotation)
.with_description("Quote from chapter 3")
.with_license("CC-BY-4.0");
assert_eq!(derivation.derivation_type, DerivationType::Quotation);
assert!(derivation.description.is_some());
}
#[test]
fn test_provenance_record_serialization() {
let merkle = MerkleInfo::new(test_hash(), 3, HashAlgorithm::Sha256);
let record = ProvenanceRecord::new(test_hash(), merkle);
let json = record.to_json().unwrap();
assert!(json.contains("\"version\": \"0.1\""));
assert!(json.contains("\"blockCount\": 3"));
let deserialized = ProvenanceRecord::from_json(&json).unwrap();
assert_eq!(deserialized.merkle.block_count, 3);
}
#[test]
fn test_timestamp_method_display() {
assert_eq!(TimestampMethod::Rfc3161.to_string(), "RFC 3161");
assert_eq!(TimestampMethod::Bitcoin.to_string(), "Bitcoin");
}
#[test]
fn test_derivation_type_display() {
assert_eq!(DerivationType::Quotation.to_string(), "Quotation");
assert_eq!(DerivationType::Translation.to_string(), "Translation");
}
}