From 12988da089bfd916e7eb6e11563a970a13825603 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Sun, 13 Nov 2022 17:43:37 +0100 Subject: [PATCH] Add `Patch`, `Issue`, `Label` COBs Signed-off-by: Alexis Sellier --- Cargo.lock | 89 +++ radicle/Cargo.toml | 2 + radicle/src/cob.rs | 9 + radicle/src/cob/doc.rs | 243 +++++++ radicle/src/cob/issue.rs | 766 ++++++++++++++++++++++ radicle/src/cob/label.rs | 176 ++++++ radicle/src/cob/patch.rs | 1082 ++++++++++++++++++++++++++++++++ radicle/src/cob/shared.rs | 384 ++++++++++++ radicle/src/cob/store.rs | 204 ++++++ radicle/src/cob/transaction.rs | 72 +++ radicle/src/cob/value.rs | 77 +++ radicle/src/profile.rs | 6 +- radicle/src/test.rs | 24 + 13 files changed, 3133 insertions(+), 1 deletion(-) create mode 100644 radicle/src/cob/doc.rs create mode 100644 radicle/src/cob/issue.rs create mode 100644 radicle/src/cob/label.rs create mode 100644 radicle/src/cob/patch.rs create mode 100644 radicle/src/cob/shared.rs create mode 100644 radicle/src/cob/store.rs create mode 100644 radicle/src/cob/transaction.rs create mode 100644 radicle/src/cob/value.rs diff --git a/Cargo.lock b/Cargo.lock index cfbb0907..e3c63597 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "automerge" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e97999332403b0685c203d4937730f72777829c8b6ed92d6f1ccba017f38e61" +dependencies = [ + "flate2", + "fxhash", + "hex", + "itertools", + "leb128", + "serde", + "sha2 0.10.6", + "smol_str", + "thiserror", + "tinyvec", + "tracing", + "uuid 0.8.2", +] + [[package]] name = "axum" version = "0.5.17" @@ -550,6 +570,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "elliptic-curve" version = "0.12.3" @@ -664,6 +690,15 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -807,6 +842,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -954,6 +995,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.4" @@ -998,6 +1048,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "lexopt" version = "0.2.1" @@ -1475,6 +1531,7 @@ dependencies = [ name = "radicle" version = "0.2.0" dependencies = [ + "automerge", "base64", "byteorder", "crossbeam-channel", @@ -1501,6 +1558,7 @@ dependencies = [ "sqlite", "tempfile", "thiserror", + "uuid 1.2.1", "zeroize", ] @@ -1681,6 +1739,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -1949,6 +2008,15 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smol_str" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7475118a28b7e3a2e157ce0131ba8c5526ea96e90ee601d9f6bb2e286a35ab44" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.4.7" @@ -2411,6 +2479,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.8", + "serde", +] + +[[package]] +name = "uuid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +dependencies = [ + "getrandom 0.2.8", + "rand 0.8.5", + "serde", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/radicle/Cargo.toml b/radicle/Cargo.toml index b86b517f..30179177 100644 --- a/radicle/Cargo.toml +++ b/radicle/Cargo.toml @@ -11,6 +11,7 @@ test = ["quickcheck"] sql = ["sqlite"] [dependencies] +automerge = { version = "0.1" } base64 = { version= "0.13" } byteorder = { version = "1.4" } crossbeam-channel = { version = "0.5.6" } @@ -30,6 +31,7 @@ sqlite = { version = "0.28.1", optional = true } nonempty = { version = "0.8.0", features = ["serialize"] } tempfile = { version = "3.3.0" } thiserror = { version = "1" } +uuid = { version = "1.1.2", features = ["v4", "fast-rng", "serde"] } zeroize = { version = "1.5.7" } [dependencies.git2] diff --git a/radicle/src/cob.rs b/radicle/src/cob.rs index c55080dd..53227e60 100644 --- a/radicle/src/cob.rs +++ b/radicle/src/cob.rs @@ -1,3 +1,12 @@ +pub mod doc; +pub mod issue; +pub mod label; +pub mod patch; +pub mod shared; +pub mod store; +pub mod transaction; +pub mod value; + pub use cob::{ identity, object::collaboration::error, CollaborativeObject, Create, Entry, History, ObjectId, TypeName, Update, diff --git a/radicle/src/cob/doc.rs b/radicle/src/cob/doc.rs new file mode 100644 index 00000000..44b52e4f --- /dev/null +++ b/radicle/src/cob/doc.rs @@ -0,0 +1,243 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::hash::Hash; +use std::ops::Deref; +use std::str::FromStr; + +use automerge::{Automerge, AutomergeError, ObjType}; + +use crate::cob::value::{FromValue, ValueError}; + +/// Error decoding a document. +#[derive(thiserror::Error, Debug)] +pub enum DocumentError { + #[error(transparent)] + Automerge(#[from] AutomergeError), + #[error("property '{0}' not found in object")] + PropertyNotFound(automerge::Prop), + #[error("error decoding property `{0}`")] + Property(String), + #[error("property '{0}' is not an object")] + NotAnObject(automerge::Prop), + #[error("Unexpected object type: expected `{expected}`, got `{actual}`")] + ObjectType { + expected: automerge::ObjType, + actual: automerge::ObjType, + }, + #[error("error decoding value: {0}")] + Value(#[from] ValueError), + #[error("list under `{0}` cannot be empty")] + EmptyList(&'static str), +} + +/// Automerge document decoder. +/// +/// Wraps a document, providing convenience functions. Derefs to the underlying doc. +#[derive(Copy, Clone)] +pub struct Document<'a> { + doc: &'a Automerge, +} + +impl<'a> Document<'a> { + /// Create a new document from an automerge document. + pub fn new(doc: &'a Automerge) -> Self { + Self { doc } + } + + /// Get the value of a property of an object. + pub fn get, P: Into, T: FromValue<'a>>( + &self, + id: O, + prop: P, + ) -> Result { + let prop = prop.into(); + let (val, _) = Document::get_raw(self, id, prop)?; + + T::from_value(val).map_err(DocumentError::from) + } + + /// Get an object's raw property. + pub fn get_raw, P: Into>( + &self, + id: O, + prop: P, + ) -> Result<(automerge::Value<'a>, automerge::ObjId), DocumentError> { + let prop = prop.into(); + + self.doc + .get(id.as_ref(), prop.clone())? + .ok_or(DocumentError::PropertyNotFound(prop)) + } + + /// Get the id of an object's property. + pub fn get_id, P: Into>( + &self, + id: O, + prop: P, + ) -> Result { + self.get_raw(id, prop).map(|(_, id)| id) + } + + /// Get a property using a lookup function. + pub fn lookup, P: Into>( + &self, + id: O, + prop: P, + lookup: fn(Document, &automerge::ObjId) -> Result, + ) -> Result { + let obj_id = self.get_id(&id, prop)?; + lookup(*self, &obj_id) + } + + /// Get a list-like value from an object. + pub fn list, P: Into>( + &self, + id: O, + prop: P, + item: fn(Document, &automerge::ObjId) -> Result, + ) -> Result, DocumentError> { + let prop = prop.into(); + let id = id.as_ref(); + + let Some((list, list_id)) = self.doc.get(id, prop.clone())? else { + return Err(DocumentError::PropertyNotFound(prop)); + }; + let Some(objtype) = list.to_objtype() else { + return Err(DocumentError::NotAnObject(prop)); + }; + if objtype != ObjType::List { + return Err(DocumentError::ObjectType { + expected: ObjType::List, + actual: objtype, + }); + } + + let mut objs: Vec = Vec::new(); + for i in 0..self.length(&list_id) { + let Some((_, item_id)) = self.doc.get(&list_id, i as usize)? else { + return Err(DocumentError::PropertyNotFound(prop)); + }; + let item = item(*self, &item_id)?; + + objs.push(item); + } + Ok(objs) + } + + /// Get a map-like value from an object. + pub fn map< + V: Default, + K: Hash + Eq + FromStr, + O: AsRef, + P: Into, + >( + &self, + id: O, + prop: P, + mut value: impl FnMut(&mut V), + ) -> Result, DocumentError> { + let prop = prop.into(); + let id = id.as_ref(); + + let Some((obj, obj_id)) = self.doc.get(id, prop.clone())? else { + return Err(DocumentError::PropertyNotFound(prop))?; + }; + let Some(objtype) = obj.to_objtype() else { + return Err(DocumentError::NotAnObject(prop)); + }; + if objtype != ObjType::Map { + return Err(DocumentError::ObjectType { + expected: ObjType::Map, + actual: objtype, + }); + } + + let mut map = HashMap::new(); + for key in self.doc.keys(&obj_id) { + let key = K::from_str(&key).map_err(|_| DocumentError::Property(key))?; + let val = map.entry(key).or_default(); + + value(val); + } + Ok(map) + } + + /// Get a folded value out of an object. + pub fn fold< + T: Default, + V: FromValue<'a> + fmt::Debug, + O: AsRef, + P: Into, + >( + &self, + id: O, + prop: P, + mut f: impl FnMut(&mut T, V), + ) -> Result { + let prop = prop.into(); + let id = id.as_ref(); + + let Some((obj, obj_id)) = self.doc.get(id, prop.clone())? else { + return Err(DocumentError::PropertyNotFound(prop)); + }; + let Some(objtype) = obj.to_objtype() else { + return Err(DocumentError::NotAnObject(prop)); + }; + if objtype != ObjType::List { + return Err(DocumentError::ObjectType { + expected: ObjType::List, + actual: objtype, + }); + } + + let mut acc = T::default(); + for i in 0..self.doc.length(&obj_id) { + let Some((item, _)) = self.doc.get(&obj_id, i as usize)? else { + return Err(DocumentError::PropertyNotFound(prop)); + }; + let val = V::from_value(item)?; + + f(&mut acc, val); + } + Ok(acc) + } + + /// Get the keys of a map-like property. + pub fn keys, P: Into>( + &self, + id: O, + prop: P, + ) -> Result, DocumentError> { + let prop = prop.into(); + let id = id.as_ref(); + + let Some((obj, obj_id)) = self.doc.get(id, prop.clone())? else { + return Err(DocumentError::PropertyNotFound(prop)); + }; + let Some(objtype) = obj.to_objtype() else { + return Err(DocumentError::NotAnObject(prop)); + }; + if objtype != ObjType::Map { + return Err(DocumentError::ObjectType { + expected: ObjType::Map, + actual: objtype, + }); + } + + let mut keys = HashSet::new(); + for key in self.doc.keys(&obj_id) { + let key = K::from_str(&key).map_err(|_| DocumentError::Property(key))?; + + keys.insert(key); + } + Ok(keys) + } +} + +impl<'a> Deref for Document<'a> { + type Target = Automerge; + + fn deref(&self) -> &Self::Target { + self.doc + } +} diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs new file mode 100644 index 00000000..c7a3a7ac --- /dev/null +++ b/radicle/src/cob/issue.rs @@ -0,0 +1,766 @@ +#![allow(clippy::large_enum_variant)] +use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; +use std::ops::ControlFlow; +use std::str::FromStr; + +use automerge::{Automerge, ObjType, ScalarValue, Value}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +use crate::cob::doc::{Document, DocumentError}; +use crate::cob::shared; +use crate::cob::shared::*; +use crate::cob::store::{Error, FromHistory, Store}; +use crate::cob::transaction::{Transaction, TransactionError}; +use crate::cob::value::{FromValue, ValueError}; +use crate::cob::{Contents, History, ObjectId, TypeName}; +use crate::prelude::*; + +/// Type name of an issue. +pub static TYPENAME: Lazy = + Lazy::new(|| FromStr::from_str("xyz.radicle.issue").expect("type name is valid")); + +/// Identifier for an issue. +pub type IssueId = ObjectId; + +/// Reason why an issue was closed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CloseReason { + Solved, + Other, +} + +/// Issue state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase", tag = "status")] +pub enum State { + /// The issue is open. + Open, + /// The issue is closed. + Closed { reason: CloseReason }, +} + +impl State { + fn lifecycle_message(self) -> String { + match self { + State::Open => "Open issue".to_owned(), + State::Closed { .. } => "Close issue".to_owned(), + } + } +} + +impl From for ScalarValue { + fn from(state: State) -> Self { + match state { + State::Open => ScalarValue::from("open"), + State::Closed { + reason: CloseReason::Solved, + } => ScalarValue::from("solved"), + State::Closed { + reason: CloseReason::Other, + } => ScalarValue::from("closed"), + } + } +} + +impl<'a> FromValue<'a> for State { + fn from_value(value: Value) -> Result { + let state = value.to_str().ok_or(ValueError::InvalidType)?; + + match state { + "open" => Ok(Self::Open), + "closed" => Ok(Self::Closed { + reason: CloseReason::Other, + }), + "solved" => Ok(Self::Closed { + reason: CloseReason::Solved, + }), + _ => Err(ValueError::InvalidValue(value.to_string())), + } + } +} + +/// An issue or "ticket". +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Issue { + pub author: Author, + pub title: String, + pub state: State, + pub comment: Comment, + pub discussion: Discussion, + pub labels: HashSet