local_issues_lib/
lib.rs

1mod db;
2mod message;
3
4use chrono::{DateTime, Local};
5use db::{load_db, save};
6use serde::{Deserialize, Serialize};
7// use sled::open;
8use 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    /// count of delted this issue
38    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    /// edit title by arg title: String
95    fn edit_title(&mut self, title: String) {
96        self.title = title
97    }
98
99    // fn commit(&mut self, new_message: String) {
100    //     self.commit_messages.push(new_message);
101    // }
102
103    /// update `updated_at` by now time
104    fn update_date(&mut self) {
105        self.updated_at = Local::now();
106    }
107
108    /// edit `due_date` by arg
109    fn edit_due_date(&mut self, new_due: DateTime<Local>) {
110        self.due_date = Some(new_due);
111    }
112
113    /// edit status
114    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    /// self.tagsがSomeの場合にのみ`new_tags`をappend
131    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    /// decrement delete flag count
186    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
199/// 部分一致の場合は、`Some(item or item2)`
200/// 0 is item、1 is item2
201pub 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
227/// Projectの操作
228impl Project {
229    /// # args
230    ///
231    /// * `title` - Project title
232    /// * `path` - work dir path that set `db.json`
233    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    /// save data to file and purge deleted marked issues.
254    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    /// MarkedAsdeleteのチェックなしで保存
262    /// また、デクリメント処理もしない
263    /// **カウントが実際の操作とズレる可能性があるため注意**
264    pub fn save_without_delete(self) -> Result<(), Error> {
265        db::save(self)?;
266        Ok(())
267    }
268
269    /// タイトルが完全一致したidを`Option<Vec<u64>>`で返す
270    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    /// タイトルが部分一致したidを`Option<Vec<u64>>`で返す
281    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    /// add tags to self.tags from arg: `Vec<String>`
307    pub fn add_tags_to_project(&mut self, new_tags: &mut Vec<String>) {
308        self.tags.append(new_tags);
309    }
310
311    /// remove tags from self.tags, by tag_names: `Vec<String>`
312    pub fn remove_tag(&mut self, tag_names: Vec<String>) {
313        // fがtag_namesに含まれている場合は削除される。
314        self.tags.retain(|f| tag_names.iter().any(|t| t != f));
315    }
316
317    /// return cloned current tags: `Vec<String>`
318    pub fn get_tags(&mut self) -> Vec<String> {
319        self.tags.clone()
320    }
321
322    /// Project.bodyに`new_issue`を追加(idのインクリメントは自動)
323    pub fn add_issue(&mut self, new_issue: Issue) {
324        self.insert(new_issue);
325    }
326
327    /// idを元にissueを削除
328    /// statusをdeleteにする訳ではないので注意
329    /// カウント付きで削除するには`edit_status()`で
330    /// ⚠️いつかはカウントにするかもしれない
331    /// 以下の`remove_issue_from_title()`や`remove_all_issue_from_title`も同様
332    pub fn remove_issue(&mut self, id: &u64) -> Option<Issue> {
333        self.body.remove(id)
334    }
335
336    /// 完全一致が一つだった場合にのみ削除
337    /// Noneの場合は削除した項目なし
338    /// ⚠️いつかはカウントにするかもしれない
339    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    /// 完全一致した内容を全て削除
356    /// ⚠️いつかはカウントにするかもしれない
357    /// Noneの場合は削除した項目なし
358    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
368/// issueの操作
369impl Project {
370    /// current_idをインクリメントして挿入
371    fn insert(&mut self, new_issue: Issue) {
372        self.current_id += 1;
373        self.body.insert(self.current_id, new_issue);
374    }
375
376    /// `new_tag<S>`をidを元にIssueへ追記
377    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    /// idを元にissueのtagをクリア
388    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    /// idに対応するissueのタイトルを変更
400    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    /// idを元にissueの`due date`を変更
415    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    /// デクリメント処理
437    fn decrement_delete_flag(&mut self) {
438        for i in self.body.iter_mut() {
439            i.1.decrement_delete_count();
440        }
441    }
442
443    /// delete flagが0のissueのidをVecで返す
444    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    /// delete flagが0のissueを削除する
459    /// idの順は変更なし
460    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
469/// 固有issueの操作
470impl 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    // i dont know whether it works correctly
520    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        // fs::remove_dir(&workdir).unwrap();
591        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        // pro.save().unwrap();
597
598        println!("{}", pro);
599        println!("{}", pro.oneline_fmt());
600        let ids = vec![1];
601        println!("{}", pro.filterd_string(ids));
602    }
603}