use std::fmt::{self, Display}; use std::str::FromStr; use localtime::LocalTime; use serde::{Deserialize, Serialize}; use crate::git_ext::Oid; use crate::prelude::*; /// Timestamp used for COB operations. pub type Timestamp = LocalTime; /// Author. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 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 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 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) || 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<'de> serde::de::Visitor<'de> 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 { write!(f, "{}", self.0) } } impl From