Skip to main content

prj_core/
project.rs

1use std::path::{Path, PathBuf};
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::config::Config;
7use crate::detect::{BuildSystem, VcsType};
8use crate::error::PrjError;
9
10/// A registered project with its detected metadata.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Project {
13    pub name: String,
14    pub path: PathBuf,
15    pub vcs: Vec<VcsType>,
16    pub build_systems: Vec<BuildSystem>,
17    pub artifact_dirs: Vec<String>,
18    pub added_at: DateTime<Utc>,
19    #[serde(default)]
20    pub tags: Vec<String>,
21}
22
23/// Persistent store of all registered projects, serialized as TOML.
24#[derive(Debug, Default, Serialize, Deserialize)]
25pub struct ProjectDatabase {
26    #[serde(default)]
27    pub projects: Vec<Project>,
28}
29
30impl ProjectDatabase {
31    /// Load the database from disk, or return an empty one if it doesn't exist.
32    pub fn load(config: &Config) -> Result<Self, PrjError> {
33        let path = config.database_path();
34        if path.exists() {
35            let content =
36                std::fs::read_to_string(&path).map_err(|e| PrjError::DatabaseRead(Box::new(e)))?;
37            toml::from_str(&content).map_err(|e| PrjError::DatabaseRead(Box::new(e)))
38        } else {
39            Ok(Self::default())
40        }
41    }
42
43    /// Save the database to disk.
44    pub fn save(&self, config: &Config) -> Result<(), PrjError> {
45        let path = config.database_path();
46        if let Some(parent) = path.parent() {
47            std::fs::create_dir_all(parent).map_err(|e| PrjError::DatabaseWrite(Box::new(e)))?;
48        }
49        let content =
50            toml::to_string_pretty(self).map_err(|e| PrjError::DatabaseWrite(Box::new(e)))?;
51        std::fs::write(&path, content).map_err(|e| PrjError::DatabaseWrite(Box::new(e)))?;
52        Ok(())
53    }
54
55    /// Add a project. Returns error if a project with the same path already exists.
56    pub fn add(&mut self, project: Project) -> Result<(), PrjError> {
57        if self.projects.iter().any(|p| p.path == project.path) {
58            return Err(PrjError::ProjectAlreadyExists(
59                project.path.display().to_string(),
60            ));
61        }
62        self.projects.push(project);
63        Ok(())
64    }
65
66    /// Remove a project by name. Returns error if not found.
67    pub fn remove(&mut self, name: &str) -> Result<Project, PrjError> {
68        let idx = self
69            .projects
70            .iter()
71            .position(|p| p.name == name)
72            .ok_or_else(|| PrjError::ProjectNotFound(name.to_string()))?;
73        Ok(self.projects.remove(idx))
74    }
75
76    /// Find a project by name.
77    pub fn find(&self, name: &str) -> Option<&Project> {
78        self.projects.iter().find(|p| p.name == name)
79    }
80
81    /// Find a project by name (mutable).
82    pub fn find_mut(&mut self, name: &str) -> Option<&mut Project> {
83        self.projects.iter_mut().find(|p| p.name == name)
84    }
85
86    /// Add tags to a project.
87    pub fn add_tags(&mut self, name: &str, tags: &[String]) -> Result<(), PrjError> {
88        let project = self
89            .find_mut(name)
90            .ok_or_else(|| PrjError::ProjectNotFound(name.to_string()))?;
91        for tag in tags {
92            if !project.tags.contains(tag) {
93                project.tags.push(tag.clone());
94            }
95        }
96        project.tags.sort();
97        Ok(())
98    }
99
100    /// Remove tags from a project.
101    pub fn remove_tags(&mut self, name: &str, tags: &[String]) -> Result<(), PrjError> {
102        let project = self
103            .find_mut(name)
104            .ok_or_else(|| PrjError::ProjectNotFound(name.to_string()))?;
105        project.tags.retain(|t| !tags.contains(t));
106        Ok(())
107    }
108
109    /// Find projects whose paths no longer exist.
110    pub fn find_orphaned(&self) -> Vec<&Project> {
111        self.projects.iter().filter(|p| !p.path.exists()).collect()
112    }
113
114    /// Remove projects whose paths no longer exist, returning the removed ones.
115    pub fn remove_orphaned(&mut self) -> Vec<Project> {
116        let (orphaned, alive): (Vec<_>, Vec<_>) = std::mem::take(&mut self.projects)
117            .into_iter()
118            .partition(|p| !p.path.exists());
119        self.projects = alive;
120        orphaned
121    }
122
123    /// Register a project at the given path with detection.
124    pub fn register(&mut self, path: &Path, name: Option<&str>) -> Result<&Project, PrjError> {
125        let path = path
126            .canonicalize()
127            .map_err(|_| PrjError::PathNotFound(path.to_path_buf()))?;
128
129        if !path.is_dir() {
130            return Err(PrjError::NotADirectory(path));
131        }
132
133        let detection = crate::detect::detect_project(&path);
134
135        let name = name.map(|s| s.to_string()).unwrap_or_else(|| {
136            path.file_name()
137                .map(|n| n.to_string_lossy().to_string())
138                .unwrap_or_else(|| "unknown".to_string())
139        });
140
141        let project = Project {
142            name,
143            path,
144            vcs: detection.vcs,
145            build_systems: detection.build_systems,
146            artifact_dirs: detection.artifact_dirs,
147            added_at: Utc::now(),
148            tags: Vec::new(),
149        };
150
151        self.add(project)?;
152        Ok(self.projects.last().expect("just pushed"))
153    }
154}