use std::marker::PhantomData; use std::ops::Deref; use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; use crate::cob::op::Op; use crate::cob::patch; use crate::cob::patch::Patch; use crate::cob::store::encoding; use crate::cob::{Entry, History, Manifest, Timestamp, Version}; use crate::crypto::Signer; use crate::git; use crate::git::ext::author::Author; use crate::git::ext::commit::headers::Headers; use crate::git::ext::commit::{trailers::OwnedTrailer, Commit}; use crate::git::Oid; use crate::prelude::Did; use crate::storage::ReadRepository; use crate::test::arbitrary; use super::store::FromHistory; use super::thread; /// Convenience type for building histories. #[derive(Debug, Clone, PartialEq, Eq)] pub struct HistoryBuilder { history: History, resource: Oid, time: Timestamp, witness: PhantomData, } impl AsRef for HistoryBuilder { fn as_ref(&self) -> &History { &self.history } } impl HistoryBuilder { pub fn comment( &mut self, body: impl ToString, reply_to: Option, signer: &G, ) -> Oid { let action = thread::Action::Comment { body: body.to_string(), reply_to, }; self.commit(&action, signer) } } impl HistoryBuilder where T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static, { pub fn new(action: &T::Action, time: Timestamp, signer: &G) -> HistoryBuilder { let resource = arbitrary::oid(); let (data, root) = encoded::(action, time, [], signer); let manifest = Manifest::new(T::type_name().clone(), Version::default()); Self { history: History::new_from_root( root, *signer.public_key(), resource, NonEmpty::new(data), time.as_secs(), manifest, ), time, resource, witness: PhantomData, } } pub fn root(&self) -> &Entry { self.history.root() } pub fn merge(&mut self, other: Self) { self.history.merge(other.history); } pub fn commit(&mut self, action: &T::Action, signer: &G) -> git::ext::Oid { let timestamp = self.time; let tips = self.tips(); let (data, oid) = encoded::(action, timestamp, tips, signer); let manifest = Manifest::new(T::type_name().clone(), Version::default()); self.history.extend( oid, *signer.public_key(), self.resource, NonEmpty::new(data), timestamp.as_secs(), manifest, ); oid } } impl Deref for HistoryBuilder { type Target = History; fn deref(&self) -> &Self::Target { &self.history } } /// Create a new test history. pub fn history( action: &T::Action, time: Timestamp, signer: &G, ) -> HistoryBuilder where T::Action: Serialize + Eq + 'static, { HistoryBuilder::new(action, time, signer) } /// An object that can be used to create and sign operations. pub struct Actor { pub signer: G, } impl Default for Actor { fn default() -> Self { Self::new(G::default()) } } impl Actor { pub fn new(signer: G) -> Self { Self { signer } } } impl Actor { /// Create a new operation. pub fn op_with( &mut self, action: T::Action, identity: Oid, timestamp: Timestamp, ) -> Op where T::Action: Clone + Serialize, { let data = encoding::encode(serde_json::json!({ "action": action, "nonce": fastrand::u64(..), })) .unwrap(); let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &data).unwrap(); let id = oid.into(); let author = *self.signer.public_key(); let actions = NonEmpty::new(action); let manifest = Manifest::new(T::type_name().clone(), Version::default()); Op { id, actions, author, timestamp, identity, manifest, } } /// Create a new operation. pub fn op(&mut self, action: T::Action) -> Op where T::Action: Clone + Serialize, { let identity = arbitrary::oid(); let timestamp = Timestamp::now(); self.op_with::(action, identity, timestamp) } /// Get the actor's DID. pub fn did(&self) -> Did { self.signer.public_key().into() } } impl Actor { /// Create a patch. pub fn patch( &mut self, title: impl ToString, description: impl ToString, base: git::Oid, oid: git::Oid, repo: &R, ) -> Result { Patch::from_ops( [ self.op::(patch::Action::Revision { description: description.to_string(), base, oid, resolves: Default::default(), }), self.op::(patch::Action::Edit { title: title.to_string(), target: patch::MergeTarget::default(), }), ], repo, ) } } /// Encode an action and return its hash. /// /// Doesn't encode in the same way as we do in production, but attempts to include the same data /// that feeds into the hash entropy, so that changing any input will change the resulting oid. pub fn encoded( action: &T::Action, timestamp: Timestamp, parents: impl IntoIterator, signer: &G, ) -> (Vec, git::ext::Oid) { let data = encoding::encode(action).unwrap(); let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &data).unwrap(); let parents = parents.into_iter().map(|o| *o); let author = Author { name: "radicle".to_owned(), email: signer.public_key().to_human(), time: git_ext::author::Time::new(timestamp.as_secs() as i64, 0), }; let commit = Commit::new::<_, _, OwnedTrailer>( oid, parents, author.clone(), author, Headers::new(), String::default(), [], ) .to_string(); let hash = git::raw::Oid::hash_object(git::raw::ObjectType::Commit, commit.as_bytes()).unwrap(); (data, hash.into()) }