1use chrono::{DateTime, Utc};
2
3use serde::{Deserialize, Serialize};
4
5#[cfg(feature = "json")]
6mod json;
7#[cfg(feature = "json")]
8pub use json::JsonDataProvide;
9
10#[cfg(feature = "sqlite")]
11mod sqlite;
12#[cfg(feature = "sqlite")]
13pub use sqlite::SqliteDataProvide;
14
15pub const TRANSFER_DATA_VERSION: u16 = 100;
16
17#[derive(Debug, thiserror::Error)]
18pub enum ModifyEntryError {
19 #[error("{0}")]
20 ValidationError(String),
21 #[error("{0}")]
22 DataError(#[from] anyhow::Error),
23}
24
25#[allow(async_fn_in_trait)]
27pub trait DataProvider {
28 async fn load_all_entries(&self) -> anyhow::Result<Vec<Entry>>;
29 async fn add_entry(&self, entry: EntryDraft) -> Result<Entry, ModifyEntryError>;
30 async fn restore_entry(&self, entry: Entry) -> Result<Entry, ModifyEntryError>;
32 async fn remove_entry(&self, entry_id: u32) -> anyhow::Result<()>;
33 async fn update_entry(&self, entry: Entry) -> Result<Entry, ModifyEntryError>;
34 async fn get_export_object(&self, entries_ids: &[u32]) -> anyhow::Result<EntriesDTO>;
35 async fn import_entries(&self, entries_dto: EntriesDTO) -> anyhow::Result<()> {
36 debug_assert_eq!(
37 TRANSFER_DATA_VERSION, entries_dto.version,
38 "Version mismatches check if there is a need to do a converting to the data"
39 );
40
41 for entry_draft in entries_dto.entries {
42 self.add_entry(entry_draft).await?;
43 }
44
45 Ok(())
46 }
47 async fn assign_priority_to_entries(&self, priority: u32) -> anyhow::Result<()>;
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Entry {
53 pub id: u32,
54 pub date: DateTime<Utc>,
55 pub title: String,
56 pub content: String,
57 #[serde(default)]
58 pub tags: Vec<String>,
59 #[serde(default)]
60 pub priority: Option<u32>,
61}
62
63impl Entry {
64 #[allow(dead_code)]
65 pub fn new(
66 id: u32,
67 date: DateTime<Utc>,
68 title: String,
69 content: String,
70 tags: Vec<String>,
71 priority: Option<u32>,
72 ) -> Self {
73 Self {
74 id,
75 date,
76 title,
77 content,
78 tags,
79 priority,
80 }
81 }
82
83 pub fn from_draft(id: u32, draft: EntryDraft) -> Self {
84 Self {
85 id,
86 date: draft.date,
87 title: draft.title,
88 content: draft.content,
89 tags: draft.tags,
90 priority: draft.priority,
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct EntryDraft {
97 pub date: DateTime<Utc>,
98 pub title: String,
99 pub content: String,
100 pub tags: Vec<String>,
101 pub priority: Option<u32>,
102}
103
104impl EntryDraft {
105 pub fn new(
106 date: DateTime<Utc>,
107 title: String,
108 tags: Vec<String>,
109 priority: Option<u32>,
110 ) -> Self {
111 let content = String::new();
112 Self {
113 date,
114 title,
115 content,
116 tags,
117 priority,
118 }
119 }
120
121 #[must_use]
122 pub fn with_content(mut self, content: String) -> Self {
123 self.content = content;
124 self
125 }
126
127 pub fn from_entry(entry: Entry) -> Self {
128 Self {
129 date: entry.date,
130 title: entry.title,
131 content: entry.content,
132 tags: entry.tags,
133 priority: entry.priority,
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct EntriesDTO {
141 pub version: u16,
142 pub entries: Vec<EntryDraft>,
143}
144
145impl EntriesDTO {
146 pub fn new(entries: Vec<EntryDraft>) -> Self {
147 Self {
148 version: TRANSFER_DATA_VERSION,
149 entries,
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use std::sync::Mutex;
157
158 use chrono::TimeZone;
159
160 use super::*;
161
162 fn sample_draft() -> EntryDraft {
163 EntryDraft {
164 date: Utc.with_ymd_and_hms(2024, 1, 2, 3, 4, 5).unwrap(),
165 title: String::from("Draft"),
166 content: String::from("Body"),
167 tags: vec![String::from("one"), String::from("two")],
168 priority: Some(3),
169 }
170 }
171
172 struct ImportStubProvider {
173 added_entries: Mutex<Vec<EntryDraft>>,
174 fail_on_call: Option<usize>,
175 }
176
177 impl ImportStubProvider {
178 fn new(fail_on_call: Option<usize>) -> Self {
179 Self {
180 added_entries: Mutex::new(Vec::new()),
181 fail_on_call,
182 }
183 }
184 }
185
186 impl DataProvider for ImportStubProvider {
187 async fn load_all_entries(&self) -> anyhow::Result<Vec<Entry>> {
188 unreachable!("not used in these tests");
189 }
190
191 async fn add_entry(&self, entry: EntryDraft) -> Result<Entry, ModifyEntryError> {
192 let mut added_entries = self.added_entries.lock().unwrap();
193 let call_idx = added_entries.len();
194 added_entries.push(entry.clone());
195
196 if self.fail_on_call == Some(call_idx) {
197 return Err(ModifyEntryError::ValidationError(format!(
198 "fail on {call_idx}"
199 )));
200 }
201
202 Ok(Entry::from_draft(call_idx as u32, entry))
203 }
204
205 async fn restore_entry(&self, _entry: Entry) -> Result<Entry, ModifyEntryError> {
206 unreachable!("not used in these tests");
207 }
208
209 async fn remove_entry(&self, _entry_id: u32) -> anyhow::Result<()> {
210 unreachable!("not used in these tests");
211 }
212
213 async fn update_entry(&self, _entry: Entry) -> Result<Entry, ModifyEntryError> {
214 unreachable!("not used in these tests");
215 }
216
217 async fn get_export_object(&self, _entries_ids: &[u32]) -> anyhow::Result<EntriesDTO> {
218 unreachable!("not used in these tests");
219 }
220
221 async fn assign_priority_to_entries(&self, _priority: u32) -> anyhow::Result<()> {
222 unreachable!("not used in these tests");
223 }
224 }
225
226 #[test]
227 fn draft_to_entry() {
228 let draft = sample_draft();
229
230 let entry = Entry::from_draft(7, draft.clone());
231
232 assert_eq!(entry.id, 7);
233 assert_eq!(entry.date, draft.date);
234 assert_eq!(entry.title, draft.title);
235 assert_eq!(entry.content, draft.content);
236 assert_eq!(entry.tags, draft.tags);
237 assert_eq!(entry.priority, draft.priority);
238 }
239
240 #[test]
241 fn with_content_replaces_only_body() {
242 let draft = sample_draft();
243
244 let updated = draft.clone().with_content(String::from("Updated"));
245
246 assert_eq!(updated.content, "Updated");
247 assert_eq!(updated.date, draft.date);
248 assert_eq!(updated.title, draft.title);
249 assert_eq!(updated.tags, draft.tags);
250 assert_eq!(updated.priority, draft.priority);
251 }
252
253 #[test]
254 fn from_entry_drops_id_only() {
255 let entry = Entry::new(
256 11,
257 Utc.with_ymd_and_hms(2023, 11, 12, 13, 14, 15).unwrap(),
258 String::from("Title"),
259 String::from("Content"),
260 vec![String::from("tag")],
261 Some(2),
262 );
263
264 let draft = EntryDraft::from_entry(entry.clone());
265
266 assert_eq!(draft.date, entry.date);
267 assert_eq!(draft.title, entry.title);
268 assert_eq!(draft.content, entry.content);
269 assert_eq!(draft.tags, entry.tags);
270 assert_eq!(draft.priority, entry.priority);
271 }
272
273 #[test]
274 fn dto_sets_version() {
275 let dto = EntriesDTO::new(vec![sample_draft()]);
276
277 assert_eq!(dto.version, TRANSFER_DATA_VERSION);
278 assert_eq!(dto.entries, vec![sample_draft()]);
279 }
280
281 #[tokio::test]
282 async fn import_entries_keeps_order() {
283 let provider = ImportStubProvider::new(None);
284 let entries = vec![
285 sample_draft(),
286 EntryDraft::new(
287 Utc.with_ymd_and_hms(2025, 6, 7, 8, 9, 10).unwrap(),
288 String::from("Second"),
289 vec![String::from("x")],
290 None,
291 ),
292 ];
293
294 provider
295 .import_entries(EntriesDTO::new(entries.clone()))
296 .await
297 .unwrap();
298
299 let added_entries = provider.added_entries.lock().unwrap().clone();
300 assert_eq!(added_entries, entries);
301 }
302
303 #[tokio::test]
304 async fn import_entries_stops_on_error() {
305 let provider = ImportStubProvider::new(Some(1));
306 let entries = vec![
307 sample_draft(),
308 EntryDraft::new(
309 Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
310 String::from("Second"),
311 vec![],
312 None,
313 ),
314 EntryDraft::new(
315 Utc.with_ymd_and_hms(2025, 1, 2, 0, 0, 0).unwrap(),
316 String::from("Third"),
317 vec![],
318 None,
319 ),
320 ];
321
322 let err = provider
323 .import_entries(EntriesDTO::new(entries.clone()))
324 .await
325 .unwrap_err();
326
327 assert_eq!(err.to_string(), "fail on 1");
328
329 let added_entries = provider.added_entries.lock().unwrap().clone();
331 assert_eq!(added_entries, entries[..2].to_vec());
332 }
333}