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}