1use std::{fs, path::Path};
2
3use doing_error::{Error, Result};
4
5use crate::{Document, serializer};
6
7pub fn create_file(path: &Path, default_section: &str) -> Result<()> {
12 if fs::metadata(path).map(|m| m.len() > 0).unwrap_or(false) {
13 return Ok(());
14 }
15
16 if let Some(parent) = path.parent() {
17 fs::create_dir_all(parent)?;
18 }
19
20 fs::write(path, format!("{default_section}:\n"))?;
21 Ok(())
22}
23
24pub fn read_file(path: &Path) -> Result<Document> {
26 let content = fs::read_to_string(path)?;
27 Ok(Document::parse(&content))
28}
29
30pub fn write_file(doc: &Document, path: &Path) -> Result<()> {
36 let content = serializer::serialize(doc);
37
38 let parent = path.parent().ok_or_else(|| {
39 Error::Io(std::io::Error::new(
40 std::io::ErrorKind::InvalidInput,
41 "path has no parent directory",
42 ))
43 })?;
44
45 let temp = tempfile::NamedTempFile::new_in(parent)?;
46 fs::write(temp.path(), &content)?;
47 temp.persist(path).map_err(|e| Error::Io(e.error))?;
48
49 Ok(())
50}
51
52#[cfg(test)]
53mod test {
54 use super::*;
55 use crate::{Entry, Note, Section, Tags};
56
57 mod create_file {
58 use pretty_assertions::assert_eq;
59
60 use super::*;
61
62 #[test]
63 fn it_creates_a_new_file_with_default_section() {
64 let dir = tempfile::tempdir().unwrap();
65 let path = dir.path().join("test.md");
66
67 create_file(&path, "Currently").unwrap();
68
69 let content = fs::read_to_string(&path).unwrap();
70 assert_eq!(content, "Currently:\n");
71 }
72
73 #[test]
74 fn it_creates_parent_directories() {
75 let dir = tempfile::tempdir().unwrap();
76 let path = dir.path().join("nested/deep/test.md");
77
78 create_file(&path, "Currently").unwrap();
79
80 assert!(path.exists());
81 }
82
83 #[test]
84 fn it_does_not_overwrite_existing_non_empty_file() {
85 let dir = tempfile::tempdir().unwrap();
86 let path = dir.path().join("test.md");
87 fs::write(&path, "Already here\n").unwrap();
88
89 create_file(&path, "Currently").unwrap();
90
91 let content = fs::read_to_string(&path).unwrap();
92 assert_eq!(content, "Already here\n");
93 }
94
95 #[test]
96 fn it_overwrites_empty_file() {
97 let dir = tempfile::tempdir().unwrap();
98 let path = dir.path().join("test.md");
99 fs::write(&path, "").unwrap();
100
101 create_file(&path, "Currently").unwrap();
102
103 let content = fs::read_to_string(&path).unwrap();
104 assert_eq!(content, "Currently:\n");
105 }
106 }
107
108 mod read_file {
109 use chrono::{Local, TimeZone};
110 use pretty_assertions::assert_eq;
111
112 use super::*;
113
114 #[test]
115 fn it_reads_and_parses_a_doing_file() {
116 let dir = tempfile::tempdir().unwrap();
117 let path = dir.path().join("test.md");
118 fs::write(
119 &path,
120 "Currently:\n\t- 2024-03-17 14:30 | Test task <aaaabbbbccccddddeeeeffffaaaabbbb>\n",
121 )
122 .unwrap();
123
124 let doc = read_file(&path).unwrap();
125
126 assert!(doc.has_section("Currently"));
127 let entries = doc.entries_in_section("Currently");
128 assert_eq!(entries.len(), 1);
129 assert_eq!(entries[0].title(), "Test task");
130 assert_eq!(
131 entries[0].date(),
132 Local.with_ymd_and_hms(2024, 3, 17, 14, 30, 0).unwrap()
133 );
134 }
135
136 #[test]
137 fn it_returns_error_for_missing_file() {
138 let dir = tempfile::tempdir().unwrap();
139 let path = dir.path().join("nonexistent.md");
140
141 let result = read_file(&path);
142
143 assert!(result.is_err());
144 }
145 }
146
147 mod write_file {
148 use chrono::{Local, TimeZone};
149 use pretty_assertions::assert_eq;
150
151 use super::*;
152
153 #[test]
154 fn it_writes_document_to_file() {
155 let dir = tempfile::tempdir().unwrap();
156 let path = dir.path().join("test.md");
157 let mut doc = Document::new();
158 let mut section = Section::new("Currently");
159 section.add_entry(Entry::new(
160 Local.with_ymd_and_hms(2024, 3, 17, 14, 30, 0).unwrap(),
161 "Test task",
162 Tags::new(),
163 Note::new(),
164 "Currently",
165 Some("aaaabbbbccccddddeeeeffffaaaabbbb"),
166 ));
167 doc.add_section(section);
168
169 write_file(&doc, &path).unwrap();
170
171 let content = fs::read_to_string(&path).unwrap();
172 assert!(content.contains("Currently:"));
173 assert!(content.contains("Test task"));
174 }
175
176 #[test]
177 fn it_round_trips_through_read_and_write() {
178 let dir = tempfile::tempdir().unwrap();
179 let path = dir.path().join("test.md");
180 let original = "\
181Currently:
182\t- 2024-03-17 14:30 | Working on feature @coding <aaaabbbbccccddddeeeeffffaaaabbbb>
183\t\tA note about the work
184Archive:
185\t- 2024-03-16 10:00 | Old task @done(2024-03-16 11:00) <bbbbccccddddeeeeffffaaaabbbbcccc>";
186 fs::write(&path, original).unwrap();
187
188 let doc = read_file(&path).unwrap();
189 write_file(&doc, &path).unwrap();
190
191 let content = fs::read_to_string(&path).unwrap();
192 assert_eq!(content, original);
193 }
194 }
195}