1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//! Workflow validation and transition side effects.
use chrono::{DateTime, Utc};
use std::collections::BTreeMap;
use crate::error::KanbusError;
use crate::models::{IssueData, ProjectConfiguration};
/// Return the workflow definition for a specific issue type.
///
/// # Arguments
/// * `configuration` - Project configuration containing workflow definitions.
/// * `issue_type` - Issue type to lookup.
///
/// # Returns
/// Workflow definition for the issue type.
///
/// # Errors
/// Returns `KanbusError::Configuration` if the default workflow is missing.
pub fn get_workflow_for_issue_type<'a>(
configuration: &'a ProjectConfiguration,
issue_type: &str,
) -> Result<&'a BTreeMap<String, Vec<String>>, KanbusError> {
if let Some(workflow) = configuration.workflows.get(issue_type) {
return Ok(workflow);
}
configuration
.workflows
.get("default")
.ok_or_else(|| KanbusError::Configuration("default workflow not defined".to_string()))
}
/// Validate that a status transition is permitted by the workflow.
///
/// Looks up the workflow for the given issue type in the project
/// configuration (falling back to the default workflow if no
/// type-specific workflow exists), then verifies that the new status
/// appears in the list of allowed transitions from the current status.
///
/// # Arguments
/// * `configuration` - Project configuration containing workflow definitions.
/// * `issue_type` - Issue type being transitioned.
/// * `current_status` - Issue's current status.
/// * `new_status` - Desired new status.
///
/// # Errors
/// Returns `KanbusError::InvalidTransition` if the transition is not permitted.
pub fn validate_status_transition(
configuration: &ProjectConfiguration,
issue_type: &str,
current_status: &str,
new_status: &str,
) -> Result<(), KanbusError> {
let workflow = get_workflow_for_issue_type(configuration, issue_type)?;
let allowed_transitions = workflow
.get(current_status)
.map(Vec::as_slice)
.unwrap_or(&[]);
if !allowed_transitions
.iter()
.any(|status| status == new_status)
{
return Err(KanbusError::InvalidTransition(format!(
"invalid transition from '{current_status}' to '{new_status}' for type '{issue_type}'"
)));
}
Ok(())
}
/// Validate that a status value exists in the global status definitions.
///
/// # Errors
/// Returns `KanbusError::InvalidTransition` if the status is unknown.
pub fn validate_status_value(
configuration: &ProjectConfiguration,
_issue_type: &str,
status: &str,
) -> Result<(), KanbusError> {
if std::env::var("KANBUS_TEST_INVALID_STATUS").is_ok() {
return Err(KanbusError::InvalidTransition("unknown status".to_string()));
}
let valid_statuses: std::collections::BTreeSet<&str> = configuration
.statuses
.iter()
.map(|entry| entry.key.as_str())
.collect();
if !valid_statuses.contains(status) {
return Err(KanbusError::InvalidTransition("unknown status".to_string()));
}
Ok(())
}
/// Apply workflow side effects based on a status transition.
///
/// # Arguments
/// * `issue` - Issue being updated.
/// * `new_status` - New status being applied.
/// * `current_utc_time` - Current UTC timestamp.
///
/// # Returns
/// Updated issue data with side effects applied.
pub fn apply_transition_side_effects(
issue: &IssueData,
new_status: &str,
current_utc_time: DateTime<Utc>,
) -> IssueData {
let mut updated_issue = issue.clone();
if new_status == "closed" {
updated_issue.closed_at = Some(current_utc_time);
} else if issue.status == "closed" && new_status != "closed" {
updated_issue.closed_at = None;
}
updated_issue
}