use serde::Serialize;
use crate::config::{Badge, Colour};
use crate::model::types::Bead;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Badged {
pub key: String,
pub text: String,
pub link: Option<String>,
pub colour: Option<Colour>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "undrawn", rename_all = "kebab-case")]
pub enum Undrawn {
Badge { key: String },
Link { key: String },
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Badges {
pub drawn: Vec<Badged>,
pub undrawn: Vec<Undrawn>,
}
pub fn badges_for(bead: &Bead, badges: &[Badge]) -> Badges {
let mut drawn = Vec::new();
let mut undrawn = Vec::new();
for badge in badges {
let Some(value) = bead.metadata.get(&badge.key) else {
continue;
};
let promised = badge.link.is_some();
let Some(text) = badge.apply(value) else {
if promised {
undrawn.push(Undrawn::Badge {
key: badge.key.clone(),
});
}
continue;
};
let link = badge.link_for(value);
if promised && link.is_none() {
undrawn.push(Undrawn::Link {
key: badge.key.clone(),
});
}
drawn.push(Badged {
key: badge.key.clone(),
text,
link,
colour: badge.colour,
});
}
Badges { drawn, undrawn }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collect::bd::parse_beads;
use pretty_assertions::assert_eq;
use crate::config::Pattern;
fn matching(pattern: &str) -> Pattern {
Pattern::new(pattern).expect("the pattern compiles")
}
fn bead_with(metadata: &str) -> Bead {
let json =
format!(r#"[{{"id":"p-1","title":"root","status":"open","metadata":{metadata}}}]"#);
parse_beads(&json).expect("the bead parses").remove(0)
}
#[test]
fn badges_render_only_where_the_key_and_match_agree() {
let bead = bead_with(r#"{"blocked_on":"human","delivery_pr":"owner/repo#7"}"#);
let cfg = vec![
Badge {
key: "blocked_on".into(),
match_value: Some(matching("human")),
render: "waiting".into(),
link: None,
colour: None,
},
Badge {
key: "blocked_on".into(),
match_value: Some(matching("dependency")),
render: "dep".into(),
link: None,
colour: None,
},
Badge {
key: "absent_key".into(),
match_value: None,
render: "never".into(),
link: None,
colour: None,
},
];
let got = badges_for(&bead, &cfg);
assert_eq!(
got.drawn,
vec![Badged {
key: "blocked_on".to_string(),
text: "waiting".to_string(),
link: None,
colour: None,
}]
);
}
#[test]
fn badges_render_a_configured_key_without_interpreting_it() {
let bead = bead_with(r#"{"xyzzy":"plugh"}"#);
let cfg = vec![Badge {
key: "xyzzy".into(),
match_value: None,
render: "→ {}".into(),
link: None,
colour: None,
}];
let got = badges_for(&bead, &cfg);
assert_eq!(
got.drawn,
vec![Badged {
key: "xyzzy".to_string(),
text: "→ plugh".to_string(),
link: None,
colour: None,
}]
);
}
#[test]
fn a_badges_colour_travels_with_its_text() {
let bead = bead_with(r#"{"jira":"ATLAS-19"}"#);
let cfg = vec![Badge {
key: "jira".into(),
match_value: None,
render: "{}".into(),
link: None,
colour: Some(Colour::Status),
}];
let got = badges_for(&bead, &cfg);
assert_eq!(
got.drawn,
vec![Badged {
key: "jira".to_string(),
text: "ATLAS-19".to_string(),
link: None,
colour: Some(Colour::Status),
}]
);
}
#[test]
fn a_bead_with_no_configured_badges_renders_none() {
let bead = bead_with(r#"{"blocked_on":"human"}"#);
assert_eq!(badges_for(&bead, &[]), Badges::default());
}
fn qualified_only() -> Badge {
Badge {
key: "delivery_pr".into(),
match_value: Some(matching(
r"(?<owner>[^/]+)/(?<repo>[^#]+)#(?<number>[0-9]+)",
)),
render: "⇢ #{number}".into(),
link: Some("https://forge.invalid/{owner}/{repo}/pull/{number}".into()),
colour: None,
}
}
#[test]
fn a_badge_meant_to_draw_reports_a_value_its_pattern_cannot_read() {
let bead = bead_with(r#"{"delivery_pr":"30"}"#);
let got = badges_for(&bead, &[qualified_only()]);
assert_eq!(got.drawn, Vec::new());
assert_eq!(
got.undrawn,
vec![Undrawn::Badge {
key: "delivery_pr".to_string()
}]
);
}
#[test]
fn a_badge_configured_to_decline_stays_silent() {
let bead = bead_with(r#"{"blocked_on":"dependency"}"#);
let filter = Badge {
key: "blocked_on".into(),
match_value: Some(matching("human")),
render: "⏸ waiting".into(),
link: None,
colour: None,
};
let got = badges_for(&bead, &[filter]);
assert_eq!(got.drawn, Vec::new());
assert_eq!(got.undrawn, Vec::new());
}
#[test]
fn a_badge_whose_key_the_bead_does_not_carry_reports_nothing() {
let bead = bead_with(r#"{"blocked_on":"human"}"#);
let got = badges_for(&bead, &[qualified_only()]);
assert_eq!(got.drawn, Vec::new());
assert_eq!(got.undrawn, Vec::new());
}
#[test]
fn a_badge_reports_a_link_its_value_could_not_fill() {
let bead = bead_with(r#"{"delivery_pr":"30"}"#);
let either_form = Badge {
match_value: Some(matching(
r"(?:(?<owner>[^/]+)/(?<repo>[^#]+))?#?(?<number>[0-9]+)",
)),
..qualified_only()
};
let got = badges_for(&bead, &[either_form]);
assert_eq!(
got.drawn,
vec![Badged {
key: "delivery_pr".to_string(),
text: "⇢ #30".to_string(),
link: None,
colour: None,
}]
);
assert_eq!(
got.undrawn,
vec![Undrawn::Link {
key: "delivery_pr".to_string()
}]
);
}
#[test]
fn a_badge_that_draws_its_link_reports_nothing() {
let bead = bead_with(r#"{"delivery_pr":"orbital/atlas#30"}"#);
let got = badges_for(&bead, &[qualified_only()]);
assert_eq!(
got.drawn,
vec![Badged {
key: "delivery_pr".to_string(),
text: "⇢ #30".to_string(),
link: Some("https://forge.invalid/orbital/atlas/pull/30".to_string()),
colour: None,
}]
);
assert_eq!(got.undrawn, Vec::new());
}
}