Skip to main content

vex_project/
lib.rs

1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use vex_domain::{ProjectId, validate_slug};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Project {
10    pub id: ProjectId,
11    pub name: String,
12    pub local_path: PathBuf,
13    pub github_remote: String,
14    pub created_at: DateTime<Utc>,
15}
16
17#[derive(Debug, Error)]
18pub enum ProjectError {
19    #[error("project not found: {0}")]
20    NotFound(String),
21    #[error("project already exists: {0}")]
22    AlreadyExists(String),
23    #[error(
24        "invalid project name '{0}': must be alphanumeric with hyphens, starting with alphanumeric"
25    )]
26    InvalidName(String),
27    #[error("invalid path: {0}")]
28    InvalidPath(String),
29    #[error("not a git repository: {0}")]
30    NotGitRepo(String),
31    #[error("no GitHub remote found in {0}")]
32    NoGithubRemote(String),
33    #[error("IO error: {0}")]
34    Io(#[from] std::io::Error),
35    #[error("{0}")]
36    Other(String),
37}
38
39pub trait ProjectRepo {
40    fn create(&self, project: &Project) -> Result<(), ProjectError>;
41    fn find_by_id(&self, id: &ProjectId) -> Result<Option<Project>, ProjectError>;
42    fn find_by_name(&self, name: &str) -> Result<Option<Project>, ProjectError>;
43    fn list_all(&self) -> Result<Vec<Project>, ProjectError>;
44    fn delete(&self, id: &ProjectId) -> Result<(), ProjectError>;
45}
46
47pub fn validate_project_name(name: &str) -> Result<(), ProjectError> {
48    if !validate_slug(name) {
49        return Err(ProjectError::InvalidName(name.into()));
50    }
51    Ok(())
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn valid_project_names() {
60        assert!(validate_project_name("my-project").is_ok());
61        assert!(validate_project_name("vex").is_ok());
62        assert!(validate_project_name("a").is_ok());
63        assert!(validate_project_name("project-123").is_ok());
64        assert!(validate_project_name("123-abc").is_ok());
65    }
66
67    #[test]
68    fn invalid_project_names() {
69        assert!(validate_project_name("").is_err());
70        assert!(validate_project_name("-leading-hyphen").is_err());
71        assert!(validate_project_name("has spaces").is_err());
72        assert!(validate_project_name("has_underscore").is_err());
73        assert!(validate_project_name("has.dot").is_err());
74        assert!(validate_project_name("has/slash").is_err());
75    }
76}