use knot_git::CommitRange; use knot_types::{AccountDid, Oid, RecordBody, RecordBodyError, Tid, UnixSeconds}; use serde::{Serialize, Serializer}; use crate::{CreatedAt, RecordError, encoded}; pub const PATCH_COLLECTION: &str = "org.tangled.review.patch"; collection!( patch_collection, PATCH_COLLECTION, "Patch collection is valid nsid" ); #[derive(Debug, Clone, PartialEq, Eq)] pub struct PatchRecord { ticket: Tid, range: CommitRange, created_at: CreatedAt, editor: AccountDid, } impl PatchRecord { pub fn new( ticket: Tid, range: CommitRange, created_at: UnixSeconds, editor: AccountDid, ) -> Result { Ok(Self { ticket, range, created_at: CreatedAt::at(created_at)?, editor, }) } pub fn body(&self) -> Result { encoded(self) } } #[derive(Serialize)] struct OidEncoding<'a> { oid: &'a Oid, } #[derive(Serialize)] struct PatchEncoding<'a> { #[serde(rename = "$type")] record_type: knot_types::RecordCollection, ticket: &'a Tid, base: OidEncoding<'a>, head: OidEncoding<'a>, #[serde(rename = "createdAt")] created_at: &'a CreatedAt, #[serde(rename = "x-tngl-editor")] editor: &'a AccountDid, } impl Serialize for PatchRecord { fn serialize(&self, serializer: S) -> Result { PatchEncoding { record_type: patch_collection(), ticket: &self.ticket, base: OidEncoding { oid: &self.range.base, }, head: OidEncoding { oid: &self.range.head, }, created_at: &self.created_at, editor: &self.editor, } .serialize(serializer) } } #[cfg(test)] mod tests { use super::*; use crate::frozen; const PATCH: &str = "a66462617365a1636f69647828616161616161616161616161616161616161616161616161616161616161616161616161616161616468656164a1636f696478286262626262626262626262626262626262626262626262626262626262626262626262626262626265247479706578186f72672e74616e676c65642e7265766965772e7061746368667469636b65746d336c75627270747835376432326963726561746564417474313937302d30312d30315430303a30303a30315a6d782d746e676c2d656469746f7278206469643a706c633a3366776563646e76746373636a6e72783270346e37616c7a"; fn editor() -> AccountDid { AccountDid::new("did:plc:3fwecdnvtcscjnrx2p4n7alz").unwrap() } fn oid(byte: u8) -> Oid { Oid::from_hex(&(byte as char).to_string().repeat(40)).unwrap() } fn range() -> CommitRange { CommitRange { base: oid(b'a'), head: oid(b'b'), } } #[test] fn patch_record_bytes_frozen() { let record = PatchRecord::new( Tid::new("3lubrptx57d22").unwrap(), range(), UnixSeconds::new(1), editor(), ) .unwrap(); let body = record.body().unwrap(); assert_eq!( body.as_bytes(), frozen(PATCH).as_slice(), "the first emitted patch must freeze these bytes: whole seconds, utc z" ); } #[test] fn patch_embeds_ticket_and_oid_pair() { let record = PatchRecord::new( Tid::new("3lubrptx57d22").unwrap(), range(), UnixSeconds::new(1), editor(), ) .unwrap(); let value: serde_json::Value = serde_ipld_dagcbor::from_slice(record.body().unwrap().as_bytes()).unwrap(); assert_eq!(value["$type"], PATCH_COLLECTION); assert_eq!(value["ticket"], "3lubrptx57d22"); assert_eq!(value["base"]["oid"], "a".repeat(40)); assert_eq!(value["head"]["oid"], "b".repeat(40)); assert_eq!(value["x-tngl-editor"], editor().as_str()); } }