1use serde::{Deserialize, Serialize};
6
7use crate::actions::Via;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
11#[serde(tag = "kind", rename_all = "snake_case")]
12pub enum Origin {
13 Bug {
15 unit: String,
17 requirement: String,
19 task: String,
21 triad_sha256: String,
23 by: String,
25 via: Via,
27 at: u64,
29 },
30 Issue {
32 number: u64,
34 repo: Option<String>,
36 context_sha256: Option<String>,
38 },
39}
40
41impl Origin {
42 #[must_use]
44 pub fn describe(&self) -> String {
45 match self {
46 Self::Bug {
47 requirement,
48 by,
49 via,
50 ..
51 } => format!("bug {requirement}, recorded by {by} ({via})"),
52 Self::Issue { number, repo, .. } => match repo {
53 Some(r) => format!("issue {r}#{number}"),
54 None => format!("issue #{number}"),
55 },
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
64#[serde(rename_all = "snake_case")]
65pub enum Rung {
66 Quick,
68 Bug,
71 Feature,
73}
74
75impl Rung {
76 #[must_use]
78 pub const fn as_str(self) -> &'static str {
79 match self {
80 Self::Quick => "quick",
81 Self::Bug => "bug",
82 Self::Feature => "feature",
83 }
84 }
85}
86
87#[must_use]
91pub fn rung_of(unit: &str, standing: &[(String, Rung)]) -> Rung {
92 standing
93 .iter()
94 .find(|(name, _)| {
95 unit == name
96 || unit
97 .strip_prefix(name.as_str())
98 .is_some_and(|rest| rest.starts_with('/'))
99 })
100 .map_or(Rung::Feature, |(_, r)| *r)
101}
102
103#[must_use]
105pub fn subject(unit: &str, requirement: &str) -> String {
106 format!("{unit}/{requirement}")
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn origins_carry_their_kind_and_read_back() {
115 let o = Origin::Issue {
116 number: 42,
117 repo: Some("o/r".to_owned()),
118 context_sha256: None,
119 };
120 let v = serde_json::to_value(&o).unwrap_or_default();
121 assert_eq!(v["kind"], "issue");
122 assert_eq!(serde_json::from_value::<Origin>(v).ok(), Some(o.clone()));
123 assert_eq!(o.describe(), "issue o/r#42");
124 assert_eq!(subject("007-bugfixes", "FR-003"), "007-bugfixes/FR-003");
125 }
126
127 #[test]
128 fn every_rung_is_decided_by_the_standing_units() {
129 let standing = vec![
130 ("004-quick-changes".to_owned(), Rung::Quick),
131 ("007-bugfixes".to_owned(), Rung::Bug),
132 (".specify/bugs".to_owned(), Rung::Bug),
133 ];
134 assert_eq!(rung_of("004-quick-changes", &standing), Rung::Quick);
135 assert_eq!(rung_of("007-bugfixes", &standing), Rung::Bug);
136 assert_eq!(rung_of(".specify/bugs/empty-title", &standing), Rung::Bug);
137 assert_eq!(rung_of("007-bugfixes-extra", &standing), Rung::Feature);
138 assert_eq!(rung_of("001-greet", &standing), Rung::Feature);
139 assert_eq!(rung_of("001-greet", &[]), Rung::Feature);
140 assert_eq!(Rung::Quick.as_str(), "quick");
141 }
142}