pub mod cache; use std::collections::BTreeSet; use std::ops::Deref; use std::str::FromStr; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::cob; use crate::cob::common::{Author, Authorization, Label, Reaction, Timestamp, Uri}; use crate::cob::store::Transaction; use crate::cob::store::{Cob, CobAction}; use crate::cob::thread; use crate::cob::thread::{Comment, CommentId, Thread}; use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName}; use crate::crypto::Signer; use crate::identity::doc::DocError; use crate::prelude::{Did, Doc, ReadRepository, RepoId}; use crate::storage::{HasRepoId, RepositoryError, WriteRepository}; pub use cache::Cache; /// Issue operation. pub type Op = cob::Op; /// 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; /// Error updating or creating issues. #[derive(Error, Debug)] pub enum Error { /// Error loading the identity document. #[error("identity doc failed to load: {0}")] Doc(#[from] DocError), #[error("thread apply failed: {0}")] Thread(#[from] thread::Error), #[error("store: {0}")] Store(#[from] store::Error), /// Action not authorized. #[error("{0} not authorized to apply {1:?}")] NotAuthorized(ActorId, Action), /// Action not allowed. #[error("action is not allowed: {0}")] NotAllowed(EntryId), /// Title is invalid. #[error("invalid title: {0:?}")] InvalidTitle(String), /// The identity doc is missing. #[error("identity document missing")] MissingIdentity, /// General error initializing an issue. #[error("initialization failed: {0}")] Init(&'static str), /// Error decoding an operation. #[error("op decoding failed: {0}")] Op(#[from] op::OpEncodingError), #[error("failed to update issue {id} in cache: {err}")] CacheUpdate { id: IssueId, #[source] err: Box, }, #[error("failed to remove issue {id} from cache : {err}")] CacheRemove { id: IssueId, #[source] err: Box, }, #[error("failed to remove issues from cache: {err}")] CacheRemoveAll { #[source] err: Box, }, } /// Reason why an issue was closed. #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum CloseReason { Other, Solved, } impl std::fmt::Display for CloseReason { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let reason = match self { Self::Other => "unspecified", Self::Solved => "solved", }; write!(f, "{reason}") } } /// Issue state. #[derive(Debug, Default, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "status")] pub enum State { /// The issue is closed. Closed { reason: CloseReason }, /// The issue is open. #[default] Open, } impl std::fmt::Display for State { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Closed { .. } => write!(f, "closed"), Self::Open => write!(f, "open"), } } } impl State { pub fn lifecycle_message(self) -> String { match self { Self::Open => "Open issue".to_owned(), Self::Closed { .. } => "Close issue".to_owned(), } } } /// Issue state. Accumulates [`Action`]. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Issue { /// Actors assigned to this issue. pub(super) assignees: BTreeSet, /// Title of the issue. pub(super) title: String, /// Current state of the issue. pub(super) state: State, /// Associated labels. pub(super) labels: BTreeSet