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