rusty_beads/types/
status.rs

1//! Issue status definitions.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7/// The workflow state of an issue.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
9#[serde(rename_all = "snake_case")]
10pub enum Status {
11    /// Default state for new issues - work not yet started.
12    #[default]
13    Open,
14    /// Work is actively underway.
15    InProgress,
16    /// Issue is halted by dependencies.
17    Blocked,
18    /// Intentionally postponed.
19    Deferred,
20    /// Work completed.
21    Closed,
22    /// Soft-deleted issue (preserved for sync).
23    Tombstone,
24    /// Persistent bead remaining indefinitely open.
25    Pinned,
26    /// Work attached to an agent's hook.
27    Hooked,
28}
29
30impl Status {
31    /// Returns true if this status represents active work.
32    pub fn is_active(&self) -> bool {
33        matches!(self, Status::Open | Status::InProgress | Status::Blocked)
34    }
35
36    /// Returns true if this status represents completed or removed work.
37    pub fn is_terminal(&self) -> bool {
38        matches!(self, Status::Closed | Status::Tombstone)
39    }
40
41    /// Returns true if this status blocks dependent issues.
42    pub fn blocks_dependents(&self) -> bool {
43        !matches!(self, Status::Closed | Status::Tombstone)
44    }
45
46    /// All valid status values.
47    pub fn all() -> &'static [Status] {
48        &[
49            Status::Open,
50            Status::InProgress,
51            Status::Blocked,
52            Status::Deferred,
53            Status::Closed,
54            Status::Tombstone,
55            Status::Pinned,
56            Status::Hooked,
57        ]
58    }
59
60    /// Returns the string representation for database storage.
61    pub fn as_str(&self) -> &'static str {
62        match self {
63            Status::Open => "open",
64            Status::InProgress => "in_progress",
65            Status::Blocked => "blocked",
66            Status::Deferred => "deferred",
67            Status::Closed => "closed",
68            Status::Tombstone => "tombstone",
69            Status::Pinned => "pinned",
70            Status::Hooked => "hooked",
71        }
72    }
73}
74
75impl fmt::Display for Status {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(f, "{}", self.as_str())
78    }
79}
80
81impl FromStr for Status {
82    type Err = String;
83
84    fn from_str(s: &str) -> Result<Self, Self::Err> {
85        match s.to_lowercase().as_str() {
86            "open" => Ok(Status::Open),
87            "in_progress" | "in-progress" | "inprogress" => Ok(Status::InProgress),
88            "blocked" => Ok(Status::Blocked),
89            "deferred" => Ok(Status::Deferred),
90            "closed" => Ok(Status::Closed),
91            "tombstone" => Ok(Status::Tombstone),
92            "pinned" => Ok(Status::Pinned),
93            "hooked" => Ok(Status::Hooked),
94            _ => Err(format!("unknown status: {}", s)),
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_status_roundtrip() {
105        for status in Status::all() {
106            let s = status.as_str();
107            let parsed: Status = s.parse().unwrap();
108            assert_eq!(*status, parsed);
109        }
110    }
111
112    #[test]
113    fn test_status_serde() {
114        let status = Status::InProgress;
115        let json = serde_json::to_string(&status).unwrap();
116        assert_eq!(json, "\"in_progress\"");
117        let parsed: Status = serde_json::from_str(&json).unwrap();
118        assert_eq!(status, parsed);
119    }
120}