1mod db;
2mod message;
3
4use chrono::{DateTime, Local};
5use db::{load_db, save};
6use serde::{Deserialize, Serialize};
7use std::{
9 collections::HashMap,
10 fmt::{Debug, Display},
11 io,
12 path::{Path, PathBuf},
13 str,
14};
15
16#[derive(Debug)]
17pub enum Error {
18 DbNotFound,
19 IoError(io::Error),
20 SerdeError(serde_json::Error),
21}
22impl Display for Error {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 Error::DbNotFound => write!(f, "db not found."),
26 Error::IoError(error) => write!(f, "io error: {}", error),
27 Error::SerdeError(error) => write!(f, "serde error: {}", error),
28 }
29 }
30}
31
32#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, PartialOrd, Eq)]
33pub enum Status {
34 #[default]
35 Open,
36 Closed(Closed),
37 MarkedAsDelete(i32),
39}
40impl Display for Status {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Status::Open => write!(f, "open"),
44 Status::Closed(closed) => write!(f, "closed{}", closed),
45 Status::MarkedAsDelete(c) => write!(f, "Delete at {}", c),
46 }
47 }
48}
49
50#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, PartialOrd, Eq)]
51pub enum Closed {
52 #[default]
53 Resolved,
54 NotResolved,
55}
56impl Display for Closed {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Closed::Resolved => write!(f, "Resolved"),
60 Closed::NotResolved => write!(f, "NotResolved"),
61 }
62 }
63}
64
65#[derive(Default, Debug, Serialize, Deserialize, Clone)]
66pub struct Issue {
67 title: String,
68 created_at: DateTime<Local>,
69 updated_at: DateTime<Local>,
70 due_date: Option<DateTime<Local>>,
71 status: Status,
72 tags: Option<Vec<String>>,
73 commit_messages: message::CommitMessages,
74}
75impl Issue {
76 pub fn new<S: AsRef<str>>(
77 title: S,
78 due_date: Option<DateTime<Local>>,
79 status: Status,
80 tags: Option<Vec<String>>,
81 ) -> Self {
82 let now = Local::now();
83 Self {
84 title: title.as_ref().to_string(),
85 created_at: now,
86 updated_at: now,
87 due_date,
88 status,
89 tags,
90 commit_messages: message::CommitMessages::new(),
91 }
92 }
93
94 fn edit_title(&mut self, title: String) {
96 self.title = title
97 }
98
99 fn update_date(&mut self) {
105 self.updated_at = Local::now();
106 }
107
108 fn edit_due_date(&mut self, new_due: DateTime<Local>) {
110 self.due_date = Some(new_due);
111 }
112
113 fn edit_status(&mut self, new_status: Status) {
115 self.status = new_status;
116 }
117
118 fn clear_tags(&mut self) {
119 self.tags = None;
120 }
121
122 fn get_messages(&self) -> String {
123 format!("{}", self.commit_messages)
124 }
125
126 fn get_tags(&mut self) -> Option<Vec<String>> {
127 self.tags.clone()
128 }
129
130 fn add_tag(&mut self, mut new_tags: Vec<String>) {
132 if let Some(v) = &mut self.tags {
133 v.append(&mut new_tags);
134 }
135 }
136
137 fn push_commit_message<S: AsRef<str>>(&mut self, message: S) {
138 self.commit_messages.push(message);
139 }
140
141 fn is_opend(&self) -> bool {
142 matches!(self.status, Status::Open)
143 }
144
145 fn remove_commit_message(&mut self, id: u64) {
146 self.commit_messages.remove(id);
147 }
148
149 pub fn is_delete_marked(&self) -> bool {
150 matches!(self.status, Status::MarkedAsDelete(_))
151 }
152
153 fn delete_flag_is_zero(&self) -> bool {
154 match self.status {
155 Status::MarkedAsDelete(i) => i == 0,
156 _ => false,
157 }
158 }
159
160 fn delete_flag_count(&self) -> Option<i32> {
161 match self.status {
162 Status::MarkedAsDelete(c) => Some(c),
163 _ => None,
164 }
165 }
166
167 fn set_delete_count(&mut self, new_count: i32) -> Option<i32> {
168 match self.status {
169 Status::MarkedAsDelete(_) => {
170 self.status = Status::MarkedAsDelete(new_count);
171 if let Status::MarkedAsDelete(c) = self.status {
172 Some(c)
173 } else {
174 None
175 }
176 }
177 _ => None,
178 }
179 }
180
181 fn reset_delete_count(&mut self) -> Option<i32> {
182 self.set_delete_count(10)
183 }
184
185 fn decrement_delete_count(&mut self) {
187 if let Status::MarkedAsDelete(c) = self.status {
188 self.status = Status::MarkedAsDelete(c - 1)
189 }
190 }
191}
192
193pub enum MatchType {
194 Exact,
195 Partial,
196 Not,
197}
198
199pub fn compare_titles<S: AsRef<str> + Eq>(item: &S, item2: &S) -> (MatchType, Option<u8>) {
202 if item.as_ref() == item2.as_ref() {
203 (MatchType::Exact, None)
204 } else if item.as_ref().contains(item2.as_ref()) {
205 (MatchType::Partial, Some(0))
206 } else if item2.as_ref().contains(item.as_ref()) {
207 (MatchType::Partial, Some(1))
208 } else {
209 (MatchType::Not, None)
210 }
211}
212
213pub type BodyData = HashMap<u64, Issue>;
214pub type FetahedItem = (MatchType, BodyData);
215pub type FetchedList = Vec<FetahedItem>;
216
217#[derive(Debug, Serialize, Deserialize)]
218pub struct Project {
219 pub project_name: String,
220 pub work_path: PathBuf,
221 pub db_path: PathBuf,
222 pub body: BodyData,
223 current_id: u64,
224 pub tags: Vec<String>,
225}
226
227impl Project {
229 pub fn open<S: AsRef<str>, P: AsRef<Path>>(title: S, path: P) -> Result<Self, Error> {
234 let work_path = path.as_ref().join(".local_issues");
235 let db_path = work_path.join("db").with_extension("json");
236
237 if !db_path.exists() {
238 let void_body = Project {
239 project_name: title.as_ref().to_string(),
240 work_path,
241 db_path: db_path.clone(),
242 body: HashMap::new(),
243 current_id: 0,
244 tags: Vec::new(),
245 };
246 save(void_body)?;
247 };
248
249 let db = load_db(&db_path)?;
250 Ok(db)
251 }
252
253 pub fn save(mut self) -> Result<(), Error> {
255 self.purge_delete_marked_issues();
256 self.decrement_delete_flag();
257 db::save(self)?;
258 Ok(())
259 }
260
261 pub fn save_without_delete(self) -> Result<(), Error> {
265 db::save(self)?;
266 Ok(())
267 }
268
269 pub fn get_id_with_exact<S: AsRef<str>>(&self, title: &S) -> Option<Vec<u64>> {
271 let res = self
272 .body
273 .iter()
274 .filter(|f| f.1.title == title.as_ref())
275 .map(|f| *f.0)
276 .collect::<Vec<u64>>();
277 if res.is_empty() { None } else { Some(res) }
278 }
279
280 pub fn get_id_with_partial<S: AsRef<str>>(&self, title: &S) -> Option<Vec<u64>> {
282 let res = self
283 .body
284 .iter()
285 .filter(|f| f.1.title.contains(title.as_ref()) || title.as_ref().contains(&f.1.title))
286 .map(|f| *f.0)
287 .collect::<Vec<u64>>();
288 if res.is_empty() { None } else { Some(res) }
289 }
290
291 pub fn get_opened_issue_id(&self) -> Option<Vec<u64>> {
292 let res = self
293 .body
294 .iter()
295 .filter(|f| f.1.is_opend())
296 .map(|f| *f.0)
297 .collect::<Vec<u64>>();
298 if res.is_empty() { None } else { Some(res) }
299 }
300
301 pub fn get_all_issue_id(&self) -> Option<Vec<u64>> {
302 let res = self.body.iter().map(|f| *f.0).collect::<Vec<u64>>();
303 if res.is_empty() { None } else { Some(res) }
304 }
305
306 pub fn add_tags_to_project(&mut self, new_tags: &mut Vec<String>) {
308 self.tags.append(new_tags);
309 }
310
311 pub fn remove_tag(&mut self, tag_names: Vec<String>) {
313 self.tags.retain(|f| tag_names.iter().any(|t| t != f));
315 }
316
317 pub fn get_tags(&mut self) -> Vec<String> {
319 self.tags.clone()
320 }
321
322 pub fn add_issue(&mut self, new_issue: Issue) {
324 self.insert(new_issue);
325 }
326
327 pub fn remove_issue(&mut self, id: &u64) -> Option<Issue> {
333 self.body.remove(id)
334 }
335
336 pub fn remove_issue_from_title<S: AsRef<str>>(&mut self, title: S) -> Option<Issue> {
340 match self.get_id_with_exact(&title) {
341 Some(i) => {
342 if i.len() == 1 {
343 match i.first() {
344 Some(i) => self.remove_issue(i),
345 None => None,
346 }
347 } else {
348 None
349 }
350 }
351 None => None,
352 }
353 }
354
355 pub fn remove_all_issue_from_title<S: AsRef<str>>(&mut self, title: S) {
359 let res = self.get_id_with_exact(&title);
360 if let Some(c) = res {
361 for i in c {
362 self.remove_issue(&i);
363 }
364 }
365 }
366}
367
368impl Project {
370 fn insert(&mut self, new_issue: Issue) {
372 self.current_id += 1;
373 self.body.insert(self.current_id, new_issue);
374 }
375
376 pub fn add_tag_to_issue_by_id<S: AsRef<str>>(
378 &mut self,
379 id: u64,
380 new_tag: Vec<S>,
381 ) -> Option<()> {
382 self.body.get_mut(&id).map(|issue| {
383 issue.update_date();
384 issue.add_tag(new_tag.iter().map(|f| f.as_ref().to_string()).collect())
385 })
386 }
387 pub fn clear_tags_of_issue_by_id(&mut self, id: u64) -> Option<()> {
389 self.body.get_mut(&id).map(|issue| {
390 issue.update_date();
391 issue.clear_tags()
392 })
393 }
394
395 pub fn get_tags_from_issue_by_id(&self, id: u64) -> Option<Vec<String>> {
396 self.body.get(&id).and_then(|f| f.clone().get_tags())
397 }
398
399 pub fn edit_issue_title<S: AsRef<str>>(&mut self, id: u64, new_title: S) -> Option<()> {
401 self.body.get_mut(&id).map(|issue| {
402 issue.update_date();
403 issue.edit_title(new_title.as_ref().to_string())
404 })
405 }
406
407 pub fn edit_issue_status(&mut self, id: u64, new_status: Status) -> Option<()> {
408 self.body.get_mut(&id).map(|issue| {
409 issue.update_date();
410 issue.edit_status(new_status)
411 })
412 }
413
414 pub fn edit_issue_due(&mut self, id: u64, due: DateTime<Local>) -> Option<()> {
416 self.body.get_mut(&id).map(|issue| {
417 issue.update_date();
418 issue.edit_due_date(due)
419 })
420 }
421
422 pub fn get_delete_flag_count(&self, id: u64) -> Option<i32> {
423 self.body
424 .get(&id)
425 .filter(|issue| issue.is_delete_marked())
426 .and_then(|issue| issue.delete_flag_count())
427 }
428
429 pub fn reset_delete_flag_count(&mut self, id: u64) -> Option<()> {
430 self.body.get_mut(&id).map(|issue| {
431 issue.update_date();
432 issue.reset_delete_count();
433 })
434 }
435
436 fn decrement_delete_flag(&mut self) {
438 for i in self.body.iter_mut() {
439 i.1.decrement_delete_count();
440 }
441 }
442
443 fn delete_flaged_ids(&self) -> Option<Vec<u64>> {
445 let delete_marked_ids = self
446 .body
447 .iter()
448 .filter(|f| f.1.delete_flag_is_zero())
449 .map(|f| *f.0)
450 .collect::<Vec<u64>>();
451 if delete_marked_ids.is_empty() {
452 None
453 } else {
454 Some(delete_marked_ids)
455 }
456 }
457
458 fn purge_delete_marked_issues(&mut self) -> Option<()> {
461 let ids = self.delete_flaged_ids()?;
462 for i in ids {
463 self.remove_issue(&i);
464 }
465 Some(())
466 }
467}
468
469impl Project {
471 pub fn add_commit_by_id<S: AsRef<str>>(&mut self, id: u64, message: S) {
472 if let Some(issue) = self.body.get_mut(&id) {
473 issue.update_date();
474 issue.push_commit_message(message)
475 }
476 }
477
478 pub fn rm_commit_message(&mut self, id: u64) {
479 if let Some(issue) = self.body.get_mut(&id) {
480 issue.update_date();
481 issue.remove_commit_message(id)
482 }
483 }
484}
485
486impl Display for Project {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 let tagss = self
489 .tags
490 .iter()
491 .map(|f| {
492 let mut a = f.clone();
493 a.push(',');
494 a
495 })
496 .collect::<String>();
497
498 let bodys = self
499 .body
500 .iter()
501 .map(|f| {
502 let c = f;
503 format!(
504 "\t#{} {}\n\t{}\n\t{:?}\n\n",
505 c.0, c.1.title, c.1.status, c.1.tags
506 )
507 })
508 .collect::<String>();
509
510 let project_sum = format!(
511 "title: {}\ntags: {}\nIssues: \n{}",
512 self.project_name, tagss, bodys
513 );
514 write!(f, "{}", project_sum)
515 }
516}
517
518impl Project {
519 pub fn filterd_string(&self, filter_id: Vec<u64>) -> String {
521 let tagss = self
522 .tags
523 .iter()
524 .map(|f| {
525 let mut a = f.clone();
526 a.push(',');
527 a
528 })
529 .collect::<String>();
530 let bodys = filter_id
531 .iter()
532 .map(|c| {
533 self.body
534 .iter()
535 .filter(|f| f.0 == c)
536 .map(|e| format!("\t#{} {}\n", e.0, e.1.title,))
537 .collect::<String>()
538 })
539 .collect::<String>();
540 format!(
541 "title: {}\ntags: {}\nIssues: \n{}",
542 self.project_name, tagss, bodys
543 )
544 }
545 pub fn oneline_fmt(&self) -> String {
546 let tagss = self
547 .tags
548 .iter()
549 .map(|f| {
550 let mut a = f.clone();
551 a.push(',');
552 a
553 })
554 .collect::<String>();
555
556 let bodys = self
557 .body
558 .iter()
559 .map(|f| format!("\t#{} {}\n", f.0, f.1.title))
560 .collect::<String>();
561
562 format!(
563 "title: {}\ntags: {}\nIssues: \n{}",
564 self.project_name, tagss, bodys
565 )
566 }
567
568 pub fn get_commit_messages_list_by_id(&self, id: u64) -> Option<String> {
569 self.body.get(&id).map(|f| f.get_messages().to_string())
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use crate::{Issue, Project};
576 use std::env;
577
578 #[test]
579 fn test_new_pro() {
580 let workdir = env::current_dir().unwrap().join("test").join("test_open");
581 println!("{:?}", workdir);
582
583 let test_project = Project::open("test_pro", workdir).unwrap();
584 println!("{:?}", test_project);
585 }
586
587 #[test]
588 fn ctrl_issues() {
589 let workdir = env::current_dir().unwrap().join("test").join("test_ctrl");
590 let mut pro = Project::open("ctrl_test", workdir).unwrap();
592
593 pro.add_issue(Issue::new("issute1", None, crate::Status::Open, None));
594 pro.add_issue(Issue::new("title2", None, crate::Status::Open, None));
595 pro.edit_issue_title(2, "new_title7");
596 println!("{}", pro);
599 println!("{}", pro.oneline_fmt());
600 let ids = vec![1];
601 println!("{}", pro.filterd_string(ids));
602 }
603}