1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::manifest::Lineage;
10use crate::{DocumentId, HashAlgorithm};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct ProvenanceRecord {
23 pub version: String,
25
26 pub document_id: DocumentId,
28
29 pub created: DateTime<Utc>,
31
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub creator: Option<CreatorInfo>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub lineage: Option<Lineage>,
39
40 pub merkle: MerkleInfo,
42
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub timestamps: Vec<TimestampRecord>,
46
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub derived_from: Vec<DerivationRecord>,
50}
51
52impl ProvenanceRecord {
53 pub const VERSION: &'static str = "0.1";
55
56 #[must_use]
58 pub fn new(document_id: DocumentId, merkle: MerkleInfo) -> Self {
59 Self {
60 version: Self::VERSION.to_string(),
61 document_id,
62 created: Utc::now(),
63 creator: None,
64 lineage: None,
65 merkle,
66 timestamps: Vec::new(),
67 derived_from: Vec::new(),
68 }
69 }
70
71 #[must_use]
73 pub fn with_creator(mut self, creator: CreatorInfo) -> Self {
74 self.creator = Some(creator);
75 self
76 }
77
78 #[must_use]
80 pub fn with_lineage(mut self, lineage: Lineage) -> Self {
81 self.lineage = Some(lineage);
82 self
83 }
84
85 #[must_use]
87 pub fn with_timestamp(mut self, timestamp: TimestampRecord) -> Self {
88 self.timestamps.push(timestamp);
89 self
90 }
91
92 #[must_use]
94 pub fn with_derivation(mut self, derivation: DerivationRecord) -> Self {
95 self.derived_from.push(derivation);
96 self
97 }
98
99 pub fn to_json(&self) -> crate::Result<String> {
105 serde_json::to_string_pretty(self).map_err(Into::into)
106 }
107
108 pub fn from_json(json: &str) -> crate::Result<Self> {
114 serde_json::from_str(json).map_err(Into::into)
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct CreatorInfo {
122 pub name: String,
124
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub email: Option<String>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub organization: Option<String>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub uri: Option<String>,
136}
137
138impl CreatorInfo {
139 #[must_use]
141 pub fn new(name: impl Into<String>) -> Self {
142 Self {
143 name: name.into(),
144 email: None,
145 organization: None,
146 uri: None,
147 }
148 }
149
150 #[must_use]
152 pub fn with_email(mut self, email: impl Into<String>) -> Self {
153 self.email = Some(email.into());
154 self
155 }
156
157 #[must_use]
159 pub fn with_organization(mut self, org: impl Into<String>) -> Self {
160 self.organization = Some(org.into());
161 self
162 }
163
164 #[must_use]
166 pub fn with_uri(mut self, uri: impl Into<String>) -> Self {
167 self.uri = Some(uri.into());
168 self
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct MerkleInfo {
176 pub root: DocumentId,
178
179 pub block_count: usize,
181
182 pub algorithm: HashAlgorithm,
184}
185
186impl MerkleInfo {
187 #[must_use]
189 pub fn new(root: DocumentId, block_count: usize, algorithm: HashAlgorithm) -> Self {
190 Self {
191 root,
192 block_count,
193 algorithm,
194 }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct TimestampRecord {
202 pub method: TimestampMethod,
204
205 pub authority: String,
207
208 pub time: DateTime<Utc>,
210
211 pub token: String,
213
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub transaction_id: Option<String>,
217}
218
219impl TimestampRecord {
220 #[must_use]
222 pub fn rfc3161(
223 authority: impl Into<String>,
224 time: DateTime<Utc>,
225 token: impl Into<String>,
226 ) -> Self {
227 Self {
228 method: TimestampMethod::Rfc3161,
229 authority: authority.into(),
230 time,
231 token: token.into(),
232 transaction_id: None,
233 }
234 }
235
236 #[must_use]
238 pub fn bitcoin(
239 time: DateTime<Utc>,
240 token: impl Into<String>,
241 tx_id: impl Into<String>,
242 ) -> Self {
243 Self {
244 method: TimestampMethod::Bitcoin,
245 authority: "Bitcoin Mainnet".to_string(),
246 time,
247 token: token.into(),
248 transaction_id: Some(tx_id.into()),
249 }
250 }
251
252 #[must_use]
254 pub fn ethereum(
255 time: DateTime<Utc>,
256 token: impl Into<String>,
257 tx_id: impl Into<String>,
258 ) -> Self {
259 Self {
260 method: TimestampMethod::Ethereum,
261 authority: "Ethereum Mainnet".to_string(),
262 time,
263 token: token.into(),
264 transaction_id: Some(tx_id.into()),
265 }
266 }
267
268 #[must_use]
270 pub fn open_timestamps(time: DateTime<Utc>, token: impl Into<String>) -> Self {
271 Self {
272 method: TimestampMethod::OpenTimestamps,
273 authority: "OpenTimestamps".to_string(),
274 time,
275 token: token.into(),
276 transaction_id: None,
277 }
278 }
279
280 #[must_use]
286 pub fn matches_document(&self, _document_id: &DocumentId) -> bool {
287 !self.token.is_empty()
290 }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
295#[serde(rename_all = "lowercase")]
296pub enum TimestampMethod {
297 #[strum(serialize = "RFC 3161")]
299 Rfc3161,
300 Bitcoin,
302 Ethereum,
304 OpenTimestamps,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct DerivationRecord {
312 pub source: String,
314
315 pub derivation_type: DerivationType,
317
318 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub description: Option<String>,
321
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub timestamp: Option<DateTime<Utc>>,
325
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub license: Option<String>,
329}
330
331impl DerivationRecord {
332 #[must_use]
334 pub fn new(source: impl Into<String>, derivation_type: DerivationType) -> Self {
335 Self {
336 source: source.into(),
337 derivation_type,
338 description: None,
339 timestamp: None,
340 license: None,
341 }
342 }
343
344 #[must_use]
346 pub fn with_description(mut self, description: impl Into<String>) -> Self {
347 self.description = Some(description.into());
348 self
349 }
350
351 #[must_use]
353 pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
354 self.timestamp = Some(timestamp);
355 self
356 }
357
358 #[must_use]
360 pub fn with_license(mut self, license: impl Into<String>) -> Self {
361 self.license = Some(license.into());
362 self
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
368#[serde(rename_all = "camelCase")]
369pub enum DerivationType {
370 Quotation,
372 Paraphrase,
374 Translation,
376 Adaptation,
378 #[strum(serialize = "Based On")]
380 BasedOn,
381 Import,
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 fn test_hash() -> DocumentId {
390 "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
391 .parse()
392 .unwrap()
393 }
394
395 #[test]
396 fn test_provenance_record_creation() {
397 let merkle = MerkleInfo::new(test_hash(), 10, HashAlgorithm::Sha256);
398 let record = ProvenanceRecord::new(test_hash(), merkle);
399
400 assert_eq!(record.version, "0.1");
401 assert_eq!(record.merkle.block_count, 10);
402 assert!(record.timestamps.is_empty());
403 }
404
405 #[test]
406 fn test_provenance_record_with_creator() {
407 let merkle = MerkleInfo::new(test_hash(), 5, HashAlgorithm::Sha256);
408 let creator = CreatorInfo::new("Jane Doe")
409 .with_email("jane@example.com")
410 .with_organization("Acme Corp");
411
412 let record = ProvenanceRecord::new(test_hash(), merkle).with_creator(creator);
413
414 assert!(record.creator.is_some());
415 assert_eq!(record.creator.as_ref().unwrap().name, "Jane Doe");
416 }
417
418 #[test]
419 fn test_timestamp_record_rfc3161() {
420 let timestamp =
421 TimestampRecord::rfc3161("https://timestamp.example.com", Utc::now(), "base64token");
422
423 assert_eq!(timestamp.method, TimestampMethod::Rfc3161);
424 assert_eq!(timestamp.authority, "https://timestamp.example.com");
425 }
426
427 #[test]
428 fn test_timestamp_record_bitcoin() {
429 let timestamp = TimestampRecord::bitcoin(Utc::now(), "opreturn_data", "abc123def456");
430
431 assert_eq!(timestamp.method, TimestampMethod::Bitcoin);
432 assert!(timestamp.transaction_id.is_some());
433 }
434
435 #[test]
436 fn test_derivation_record() {
437 let derivation =
438 DerivationRecord::new("https://example.com/source", DerivationType::Quotation)
439 .with_description("Quote from chapter 3")
440 .with_license("CC-BY-4.0");
441
442 assert_eq!(derivation.derivation_type, DerivationType::Quotation);
443 assert!(derivation.description.is_some());
444 }
445
446 #[test]
447 fn test_provenance_record_serialization() {
448 let merkle = MerkleInfo::new(test_hash(), 3, HashAlgorithm::Sha256);
449 let record = ProvenanceRecord::new(test_hash(), merkle);
450
451 let json = record.to_json().unwrap();
452 assert!(json.contains("\"version\": \"0.1\""));
453 assert!(json.contains("\"blockCount\": 3"));
454
455 let deserialized = ProvenanceRecord::from_json(&json).unwrap();
456 assert_eq!(deserialized.merkle.block_count, 3);
457 }
458
459 #[test]
460 fn test_timestamp_method_display() {
461 assert_eq!(TimestampMethod::Rfc3161.to_string(), "RFC 3161");
462 assert_eq!(TimestampMethod::Bitcoin.to_string(), "Bitcoin");
463 }
464
465 #[test]
466 fn test_derivation_type_display() {
467 assert_eq!(DerivationType::Quotation.to_string(), "Quotation");
468 assert_eq!(DerivationType::Translation.to_string(), "Translation");
469 }
470}