Skip to main content

vex_workstream/
lib.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use vex_domain::{ProjectId, WorkstreamId, WorkstreamState, validate_slug};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Workstream {
8    pub id: WorkstreamId,
9    pub project_id: ProjectId,
10    pub name: String,
11    pub branch: String,
12    pub state: WorkstreamState,
13    pub created_at: DateTime<Utc>,
14    pub from_pr: Option<String>,
15}
16
17#[derive(Debug, Error)]
18pub enum WorkstreamError {
19    #[error("workstream not found: {0}")]
20    NotFound(String),
21    #[error("workstream already exists: {0}")]
22    AlreadyExists(String),
23    #[error(
24        "invalid workstream name '{0}': must be alphanumeric with hyphens, starting with alphanumeric"
25    )]
26    InvalidName(String),
27    #[error("invalid state transition: {from} -> {to}")]
28    InvalidTransition {
29        from: WorkstreamState,
30        to: WorkstreamState,
31    },
32    #[error("project error: {0}")]
33    ProjectError(String),
34    #[error("git error: {0}")]
35    GitError(String),
36    #[error("IO error: {0}")]
37    Io(#[from] std::io::Error),
38    #[error("{0}")]
39    Other(String),
40}
41
42pub trait WorkstreamRepo {
43    fn create(&self, workstream: &Workstream) -> Result<(), WorkstreamError>;
44    fn find_by_name(
45        &self,
46        project_id: &ProjectId,
47        name: &str,
48    ) -> Result<Option<Workstream>, WorkstreamError>;
49    fn list_by_project(&self, project_id: &ProjectId) -> Result<Vec<Workstream>, WorkstreamError>;
50    fn update(&self, workstream: &Workstream) -> Result<(), WorkstreamError>;
51    fn delete(&self, project_id: &ProjectId, id: &WorkstreamId) -> Result<(), WorkstreamError>;
52}
53
54pub fn validate_workstream_name(name: &str) -> Result<(), WorkstreamError> {
55    if !validate_slug(name) {
56        return Err(WorkstreamError::InvalidName(name.into()));
57    }
58    Ok(())
59}
60
61pub fn branch_name_for_workstream(name: &str) -> String {
62    format!("task/{name}")
63}
64
65pub fn validate_transition(
66    from: WorkstreamState,
67    to: WorkstreamState,
68) -> Result<(), WorkstreamError> {
69    match (from, to) {
70        (WorkstreamState::Active, WorkstreamState::Paused)
71        | (WorkstreamState::Active, WorkstreamState::Completed)
72        | (WorkstreamState::Paused, WorkstreamState::Active)
73        | (WorkstreamState::Completed, WorkstreamState::Archived) => Ok(()),
74        _ => Err(WorkstreamError::InvalidTransition { from, to }),
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn valid_workstream_names() {
84        assert!(validate_workstream_name("fix-login").is_ok());
85        assert!(validate_workstream_name("feature-1").is_ok());
86        assert!(validate_workstream_name("a").is_ok());
87    }
88
89    #[test]
90    fn invalid_workstream_names() {
91        assert!(validate_workstream_name("").is_err());
92        assert!(validate_workstream_name("-bad").is_err());
93        assert!(validate_workstream_name("has spaces").is_err());
94        assert!(validate_workstream_name("under_score").is_err());
95    }
96
97    #[test]
98    fn branch_name_generation() {
99        assert_eq!(branch_name_for_workstream("fix-login"), "task/fix-login");
100    }
101
102    #[test]
103    fn valid_transitions() {
104        use WorkstreamState::*;
105        assert!(validate_transition(Active, Paused).is_ok());
106        assert!(validate_transition(Active, Completed).is_ok());
107        assert!(validate_transition(Paused, Active).is_ok());
108        assert!(validate_transition(Completed, Archived).is_ok());
109    }
110
111    #[test]
112    fn invalid_transitions() {
113        use WorkstreamState::*;
114        assert!(validate_transition(Paused, Completed).is_err());
115        assert!(validate_transition(Paused, Archived).is_err());
116        assert!(validate_transition(Archived, Active).is_err());
117        assert!(validate_transition(Completed, Active).is_err());
118        assert!(validate_transition(Active, Archived).is_err());
119        assert!(validate_transition(Active, Active).is_err());
120    }
121}