From 1cb89868305760a2c8ee536a5e34efeedcbe2c7b Mon Sep 17 00:00:00 2001 From: Slack Coder Date: Fri, 27 Jan 2023 09:12:37 -0500 Subject: [PATCH] Add Op ID string formatting To help users, create an Op ID string format of the form `/`. Signed-off-by: Slack Coder --- Cargo.lock | 1 + radicle-crdt/Cargo.toml | 1 + radicle-crdt/src/clock.rs | 39 ++++++++++++++++++++++++ radicle/src/cob/op.rs | 62 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 6539cfda..7eb7bcad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1886,6 +1886,7 @@ dependencies = [ "radicle-crypto", "serde", "tempfile", + "thiserror", ] [[package]] diff --git a/radicle-crdt/Cargo.toml b/radicle-crdt/Cargo.toml index a29b6147..01d46739 100644 --- a/radicle-crdt/Cargo.toml +++ b/radicle-crdt/Cargo.toml @@ -11,6 +11,7 @@ fastrand = { version = "1.8.0", optional = true } num-traits = { version = "0.2.15", default-features = false, features = ["std"] } qcheck = { version = "1", optional = true } serde = { version = "1" } +thiserror = { version = "1" } [dependencies.radicle-crypto] path = "../radicle-crypto" diff --git a/radicle-crdt/src/clock.rs b/radicle-crdt/src/clock.rs index 6a00fa03..9aa2a6af 100644 --- a/radicle-crdt/src/clock.rs +++ b/radicle-crdt/src/clock.rs @@ -1,8 +1,11 @@ +use std::fmt; +use std::str::FromStr; use std::time::SystemTime; use std::time::UNIX_EPOCH; use num_traits::Bounded; use serde::{Deserialize, Serialize}; +use thiserror::Error; use crate::ord::Max; use crate::Semilattice as _; @@ -45,6 +48,12 @@ impl Lamport { } } +impl fmt::Display for Lamport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.get()) + } +} + impl From for Lamport { fn from(counter: u64) -> Self { Self { @@ -53,6 +62,36 @@ impl From for Lamport { } } +impl From for u64 { + fn from(arg: Lamport) -> Self { + arg.get() + } +} + +/// Error decoding an operation from an entry. +#[derive(Error, Debug)] +pub enum LamportError { + #[error("invalid lamport clock value")] + Invalid, +} + +impl FromStr for Lamport { + type Err = LamportError; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl TryFrom<&str> for Lamport { + type Error = LamportError; + + fn try_from(s: &str) -> Result { + let v = s.parse::().map_err(|_| LamportError::Invalid)?; + Ok(v.into()) + } +} + impl Bounded for Lamport { fn min_value() -> Self { Self::from(u64::min_value()) diff --git a/radicle/src/cob/op.rs b/radicle/src/cob/op.rs index f1d2754b..495a6fb5 100644 --- a/radicle/src/cob/op.rs +++ b/radicle/src/cob/op.rs @@ -1,4 +1,7 @@ use std::collections::BTreeMap; +use std::fmt; +use std::str; +use std::str::FromStr; use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; @@ -34,6 +37,47 @@ impl OpId { } } +impl fmt::Display for OpId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.1, self.0) + } +} + +/// Error decoding an operation from an entry. +#[derive(Error, Debug)] +pub enum OpIdError { + #[error("cannot parse op id from empty string")] + Empty, + #[error("badly formatted op id")] + BadFormat, +} + +impl FromStr for OpId { + type Err = OpIdError; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl TryFrom<&str> for OpId { + type Error = OpIdError; + + fn try_from(s: &str) -> Result { + if s.is_empty() { + return Err(OpIdError::Empty); + } + + let Some((actor_id, clock)) = s.split_once('/') else { + return Err(OpIdError::BadFormat); + }; + Ok(Self( + Lamport::from_str(clock).map_err(|_| OpIdError::BadFormat)?, + ActorId::from_str(actor_id).map_err(|_| OpIdError::BadFormat)?, + )) + } +} + /// The author of an [`Op`]. pub type ActorId = PublicKey; @@ -182,3 +226,21 @@ impl Actor { op } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_opid_try_from_str() { + let str = "z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/12"; + let id = OpId::try_from(str).expect("Op ID parses string"); + assert_eq!(str, id.to_string(), "string conversion is consistent"); + + let str = ""; + assert!(OpId::try_from(str).is_err(), "empty strings are invalid"); + + let str = "jlkjfksgi"; + assert!(OpId::try_from(str).is_err(), "badly formatted string"); + } +}