diary/ops/
add.rs

1//! # Add operations
2//!
3//! The add module contains functionality relating to the add command,
4//! independent of the CLI.
5use std::{fs::File, io::Write};
6
7use chrono::prelude::*;
8
9use crate::{errors::DiaryError, utils::editing, Diary, EntryContent};
10
11/// The options available to the add command.
12pub struct AddOptions {
13    /// An optional entry tag.
14    pub tag: Option<String>,
15    /// Optional adding contents.
16    pub content: Option<String>,
17}
18
19/// Adds the given content to a file.
20///
21/// # Arguments
22///
23/// * `file` The file to add the content to.
24/// * `content` The content to add to the file above.
25/// * `tag` The optional tag to place above the content.
26///
27/// # Errors
28///
29/// * If the content provided is empty.
30fn add_content(mut file: File, content: String, tag: Option<String>) -> Result<(), DiaryError> {
31    if content.is_empty() {
32        return Err(DiaryError::NoContent);
33    }
34
35    if let Some(tag) = tag {
36        file.write_all(tag.as_bytes())?;
37    }
38    editing::add_user_content_to_file(&mut file, content)?;
39    Ok(())
40}
41
42/// Adds user provided content to a diary entry.
43///
44/// # Arguments
45///
46/// * `opts` - The options available to the add function.
47/// * `diary` - Struct representing the diary.
48/// * `date` - The date of the entry to add to.
49/// * `string_getter` - The function that obtains the string to add to the file.
50///
51/// # Returns
52///
53/// The unit if adding the content was successful, a DiaryError otherwise.
54pub fn add(
55    opts: &AddOptions,
56    diary: &Diary,
57    date: &DateTime<Local>,
58    string_getter: editing::StringGetter,
59) -> Result<(), DiaryError> {
60    let file = match diary.get_entry_file(date) {
61        Ok(file) => file,
62        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
63            return Err(DiaryError::NoEntry { source: Some(e) })
64        }
65        Err(e) => return Err(e.into()), // uncovered.
66    };
67
68    let content = match &opts.content {
69        Some(content) => content.to_owned() + "\n",
70        None => string_getter("".to_owned())?,
71    };
72
73    let tag_result = opts
74        .tag
75        .as_ref()
76        .map(|tag| diary.file_type().tag(tag.to_string()));
77
78    add_content(file, content, tag_result)
79}
80
81#[cfg(test)]
82mod test {
83    use std::fs;
84
85    use chrono::{Local, TimeZone};
86
87    use crate::{
88        ops::{
89            add::{add, AddOptions},
90            testing,
91        },
92        utils::editing::test::{test_empty_string_getter, test_string_getter},
93        Diary,
94    };
95
96    #[test]
97    fn quick_add() {
98        let config = testing::temp_config();
99
100        testing::default_init(config.diary_path());
101
102        let entry_date = Local.with_ymd_and_hms(2021, 11, 6, 0, 0, 0).unwrap();
103        testing::new_entry(&config, &entry_date);
104
105        let diary = Diary::from_config(&config).unwrap();
106
107        let opts = AddOptions {
108            tag: None,
109            content: Some("testing quick add".to_owned()),
110        };
111        add(&opts, &diary, &entry_date, test_string_getter).unwrap();
112
113        let diary_file = Diary::from_config(&config).unwrap();
114
115        let entry_path = diary_file.get_entry_path(&entry_date);
116
117        let content = fs::read_to_string(entry_path).unwrap();
118
119        assert!(content.contains("testing quick add"));
120    }
121
122    #[test]
123    fn add_no_tag() {
124        let config = testing::temp_config();
125
126        testing::default_init(config.diary_path());
127
128        let entry_date = Local.with_ymd_and_hms(2021, 11, 6, 0, 0, 0).unwrap();
129        testing::new_entry(&config, &entry_date);
130
131        let diary = Diary::from_config(&config).unwrap();
132
133        let opts = AddOptions {
134            tag: None,
135            content: None,
136        };
137        add(&opts, &diary, &entry_date, test_string_getter).unwrap();
138
139        let diary_file = Diary::from_config(&config).unwrap();
140
141        let entry_path = diary_file.get_entry_path(&entry_date);
142
143        let content = fs::read_to_string(entry_path).unwrap();
144
145        assert!(content.contains("Test content"));
146        assert!(!content.contains("\n\n\n"))
147    }
148
149    #[test]
150    fn add_with_tag() {
151        let config = testing::temp_config();
152        testing::default_init(config.diary_path());
153
154        let entry_date = Local.with_ymd_and_hms(2021, 11, 6, 0, 0, 0).unwrap();
155        testing::new_entry(&config, &entry_date);
156
157        let diary = Diary::from_config(&config).unwrap();
158        let opts = AddOptions {
159            tag: Some("Tag".to_owned()),
160            content: None,
161        };
162        add(&opts, &diary, &entry_date, test_string_getter).unwrap();
163
164        let entry_path = diary.get_entry_path(&entry_date);
165
166        let content = fs::read_to_string(entry_path).unwrap();
167
168        assert!(content.contains("Test content"));
169        assert!(content.contains("Tag"));
170    }
171
172    #[test]
173    #[should_panic(expected = "value: NoContent")]
174    fn add_empty_string() {
175        let config = testing::temp_config();
176        testing::default_init(config.diary_path());
177
178        let entry_date = Local.with_ymd_and_hms(2021, 11, 6, 0, 0, 0).unwrap();
179        testing::new_entry(&config, &entry_date);
180
181        let diary = Diary::from_config(&config).unwrap();
182        let opts = AddOptions {
183            tag: Some("Tag".to_owned()),
184            content: None,
185        };
186        add(&opts, &diary, &entry_date, test_empty_string_getter).unwrap();
187    }
188
189    #[test]
190    #[should_panic(expected = "value: NoEntry")]
191    fn add_to_nonexistent_file() {
192        let config = testing::temp_config();
193        testing::default_init(config.diary_path());
194
195        let diary = Diary::from_config(&config).unwrap();
196
197        let entry_date = Local.with_ymd_and_hms(2021, 11, 6, 0, 0, 0).unwrap();
198        let opts = AddOptions {
199            tag: Some("Tag".to_owned()),
200            content: None,
201        };
202        add(&opts, &diary, &entry_date, test_string_getter).unwrap();
203    }
204}