use serde::{Deserialize, Serialize}; use std::default::Default; use std::str::FromStr; /// Metadata about chunks. /// /// We manage three bits of metadata about chunks, in addition to its /// identifier: /// /// * for all chunks, a [SHA256][] checksum of the chunk content /// /// * for generation chunks, an indication that it is a generation /// chunk, and a timestamp for when making the generation snapshot /// ended /// /// There is no syntax or semantics imposed on the timestamp, but a /// client should probably use [ISO 8601][] representation. /// /// For HTTP, the metadata will be serialised as a JSON object, like this: /// /// ~~~json /// { /// "sha256": "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b", /// "generation": true, /// "ended": "2020-09-17T08:17:13+03:00" /// } /// ~~~ /// /// This module provides functions for serializing to and from JSON. /// The JSON doesn't have to include the fields for generations if /// they're not needed, although when serialized, they will always be /// there. /// /// After chunk metadata is created, it is immutable. /// /// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601 /// [SHA256]: https://en.wikipedia.org/wiki/SHA-2 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct ChunkMeta { sha256: String, // The remaining fields are Options so that JSON parsing doesn't // insist on them being there in the textual representation. generation: Option, ended: Option, } impl ChunkMeta { /// Create a new data chunk. /// /// Data chunks are not for generations. pub fn new(sha256: &str) -> Self { ChunkMeta { sha256: sha256.to_string(), generation: None, ended: None, } } /// Create a new generation chunk. pub fn new_generation(sha256: &str, ended: &str) -> Self { ChunkMeta { sha256: sha256.to_string(), generation: Some(true), ended: Some(ended.to_string()), } } /// Is this a generation chunk? pub fn is_generation(&self) -> bool { matches!(self.generation, Some(true)) } /// When did this generation end? pub fn ended(&self) -> Option<&str> { self.ended.as_deref() } /// SHA256 checksum of the content of the chunk. pub fn sha256(&self) -> &str { &self.sha256 } /// Serialize as JSON. pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap() } } impl FromStr for ChunkMeta { type Err = serde_json::error::Error; /// Parse a JSON representation metadata. fn from_str(s: &str) -> Result { serde_json::from_str(s) } } #[cfg(test)] mod test { use super::ChunkMeta; #[test] fn new_creates_data_chunk() { let meta = ChunkMeta::new("abcdef"); assert!(!meta.is_generation()); assert_eq!(meta.ended(), None); assert_eq!(meta.sha256(), "abcdef"); } #[test] fn new_generation_creates_generation_chunk() { let meta = ChunkMeta::new_generation("abcdef", "2020-09-17T08:17:13+03:00"); assert!(meta.is_generation()); assert_eq!(meta.ended(), Some("2020-09-17T08:17:13+03:00")); assert_eq!(meta.sha256(), "abcdef"); } #[test] fn data_chunk_from_json() { let meta: ChunkMeta = r#"{"sha256": "abcdef"}"#.parse().unwrap(); assert!(!meta.is_generation()); assert_eq!(meta.ended(), None); assert_eq!(meta.sha256(), "abcdef"); } #[test] fn generation_chunk_from_json() { let meta: ChunkMeta = r#"{"sha256": "abcdef", "generation": true, "ended": "2020-09-17T08:17:13+03:00"}"# .parse() .unwrap(); assert!(meta.is_generation()); assert_eq!(meta.ended(), Some("2020-09-17T08:17:13+03:00")); assert_eq!(meta.sha256(), "abcdef"); } #[test] fn json_roundtrip() { let meta = ChunkMeta::new_generation("abcdef", "2020-09-17T08:17:13+03:00"); let json = serde_json::to_string(&meta).unwrap(); let meta2 = serde_json::from_str(&json).unwrap(); assert_eq!(meta, meta2); } }