pub mod cache; use std::collections::BTreeSet; use std::ops::Deref; use std::str::FromStr; use std::sync::LazyLock; 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::{Comment, CommentId, Thread}; use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName}; use crate::cob::{thread, TitleError}; use crate::identity::doc::DocError; use crate::node::device::Device; use crate::node::NodeId; use crate::prelude::{Did, Doc, ReadRepository, RepoId}; use crate::storage; 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: LazyLock = LazyLock::new(|| FromStr::from_str("xyz.radicle.issue").expect("type name is valid")); /// Identifier for an issue. pub type IssueId = ObjectId; pub type IssueStream<'a> = cob::stream::Stream<'a, Action>; impl<'a> IssueStream<'a> { pub fn init(issue: IssueId, store: &'a storage::git::Repository) -> Self { let history = cob::stream::CobRange::new(&TYPENAME, &issue); Self::new(&store.backend, history, TYPENAME.clone()) } } /// 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), #[error("invalid issue title due to: {0}")] TitleError(#[from] TitleError), /// 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