use super::category::StatusCategory;
use super::value::Status;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusConfig {
pub next: Vec<String>,
pub active: bool,
pub terminal: bool,
pub label: Option<String>,
pub category: StatusCategory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusesConfig {
pub(super) entries: Vec<(String, StatusConfig)>,
pub(super) initial: String,
}
impl StatusesConfig {
pub fn new(entries: Vec<(String, StatusConfig)>, initial: String) -> Self {
StatusesConfig { entries, initial }
}
pub fn initial(&self) -> &str {
&self.initial
}
pub fn resolve(&self, name: &str) -> anyhow::Result<Status> {
self.entries
.iter()
.find(|(n, _)| n == name)
.map(|(n, cfg)| {
Status::from_parts(
n.clone(),
cfg.label.clone(),
cfg.category,
cfg.active,
cfg.terminal,
)
})
.ok_or_else(|| anyhow::anyhow!("unknown status '{name}': not in configuration"))
}
pub fn contains_name(&self, name: &str) -> bool {
self.entries.iter().any(|(n, _)| n == name)
}
pub fn contains(&self, status: &Status) -> bool {
self.contains_name(status.as_str())
}
pub fn next_for(&self, status: &Status) -> Option<&[String]> {
self.entries
.iter()
.find(|(n, _)| n == status.as_str())
.map(|(_, cfg)| cfg.next.as_slice())
}
pub fn status_names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().map(|(n, _)| n.as_str())
}
pub fn statuses(&self) -> impl Iterator<Item = Status> + '_ {
self.entries.iter().map(|(n, cfg)| {
Status::from_parts(
n.clone(),
cfg.label.clone(),
cfg.category,
cfg.active,
cfg.terminal,
)
})
}
pub fn into_entries(self) -> Vec<(String, StatusConfig)> {
self.entries
}
}
pub const DEFAULT_ISSUE_STATUSES: &[&str] = &["open", "in-progress", "closed"];
#[cfg(test)]
pub mod strategy {
use super::{StatusConfig, StatusesConfig};
use crate::domain::model::status::category::strategy::status_category;
use crate::domain::model::status::status_name::strategy::status_name;
use proptest::prelude::*;
pub fn status_config() -> impl Strategy<Value = StatusConfig> {
(
proptest::collection::vec(status_name().prop_map(|n| n.to_string()), 0..3),
any::<bool>(),
any::<bool>(),
proptest::option::of("[A-Za-z ]{1,20}"),
status_category(),
)
.prop_map(|(next, active, terminal, label, category)| StatusConfig {
next,
active,
terminal,
label,
category,
})
}
pub fn statuses_config() -> impl Strategy<Value = StatusesConfig> {
proptest::collection::vec(
(status_name().prop_map(|n| n.to_string()), status_config()),
1..5,
)
.prop_flat_map(|entries| {
let names: Vec<String> = entries.iter().map(|(n, _)| n.clone()).collect();
(Just(entries), proptest::sample::select(names))
.prop_map(|(entries, initial)| StatusesConfig::new(entries, initial))
})
}
}
#[cfg(test)]
mod tests {
use super::super::category::StatusCategory;
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn initial_is_always_resolvable(cfg in strategy::statuses_config()) {
prop_assert!(cfg.resolve(cfg.initial()).is_ok());
}
}
fn cfg() -> StatusesConfig {
StatusesConfig::default_issue()
}
#[test]
fn resolve_returns_enriched_status() {
let s = cfg().resolve("closed").unwrap();
assert_eq!(s.name, "closed");
assert_eq!(s.category, StatusCategory::Resolved);
assert!(s.terminal);
assert!(!s.active);
}
#[test]
fn resolve_unknown_returns_error() {
assert!(cfg().resolve("unknown-status").is_err());
}
#[test]
fn resolve_uses_configured_label() {
let c = StatusesConfig::new(
vec![(
"in-progress".to_string(),
StatusConfig {
next: vec![],
active: true,
terminal: false,
label: Some("In Progress".to_string()),
category: StatusCategory::Active,
},
)],
"in-progress".to_string(),
);
assert_eq!(c.resolve("in-progress").unwrap().label, "In Progress");
}
#[test]
fn contains_returns_true_for_known_status() {
let c = cfg();
let open = c.resolve("open").unwrap();
let closed = c.resolve("closed").unwrap();
assert!(c.contains(&open));
assert!(open.active);
assert!(!closed.active);
assert!(closed.terminal);
assert!(!open.terminal);
}
#[test]
fn next_for_returns_allowed_transitions() {
let c = StatusesConfig::default_issue();
let next = c.next_for(&c.resolve("open").unwrap()).unwrap();
assert!(next.iter().any(|s| s == "closed"));
assert!(!next.iter().any(|s| s == "open"));
}
#[test]
fn statuses_iterates_all() {
let names: Vec<String> = cfg().statuses().map(|s| s.name).collect();
assert!(names.contains(&"open".to_string()));
assert!(names.contains(&"in-progress".to_string()));
assert!(names.contains(&"closed".to_string()));
}
#[test]
fn default_statuses_are_all_resolvable() {
let c = cfg();
for s in DEFAULT_ISSUE_STATUSES {
assert!(c.resolve(s).is_ok(), "cannot resolve: {s}");
}
}
}