1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Dependency {
11 pub issue_id: String,
13 pub depends_on_id: String,
15 #[serde(rename = "type")]
17 pub dep_type: DependencyType,
18 pub created_at: DateTime<Utc>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub created_by: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub metadata: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub thread_id: Option<String>,
29}
30
31impl Dependency {
32 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 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 pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
60 self.created_by = Some(creator.into());
61 self
62 }
63
64 pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
66 self.metadata = Some(metadata.into());
67 self
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
73#[serde(rename_all = "snake_case")]
74pub enum DependencyType {
75 #[default]
78 Blocks,
79 ParentChild,
81 ConditionalBlocks,
83 WaitsFor,
85
86 Related,
89 DiscoveredFrom,
91
92 RepliesTo,
95 RelatesTo,
97 Duplicates,
99 Supersedes,
101
102 AuthoredBy,
105 AssignedTo,
107 ApprovedBy,
109 Attests,
111
112 Tracks,
115 Until,
117 CausedBy,
119 Validates,
121 DelegatedFrom,
123}
124
125impl DependencyType {
126 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 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 pub fn check_cycles(&self) -> bool {
164 !matches!(self, DependencyType::RelatesTo)
166 }
167
168 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}