1use crate::{
2 messages::Message,
3 project::{Project, ProjectName},
4};
5use anyhow::{anyhow, Error};
6use indexmap::IndexMap;
7use owo_colors::OwoColorize;
8use serde::{Deserialize, Serialize};
9use std::{
10 fs::OpenOptions,
11 io::{Read, Write},
12 path::PathBuf,
13 str::FromStr,
14};
15
16#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
17pub struct Database {
18 pub projects: IndexMap<ProjectName, PathBuf>,
19 #[serde(skip)]
20 pub path: Option<PathBuf>,
21}
22
23impl Database {
24 pub fn new(path: Option<PathBuf>) -> Self {
25 Self {
26 projects: IndexMap::new(),
27 path,
28 }
29 }
30
31 pub fn list_projects(&self) -> Vec<ProjectName> {
32 self.projects
33 .iter()
34 .map(|(project_name, _project_path)| project_name.clone())
35 .collect()
36 }
37
38 pub fn remove_project(&mut self, project_name: ProjectName) -> Result<(), Error> {
39 match self.projects.shift_remove(&project_name) {
40 Some(_p) => Ok(()),
41 None => Err(anyhow!(
42 "There was no project named {} on the database!",
43 project_name.bold()
44 )),
45 }
46 }
47
48 pub fn add_project(&mut self, project_name: ProjectName, path: PathBuf) -> Result<(), Error> {
49 let path = std::fs::canonicalize(path).expect("Failed to canonicalize path");
51 if let Some(p) = self.projects.get(&project_name) {
52 return Err(anyhow!(
53 "{} is registered already at {}.",
54 project_name,
55 p.to_str().unwrap().bold()
56 ));
57 }
58 for (project_name, project_path) in self.projects.iter() {
59 if project_path == &path {
60 return Err(anyhow!(
61 "The project at {} is already registered under the name {}.",
62 project_path.to_str().unwrap().bold(),
63 project_name.bold()
64 ));
65 }
66 }
67 self.projects.insert(project_name, path);
68 Ok(())
69 }
70
71 pub fn get_project_path(&self, project_name: &ProjectName) -> Option<&PathBuf> {
72 self.projects.get(project_name)
73 }
74
75 pub fn go_to_project(
77 &self,
78 project_name: &ProjectName,
79 ) -> Result<(String, Option<String>), Error> {
80 match self.projects.get(project_name) {
81 Some(project_path) => {
82 if !project_path.is_dir() {
83 return Err(anyhow!("I dont know how to tell you this, but there was a problem with the registry.\nApparently {} directory {} or {}.", project_path.to_string_lossy().bold(), "is missing".red().bold(), "is not a directory".red().bold()));
84 }
85 println!("echo \"Krabby is taking you to {}!\";", project_name.bold());
86 let s = project_path.to_string_lossy();
87 let cd_cmd = format!("cd {}", s);
88 println!("{}", cd_cmd);
89 if let Some(hook_cmd) = self.get_project_hook_cmd(project_name) {
91 println!("echo \"Running hook:\n{}\"", hook_cmd.bold());
92 println!("{}", hook_cmd);
93 return Ok((cd_cmd, Some(hook_cmd)));
94 }
95 Ok((cd_cmd, None))
96 }
97 None => Err(anyhow!(
98 "{}",
99 Message::ProjectNotFound(project_name.clone())
100 )),
101 }
102 }
103
104 pub fn get_project_hook_cmd(&self, project_name: &ProjectName) -> Option<String> {
105 if let Ok(project) = self.get_project_file(project_name) {
106 return project.get_hook_cmd();
107 }
108 None
109 }
110
111 pub fn get_project(&self, project_name: &ProjectName) -> Option<&PathBuf> {
112 self.projects.get(project_name)
113 }
114
115 fn get_project_file(&self, project_name: &ProjectName) -> Result<Project, anyhow::Error> {
116 let project_file_path = match self.get_project(project_name) {
117 Some(path) => path.join("krabby.toml"),
118 None => return Err(anyhow!("There was no project {}", project_name)),
119 };
120 Project::from_file(project_file_path)
121 }
122
123 pub fn from_string(s: &str) -> Self {
124 toml::from_str(s).unwrap()
125 }
126
127 fn set_path(&mut self, path: PathBuf) {
128 self.path = Some(path);
129 }
130
131 pub fn save(&self) {
132 self.write().unwrap_or_else(|_| {
133 panic!(
134 "Failed to save database at {}",
135 self.path
136 .clone()
137 .unwrap()
138 .into_os_string()
139 .into_string()
140 .unwrap()
141 );
142 });
143 }
144
145 pub fn write(&self) -> Result<(), Error> {
146 let mut f = std::fs::OpenOptions::new()
147 .read(true)
148 .write(true)
149 .truncate(true)
151 .create(true)
152 .open(self.path.clone().expect("Path was not set properly"))?;
153 f.write_all(self.to_string().as_bytes())?;
154 Ok(())
155 }
156
157 pub fn from_file(path: PathBuf) -> Result<Self, Error> {
158 let mut file = OpenOptions::new()
159 .read(true)
160 .write(true)
161 .create(true)
162 .open(path.clone())?;
163 let mut contents = String::new();
164 file.read_to_string(&mut contents)?;
165 if contents.is_empty() {
166 file.write_all(Database::new(Some(path.clone())).to_string().as_bytes())?;
167 }
168 let mut db = Self::from_str(&contents)
169 .unwrap_or_else(|e| panic!("Failed to read database string.\n{}", e));
170 db.set_path(path);
171 Ok(db)
172 }
173}
174
175impl FromStr for Database {
176 type Err = Error;
177
178 fn from_str(s: &str) -> Result<Self, Self::Err> {
179 let database: Result<Self, toml::de::Error> = toml::from_str(s);
180 if let Ok(db) = database {
181 return Ok(db);
182 }
183 Err(anyhow!("Failed to parse Database from string:\n{}", s))
184 }
185}
186
187impl ToString for Database {
188 fn to_string(&self) -> String {
189 toml::to_string(self).unwrap()
190 }
191}
192
193impl Default for Database {
194 fn default() -> Self {
195 Self::new(None)
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn database_is_initialized_empty() {
205 let database = Database::new(None);
206 assert!(database.projects.is_empty())
207 }
208
209 #[test]
210 fn parse_database_successfully() {
211 let mut database = Database::new(None);
212 database
213 .add_project(ProjectName::parse("project".into()), "/".into())
214 .unwrap();
215
216 let database_str = r#"
217 [projects]
218 project = "/"
219 "#;
220 let database_from_str: Database = toml::from_str(database_str).unwrap();
221
222 assert_eq!(database, database_from_str)
223 }
224
225 #[test]
226 fn add_project_to_database_successfully() {
227 let mut database = Database::new(None);
228 let res = database.add_project(ProjectName::parse("project".into()), "/".into());
229 assert!(res.is_ok());
230 let project_list: Vec<ProjectName> = vec![ProjectName::parse("project".into())];
231 assert_eq!(database.list_projects(), project_list);
232 }
233
234 #[test]
235 fn remove_existing_project_from_database() {
236 let database_str = r#"
237 [projects]
238 project1 = "/tmp/project1"
239 project2 = "/tmp/project2"
240 "#;
241 let mut database = Database::from_string(database_str);
242 database.set_path("./krabby.db".into());
243 database.save();
244 database
245 .remove_project(ProjectName::parse("project2".into()))
246 .unwrap();
247 database.save();
248
249 let single_project_database_str = r#"[projects]
250project1 = "/tmp/project1"
251"#;
252 assert_eq!(database.to_string(), single_project_database_str);
253
254 database
255 .remove_project(ProjectName::parse("project1".into()))
256 .unwrap();
257 database.save();
258
259 let empty_database_str = r#"[projects]
260"#;
261
262 assert_eq!(database.to_string(), empty_database_str);
263 }
264
265 #[test]
266 #[should_panic]
267 fn remove_project_from_empty_database() {
268 let mut database = Database::new(None);
269 database
270 .remove_project(ProjectName::parse("project".into()))
271 .expect("failed to delete project")
272 }
273
274 use crate::project::{Project, ProjectName};
275 use rand::distributions::Alphanumeric;
276 use rand::{thread_rng, Rng};
277 use std::fs;
278
279 fn create_random_database_file(path: &str) -> PathBuf {
280 let mut path = PathBuf::from(path);
281 let rand_string: String = thread_rng()
282 .sample_iter(&Alphanumeric)
283 .take(30)
284 .map(char::from)
285 .collect();
286 let database_file_name = format!("{}-krabby.db", rand_string.clone());
287 path.push(database_file_name);
288
289 Database::new(Some(path.clone()));
290
291 path
292 }
293
294 fn create_random_project(path: &str) -> (ProjectName, PathBuf) {
295 let mut path = PathBuf::from(path);
296 let rand_string: String = thread_rng()
297 .sample_iter(&Alphanumeric)
298 .take(30)
299 .map(char::from)
300 .collect();
301 let project_name = ProjectName::parse(format!("{}-project", rand_string.clone()));
302 path.push(project_name.to_string());
303
304 fs::create_dir(path.clone())
305 .unwrap_or_else(|_| panic!("Failed to create {}", path.clone().to_string_lossy()));
306 Project::new(project_name.clone(), Some(path.clone()));
307
308 (project_name, path)
309 }
310
311 fn remove_file(path: &str) {
312 fs::remove_file(path).unwrap_or_else(|_| panic!("Failed to remove {}", path))
313 }
314 fn remove_dir(path: &str) {
315 fs::remove_dir(path).unwrap_or_else(|_| panic!("Failed to remove {}", path))
316 }
317}