use std::fmt; use std::fmt::Display; use std::ops::{Deref, Range}; use std::path::PathBuf; use std::str::FromStr; use base64::prelude::{Engine, BASE64_STANDARD}; use localtime::LocalTime; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::git::Oid; use crate::prelude::{Did, PublicKey}; /// Timestamp used for COB operations. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(transparent)] pub struct Timestamp(LocalTime); impl Timestamp { pub fn from_secs(secs: u64) -> Self { Self(LocalTime::from_secs(secs)) } } impl From for Timestamp { fn from(time: LocalTime) -> Self { Self(time) } } impl From for LocalTime { fn from(time: Timestamp) -> Self { time.0 } } impl Deref for Timestamp { type Target = LocalTime; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Error, Debug, PartialEq, Eq)] pub enum TitleError { #[error("empty title")] EmptyTitle, #[error("invalid characters in title")] InvalidTitle, } /// A `Title` is used for messages that are included in collaborative objects, /// such as patches, issues, and identity changes. /// /// A `Title`: /// - Must not be empty /// - Must not contain `\n` or `\r` characters /// - Will be trimmed of any preceding or following whitespace #[derive(Display, Deserialize, Serialize, PartialEq, Eq, Clone, Debug)] #[display(inner)] pub struct Title(String); impl Title { /// # Errors /// /// [`TitleError::EmptyTitle`]: the provided `title` was empty /// [`TitleError::InvalidTitle`]: the provided `title` contained invalid /// characters pub fn new(title: &str) -> Result { if title.contains('\n') || title.contains('\r') { return Err(TitleError::InvalidTitle); } let title = title.trim(); if title.is_empty() { Err(TitleError::EmptyTitle) } else { Ok(Self(title.into())) } } } impl AsRef for Title { fn as_ref(&self) -> &str { &self.0 } } impl FromStr for Title { type Err = TitleError; fn from_str(s: &str) -> Result { Self::new(s) } } impl TryFrom for Title { type Error = TitleError; fn try_from(value: String) -> Result { Self::new(&value) } } /// Author. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Author { pub id: Did, } impl Author { pub fn new(id: impl Into) -> Self { Self { id: id.into() } } pub fn id(&self) -> &Did { &self.id } pub fn public_key(&self) -> &PublicKey { self.id.as_key() } } impl Display for Author { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.id.fmt(f) } } impl From for Author { fn from(value: PublicKey) -> Self { Self::new(value) } } #[derive(thiserror::Error, Debug)] pub enum ReactionError { #[error("invalid reaction")] InvalidReaction, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone, Serialize)] #[serde(transparent)] pub struct Reaction { emoji: char, } impl Reaction { /// Create a new reaction from an emoji. pub fn new(emoji: char) -> Result { let val = emoji as u32; let emoticons = 0x1F600..=0x1F64F; let hearts = 0x1FA75..=0x1FA77; let body_update = 0x1FAC0..=0x1FAC6; let emoticons_update = 0x1FAE0..=0x1FAF8; let misc = 0x1F300..=0x1F5FF; // Miscellaneous Symbols and Pictographs let dingbats = 0x2700..=0x27BF; let supp = 0x1F900..=0x1F9FF; // Supplemental Symbols and Pictographs let transport = 0x1F680..=0x1F6FF; if emoticons.contains(&val) || hearts.contains(&val) || body_update.contains(&val) || emoticons_update.contains(&val) || misc.contains(&val) || dingbats.contains(&val) || supp.contains(&val) || transport.contains(&val) { Ok(Self { emoji }) } else { Err(ReactionError::InvalidReaction) } } /// Get the reaction emoji. pub fn emoji(&self) -> char { self.emoji } } impl<'de> Deserialize<'de> for Reaction { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct ReactionVisitor; impl serde::de::Visitor<'_> for ReactionVisitor { type Value = Reaction; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a reaction emoji") } fn visit_char(self, v: char) -> Result where E: serde::de::Error, { Reaction::new(v).map_err(|e| E::custom(e.to_string())) } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { Reaction::from_str(v).map_err(|e| E::custom(e.to_string())) } fn visit_string(self, v: String) -> Result where E: serde::de::Error, { Reaction::from_str(&v).map_err(|e| E::custom(e.to_string())) } } deserializer.deserialize_char(ReactionVisitor) } } impl FromStr for Reaction { type Err = ReactionError; fn from_str(s: &str) -> Result { let mut chars = s.chars(); let first = chars.next().ok_or(ReactionError::InvalidReaction)?; // Reactions should not consist of more than a single emoji. if chars.next().is_some() { return Err(ReactionError::InvalidReaction); } Reaction::new(first) } } #[derive(thiserror::Error, Debug)] pub enum LabelError { #[error("invalid tag name: `{0}`")] InvalidName(String), } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct Label(String); impl Label { pub fn new(name: impl ToString) -> Result { let name = name.to_string(); if name.chars().any(|c| c.is_whitespace()) || name.is_empty() { return Err(LabelError::InvalidName(name)); } Ok(Self(name)) } pub fn name(&self) -> &str { self.0.as_str() } } impl FromStr for Label { type Err = LabelError; fn from_str(s: &str) -> Result { Self::new(s) } } impl Display for Label { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl From