rusty_beads/types/
dependency.rs

1//! Dependency type definitions.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8/// A relationship between two issues.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Dependency {
11    /// The issue that depends on another.
12    pub issue_id: String,
13    /// The issue being depended upon.
14    pub depends_on_id: String,
15    /// The type of dependency relationship.
16    #[serde(rename = "type")]
17    pub dep_type: DependencyType,
18    /// When the dependency was created.
19    pub created_at: DateTime<Utc>,
20    /// Who created the dependency.
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub created_by: Option<String>,
23    /// Type-specific metadata (JSON).
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub metadata: Option<String>,
26    /// Thread ID for conversation threading.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub thread_id: Option<String>,
29}
30
31impl Dependency {
32    /// Create a new blocking dependency.
33    pub fn blocks(issue_id: impl Into<String>, depends_on_id: impl Into<String>) -> Self {
34        Self {
35            issue_id: issue_id.into(),
36            depends_on_id: depends_on_id.into(),
37            dep_type: DependencyType::Blocks,
38            created_at: Utc::now(),
39            created_by: None,
40            metadata: None,
41            thread_id: None,
42        }
43    }
44
45    /// Create a parent-child relationship.
46    pub fn parent_child(child_id: impl Into<String>, parent_id: impl Into<String>) -> Self {
47        Self {
48            issue_id: child_id.into(),
49            depends_on_id: parent_id.into(),
50            dep_type: DependencyType::ParentChild,
51            created_at: Utc::now(),
52            created_by: None,
53            metadata: None,
54            thread_id: None,
55        }
56    }
57
58    /// Set the creator of this dependency.
59    pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
60        self.created_by = Some(creator.into());
61        self
62    }
63
64    /// Set metadata for this dependency.
65    pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
66        self.metadata = Some(metadata.into());
67        self
68    }
69}
70
71/// The type of relationship between issues.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
73#[serde(rename_all = "snake_case")]
74pub enum DependencyType {
75    // Workflow types (affect ready work calculation)
76    /// Blocks downstream work.
77    #[default]
78    Blocks,
79    /// Hierarchical parent-child relationship.
80    ParentChild,
81    /// Conditional blocking.
82    ConditionalBlocks,
83    /// Async coordination wait.
84    WaitsFor,
85
86    // Association types (non-blocking)
87    /// Generally related issues.
88    Related,
89    /// Source of discovery.
90    DiscoveredFrom,
91
92    // Graph link types
93    /// Comment thread connection.
94    RepliesTo,
95    /// Topical relationship.
96    RelatesTo,
97    /// Duplicate issues.
98    Duplicates,
99    /// Replacement relationship.
100    Supersedes,
101
102    // Entity types (HOP foundation)
103    /// Creator attribution.
104    AuthoredBy,
105    /// Work assignment.
106    AssignedTo,
107    /// Approval chain.
108    ApprovedBy,
109    /// Skill attestation.
110    Attests,
111
112    // Cross-project references
113    /// Tracks external issue.
114    Tracks,
115    /// Depends until condition.
116    Until,
117    /// Caused by external event.
118    CausedBy,
119    /// Validates work.
120    Validates,
121    /// Delegated from another issue.
122    DelegatedFrom,
123}
124
125impl DependencyType {
126    /// All valid dependency types.
127    pub fn all() -> &'static [DependencyType] {
128        &[
129            DependencyType::Blocks,
130            DependencyType::ParentChild,
131            DependencyType::ConditionalBlocks,
132            DependencyType::WaitsFor,
133            DependencyType::Related,
134            DependencyType::DiscoveredFrom,
135            DependencyType::RepliesTo,
136            DependencyType::RelatesTo,
137            DependencyType::Duplicates,
138            DependencyType::Supersedes,
139            DependencyType::AuthoredBy,
140            DependencyType::AssignedTo,
141            DependencyType::ApprovedBy,
142            DependencyType::Attests,
143            DependencyType::Tracks,
144            DependencyType::Until,
145            DependencyType::CausedBy,
146            DependencyType::Validates,
147            DependencyType::DelegatedFrom,
148        ]
149    }
150
151    /// Returns true if this dependency type blocks work.
152    pub fn is_blocking(&self) -> bool {
153        matches!(
154            self,
155            DependencyType::Blocks
156                | DependencyType::ParentChild
157                | DependencyType::ConditionalBlocks
158                | DependencyType::WaitsFor
159        )
160    }
161
162    /// Returns true if cycles should be checked for this type.
163    pub fn check_cycles(&self) -> bool {
164        // RelatesTo is symmetric so cycles don't matter
165        !matches!(self, DependencyType::RelatesTo)
166    }
167
168    /// Returns the string representation for database storage.
169    pub fn as_str(&self) -> &'static str {
170        match self {
171            DependencyType::Blocks => "blocks",
172            DependencyType::ParentChild => "parent_child",
173            DependencyType::ConditionalBlocks => "conditional_blocks",
174            DependencyType::WaitsFor => "waits_for",
175            DependencyType::Related => "related",
176            DependencyType::DiscoveredFrom => "discovered_from",
177            DependencyType::RepliesTo => "replies_to",
178            DependencyType::RelatesTo => "relates_to",
179            DependencyType::Duplicates => "duplicates",
180            DependencyType::Supersedes => "supersedes",
181            DependencyType::AuthoredBy => "authored_by",
182            DependencyType::AssignedTo => "assigned_to",
183            DependencyType::ApprovedBy => "approved_by",
184            DependencyType::Attests => "attests",
185            DependencyType::Tracks => "tracks",
186            DependencyType::Until => "until",
187            DependencyType::CausedBy => "caused_by",
188            DependencyType::Validates => "validates",
189            DependencyType::DelegatedFrom => "delegated_from",
190        }
191    }
192}
193
194impl fmt::Display for DependencyType {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        write!(f, "{}", self.as_str())
197    }
198}
199
200impl FromStr for DependencyType {
201    type Err = String;
202
203    fn from_str(s: &str) -> Result<Self, Self::Err> {
204        match s.to_lowercase().replace('-', "_").as_str() {
205            "blocks" => Ok(DependencyType::Blocks),
206            "parent_child" => Ok(DependencyType::ParentChild),
207            "conditional_blocks" => Ok(DependencyType::ConditionalBlocks),
208            "waits_for" => Ok(DependencyType::WaitsFor),
209            "related" => Ok(DependencyType::Related),
210            "discovered_from" => Ok(DependencyType::DiscoveredFrom),
211            "replies_to" => Ok(DependencyType::RepliesTo),
212            "relates_to" => Ok(DependencyType::RelatesTo),
213            "duplicates" => Ok(DependencyType::Duplicates),
214            "supersedes" => Ok(DependencyType::Supersedes),
215            "authored_by" => Ok(DependencyType::AuthoredBy),
216            "assigned_to" => Ok(DependencyType::AssignedTo),
217            "approved_by" => Ok(DependencyType::ApprovedBy),
218            "attests" => Ok(DependencyType::Attests),
219            "tracks" => Ok(DependencyType::Tracks),
220            "until" => Ok(DependencyType::Until),
221            "caused_by" => Ok(DependencyType::CausedBy),
222            "validates" => Ok(DependencyType::Validates),
223            "delegated_from" => Ok(DependencyType::DelegatedFrom),
224            _ => Err(format!("unknown dependency type: {}", s)),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_dependency_type_roundtrip() {
235        for dep_type in DependencyType::all() {
236            let s = dep_type.as_str();
237            let parsed: DependencyType = s.parse().unwrap();
238            assert_eq!(*dep_type, parsed);
239        }
240    }
241
242    #[test]
243    fn test_blocking_types() {
244        assert!(DependencyType::Blocks.is_blocking());
245        assert!(DependencyType::ParentChild.is_blocking());
246        assert!(!DependencyType::RelatesTo.is_blocking());
247        assert!(!DependencyType::Related.is_blocking());
248    }
249}