use std::fmt;
use super::category::StatusCategory;
use super::status_name::StatusName;
#[derive(Debug, Clone, Eq)]
pub struct Status {
pub name: String,
pub label: String,
pub category: StatusCategory,
pub active: bool,
pub terminal: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StatusTransition {
Unchanged,
Changed { from: Status, to: Status },
}
impl Status {
pub fn as_str(&self) -> &str {
&self.name
}
pub fn transition_to(&self, target: &Status) -> StatusTransition {
if self == target {
StatusTransition::Unchanged
} else {
StatusTransition::Changed {
from: self.clone(),
to: target.clone(),
}
}
}
pub fn new(s: &str) -> anyhow::Result<Self> {
StatusName::new(s)?;
Ok(Self::unresolved(s))
}
pub fn unresolved(name: impl Into<String>) -> Self {
let name = name.into();
let label = name.clone();
Status {
name,
label,
category: StatusCategory::Unknown,
active: false,
terminal: false,
}
}
pub(crate) fn from_parts(
name: impl Into<String>,
label: Option<String>,
category: StatusCategory,
active: bool,
terminal: bool,
) -> Self {
let name = name.into();
let label = label.unwrap_or_else(|| name.clone());
Status {
name,
label,
category,
active,
terminal,
}
}
}
impl PartialEq for Status {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl PartialOrd for Status {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Status {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name.cmp(&other.name)
}
}
impl std::hash::Hash for Status {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.name)
}
}
impl serde::Serialize for Status {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.name)
}
}
#[cfg(test)]
pub mod strategy {
use super::super::config::StatusesConfig;
use super::Status;
use proptest::prelude::*;
pub fn status() -> impl Strategy<Value = Status> {
let cfg = StatusesConfig::default_issue();
prop_oneof![
Just(cfg.resolve("open").unwrap()),
Just(cfg.resolve("in-progress").unwrap()),
Just(cfg.resolve("closed").unwrap()),
]
}
pub fn issue_status() -> impl Strategy<Value = Status> {
let cfg = StatusesConfig::default_issue();
prop_oneof![
Just(cfg.resolve("open").unwrap()),
Just(cfg.resolve("in-progress").unwrap()),
Just(cfg.resolve("closed").unwrap()),
]
}
}
#[cfg(test)]
mod tests {
use super::super::category::StatusCategory;
use super::super::config::StatusesConfig;
use super::*;
use proptest::prelude::*;
fn issue_st(s: &str) -> Status {
StatusesConfig::default_issue().resolve(s).unwrap()
}
#[test]
fn status_display_roundtrips() {
assert_eq!(issue_st("open").to_string(), "open");
}
#[test]
fn status_as_str_returns_name() {
assert_eq!(issue_st("in-progress").as_str(), "in-progress");
}
#[test]
fn status_equality_based_on_name() {
assert_eq!(issue_st("open"), issue_st("open"));
}
#[test]
fn status_ordering_is_lexicographic() {
assert!(issue_st("closed") < issue_st("open"));
}
#[test]
fn status_carries_category() {
assert_eq!(issue_st("open").category, StatusCategory::Queued);
assert_eq!(issue_st("in-progress").category, StatusCategory::Active);
assert_eq!(issue_st("closed").category, StatusCategory::Resolved);
}
#[test]
fn status_carries_active_and_terminal() {
assert!(issue_st("open").active);
assert!(!issue_st("open").terminal);
assert!(!issue_st("closed").active);
assert!(issue_st("closed").terminal);
}
#[test]
fn status_carries_label_fallback_to_name() {
assert_eq!(issue_st("open").label, "open");
}
#[test]
fn unresolved_has_unknown_category() {
let s = Status::unresolved("custom");
assert_eq!(s.category, StatusCategory::Unknown);
assert!(!s.active);
assert!(!s.terminal);
}
proptest! {
#[test]
fn prop_resolved_status_roundtrips_name(s in strategy::status()) {
let cfg = StatusesConfig::default_issue();
let resolved = cfg.resolve(s.as_str()).unwrap();
prop_assert_eq!(resolved.name, s.name);
}
}
}