1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
use crate::app;
use anyhow::{anyhow, Context, Result};
use std::io::prelude::*;
use std::io::BufReader;
use std::{collections::HashMap, fmt, fs, path::PathBuf};

/// Memo data contains the content of the memo file.
pub struct MemoData {
    content: HashMap<i32, String>,
}

impl MemoData {
    /// Create a new MemoData
    pub fn new() -> Self {
        MemoData {
            content: HashMap::new(),
        }
    }

    /// parse data from file and return a HashMap
    fn parse(data: String) -> Result<HashMap<i32, String>> {
        data.lines()
            .filter(|line| !line.is_empty())
            .map(|line| vaidate_line(line))
            .collect()
    }
}

/// Implement DataFile trait for MemoData
impl DataFile for MemoData {
    /// Load data from file
    fn load(&mut self, cli_app: &app::AppConfig) -> Result<()> {
        let data = read_file(&cli_app.data_file_path())?;
        self.content = MemoData::parse(data)?;
        Ok(())
    }

    /// Return sorted ids of items in MemoData
    fn sorted_ids(&self) -> Vec<i32> {
        let mut ids: Vec<i32> = self.content.keys().cloned().collect();
        ids.sort();
        ids
    }

    /// Return content of item with id
    fn get(&self, id: i32) -> Option<&String> {
        self.content.get(&id)
    }

    /// Add item to MemoData
    fn add(&mut self, id: i32, name: &str) -> Result<()> {
        if self.content.contains_key(&id) {
            return Err(anyhow!("Id '{}' already exists", id));
        }
        self.content.insert(id, name.to_string());
        Ok(())
    }
}

/// Implement Display trait for MemoData
impl fmt::Display for MemoData {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (id, name) in &self.content {
            writeln!(f, "{}: {}", id, name)?;
        }
        Ok(())
    }
}

/// DataFile trait is used to define the methods that a data file must implement.
pub trait DataFile: fmt::Display {
    fn load(&mut self, app: &app::AppConfig) -> Result<()>;
    fn sorted_ids(&self) -> Vec<i32>;
    fn get(&self, id: i32) -> Option<&String>;
    fn add(&mut self, id: i32, name: &str) -> Result<()>;
}

/// Get file path and file name and check if it exists
fn file_exist(file_path: &PathBuf) -> Result<bool> {
    if file_path.exists() {
        Ok(true)
    } else {
        Err(anyhow!(
            "File '{}' not found",
            file_path.to_str().unwrap_or("unknown path")
        ))
    }
}

/// Read the content of a file given its path and name
pub fn read_file(file_path: &PathBuf) -> Result<String> {
    file_exist(file_path)?;
    let mut buff_reader = BufReader::new(fs::File::open(file_path)?);
    let mut contents = String::new();
    buff_reader.read_to_string(&mut contents)?;
    Ok(contents)
}

// validate a line of file content
fn vaidate_line(line: &str) -> Result<(i32, String)> {
    let mut parts = line.splitn(2, ':');
    let id = parts
        .next()
        .ok_or_else(|| anyhow!("Missing id in line"))?
        .parse::<i32>()
        .with_context(|| format!("Invalid id in line '{}'", line))?;
    let name = parts
        .next()
        .and_then(|n| if n.is_empty() { None } else { Some(n) })
        .ok_or_else(|| anyhow!("Missing content in line '{}'", line))?
        .to_string();
    Ok((id, name))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn test_memo_data_new() {
        let d = MemoData::new();
        assert_eq!(d.content.len(), 0);
    }

    #[test]
    fn test_memo_data_parse() {
        let data = "1: one\n2: two\n3: three\n".to_string();
        let d = MemoData::parse(data).unwrap();
        assert_eq!(d.len(), 3);
    }

    #[test]
    fn test_memo_data_add() {
        let mut d = MemoData::new();
        assert_eq!(d.add(1, "one").is_ok(), true);
        assert_eq!(d.content.len(), 1);
    }

    #[test]
    fn test_memo_data_load() {
        let mut app_config = app::AppConfig::new("memo", "memo.txt");

        let dir = tempdir().unwrap();
        let mut data_dir = dir.path().to_path_buf();
        data_dir.push(app_config.name());
        fs::create_dir_all(&data_dir).unwrap();

        let mut file_dir = data_dir.clone();
        file_dir.push(app_config.data_file());
        let mut file = fs::File::create(&file_dir).unwrap();
        writeln!(file, "1: one\n2: two\n3: three\n").unwrap();

        app_config.data_dir = data_dir.clone();

        let mut d = MemoData::new();
        assert_eq!(d.load(&app_config).is_ok(), true);
    }

    #[test]
    fn test_memo_data_sorted_ids() {
        let mut d = MemoData::new();
        assert_eq!(d.add(2, "two").is_ok(), true);
        assert_eq!(d.add(1, "one").is_ok(), true);
        assert_eq!(d.add(3, "three").is_ok(), true);
        assert_eq!(d.sorted_ids(), vec![1, 2, 3]);
    }

    #[test]
    fn test_memo_data_get() {
        let mut d = MemoData::new();
        assert_eq!(d.add(1, "one").is_ok(), true);
        assert_eq!(d.get(1).unwrap(), "one");
    }

    #[test]
    fn test_file_exist() {
        let file_name = "test.txt";
        let dir = tempdir().unwrap();
        let mut file_path = dir.path().to_path_buf();
        file_path.push(file_name);
        let mut file = fs::File::create(&file_path).unwrap();
        writeln!(file, "test").unwrap();
        assert_eq!(file_exist(&file_path).unwrap(), true);
    }

    #[test]
    fn test_read_file() {
        let file_name = "test.txt";
        let dir = tempdir().unwrap();
        let mut file_path = dir.path().to_path_buf();
        file_path.push(file_name);
        let mut file = fs::File::create(&file_path).unwrap();
        writeln!(file, "test").unwrap();
        assert_eq!(read_file(&file_path).unwrap(), "test\n");
    }
}