1pub mod obfile_in_memory;
2pub mod obfile_on_disk;
3
4use crate::error::Error;
5use serde::de::DeserializeOwned;
6use std::{
7 collections::HashMap,
8 path::{Path, PathBuf},
9};
10
11pub trait ObFile<T = HashMap<String, serde_yml::Value>>: Sized
34where
35 T: DeserializeOwned + Default + Clone + Send,
36{
37 fn content(&self) -> String;
43
44 fn path(&self) -> Option<PathBuf>;
48
49 fn properties(&self) -> T;
55
56 fn from_string<P: AsRef<Path>>(raw_text: &str, path: Option<P>) -> Result<Self, Error>;
66
67 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
76 let path_buf = path.as_ref().to_path_buf();
77
78 #[cfg(feature = "logging")]
79 log::trace!("Parse obsidian file from file: {}", path_buf.display());
80
81 let data = std::fs::read(path)?;
82 let text = String::from_utf8(data)?;
83
84 Self::from_string(&text, Some(path_buf))
85 }
86}
87
88pub trait ObFileDefault: ObFile<HashMap<String, serde_yml::Value>> {
93 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error>;
99
100 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error>;
106}
107
108impl<T> ObFileDefault for T
109where
110 T: ObFile<HashMap<String, serde_yml::Value>>,
111{
112 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error> {
113 Self::from_string(text, path)
114 }
115
116 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
117 Self::from_file(path)
118 }
119}
120
121#[derive(Debug, PartialEq)]
122enum ResultParse<'a> {
123 WithProperties {
124 content: &'a str,
125 properties: &'a str,
126 },
127 WithoutProperties,
128}
129
130fn parse_obfile(raw_text: &str) -> Result<ResultParse, Error> {
131 let mut lines = raw_text.lines();
132 if lines.next().unwrap_or_default().trim_end() == "---" {
133 let closed = raw_text["---".len()..]
134 .find("---")
135 .ok_or(Error::InvalidFormat)?;
136
137 return Ok(ResultParse::WithProperties {
138 content: raw_text[(closed + 2 * "...".len())..].trim(),
139 properties: raw_text["...".len()..(closed + "...".len())].trim(),
140 });
141 }
142
143 Ok(ResultParse::WithoutProperties)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::{ResultParse, parse_obfile};
149 use crate::test_utils::init_test_logger;
150
151 #[test]
152 fn parse_obfile_without_properties() {
153 init_test_logger();
154 let test_data = "test_data";
155 let result = parse_obfile(test_data).unwrap();
156
157 assert_eq!(result, ResultParse::WithoutProperties);
158 }
159
160 #[test]
161 fn parse_obfile_with_properties() {
162 init_test_logger();
163 let test_data = "---\nproperties data\n---\ntest data";
164 let result = parse_obfile(test_data).unwrap();
165
166 assert_eq!(
167 result,
168 ResultParse::WithProperties {
169 content: "test data",
170 properties: "properties data"
171 }
172 );
173 }
174
175 #[test]
176 fn parse_obfile_without_properties_but_with_closed() {
177 init_test_logger();
178 let test_data1 = "test_data---";
179 let test_data2 = "test_data\n---\n";
180
181 let result1 = parse_obfile(test_data1).unwrap();
182 let result2 = parse_obfile(test_data2).unwrap();
183
184 assert_eq!(result1, ResultParse::WithoutProperties);
185 assert_eq!(result2, ResultParse::WithoutProperties);
186 }
187
188 #[test]
189 #[should_panic]
190 fn parse_obfile_with_properties_but_without_closed() {
191 init_test_logger();
192 let test_data = "---\nproperties data\ntest data";
193 let _ = parse_obfile(test_data).unwrap();
194 }
195
196 #[test]
197 fn parse_obfile_without_properties_but_with_spaces() {
198 init_test_logger();
199 let test_data = " ---\ndata";
200
201 let result = parse_obfile(test_data).unwrap();
202 assert_eq!(result, ResultParse::WithoutProperties);
203 }
204
205 #[test]
206 fn parse_obfile_with_properties_but_check_trim_end() {
207 init_test_logger();
208 let test_data = "---\r\nproperties data\r\n---\r \ntest data";
209 let result = parse_obfile(test_data).unwrap();
210
211 assert_eq!(
212 result,
213 ResultParse::WithProperties {
214 content: "test data",
215 properties: "properties data"
216 }
217 );
218 }
219}
220
221#[cfg(test)]
222pub(crate) mod impl_tests {
223 use super::*;
224 use crate::test_utils::init_test_logger;
225 use serde::Deserialize;
226 use std::io::Write;
227 use tempfile::NamedTempFile;
228
229 pub(crate) static TEST_DATA: &str = "---\n\
230topic: life\n\
231created: 2025-03-16\n\
232---\n\
233Test data\n\
234---\n\
235Two test data";
236
237 #[derive(Debug, Deserialize, Default, PartialEq, Clone)]
238 pub(crate) struct TestProperties {
239 pub(crate) topic: String,
240 pub(crate) created: String,
241 }
242
243 pub(crate) fn from_string<T: ObFile>() -> Result<(), Error> {
244 init_test_logger();
245 let file = T::from_string(TEST_DATA, None::<&str>)?;
246 let properties = file.properties();
247
248 assert_eq!(properties["topic"], "life");
249 assert_eq!(properties["created"], "2025-03-16");
250 assert_eq!(file.content(), "Test data\n---\nTwo test data");
251 Ok(())
252 }
253
254 pub(crate) fn from_string_without_properties<T: ObFile>() -> Result<(), Error> {
255 init_test_logger();
256 let test_data = "TEST_DATA";
257 let file = T::from_string(test_data, None::<&str>)?;
258 let properties = file.properties();
259
260 assert_eq!(properties.len(), 0);
261 assert_eq!(file.content(), test_data);
262 Ok(())
263 }
264
265 pub(crate) fn from_string_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
266 init_test_logger();
267 let broken_data = "---\n\
268 asdfv:--fs\n\
269 sfsf\n\
270 ---\n\
271 TestData";
272
273 assert!(matches!(
274 T::from_string(broken_data, None::<&str>),
275 Err(Error::Yaml(_))
276 ));
277 Ok(())
278 }
279
280 pub(crate) fn from_string_invalid_format<T: ObFile>() -> Result<(), Error> {
281 init_test_logger();
282 let broken_data = "---\n";
283
284 assert!(matches!(
285 T::from_string(broken_data, None::<&str>),
286 Err(Error::InvalidFormat)
287 ));
288 Ok(())
289 }
290
291 pub(crate) fn from_string_with_unicode<T: ObFile>() -> Result<(), Error> {
292 init_test_logger();
293 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
294 let file = T::from_string(data, None::<&str>)?;
295 let properties = file.properties();
296
297 assert_eq!(properties["data"], "💩");
298 assert_eq!(file.content(), "Super data 💩💩💩");
299 Ok(())
300 }
301
302 pub(crate) fn from_string_space_with_properties<T: ObFile>() -> Result<(), Error> {
303 init_test_logger();
304 let data = " ---\ntest: test-data\n---\n";
305 let file = T::from_string(data, None::<&str>)?;
306 let properties = file.properties();
307
308 assert_eq!(file.content(), data);
309 assert_eq!(properties.len(), 0);
310 Ok(())
311 }
312
313 pub(crate) fn from_file<T: ObFile>() -> Result<(), Error> {
314 init_test_logger();
315 let mut temp_file = NamedTempFile::new().unwrap();
316 temp_file.write_all(b"TEST_DATA").unwrap();
317
318 let file = T::from_file(temp_file.path()).unwrap();
319 assert_eq!(file.content(), "TEST_DATA");
320 assert_eq!(file.path().unwrap(), temp_file.path());
321 assert_eq!(file.properties().len(), 0);
322 Ok(())
323 }
324
325 pub(crate) fn from_file_without_properties<T: ObFile>() -> Result<(), Error> {
326 init_test_logger();
327 let test_data = "TEST_DATA";
328 let mut test_file = NamedTempFile::new().unwrap();
329 test_file.write_all(test_data.as_bytes()).unwrap();
330
331 let file = T::from_file(test_file.path())?;
332 let properties = file.properties();
333
334 assert_eq!(properties.len(), 0);
335 assert_eq!(file.content(), test_data);
336 Ok(())
337 }
338
339 pub(crate) fn from_file_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
340 init_test_logger();
341 let broken_data = "---\n\
342 asdfv:--fs\n\
343 sfsf\n\
344 ---\n\
345 TestData";
346
347 let mut test_file = NamedTempFile::new().unwrap();
348 test_file.write_all(broken_data.as_bytes()).unwrap();
349
350 assert!(matches!(
351 T::from_file(test_file.path()),
352 Err(Error::Yaml(_))
353 ));
354 Ok(())
355 }
356
357 pub(crate) fn from_file_invalid_format<T: ObFile>() -> Result<(), Error> {
358 init_test_logger();
359 let broken_data = "---\n";
360 let mut test_file = NamedTempFile::new().unwrap();
361 test_file.write_all(broken_data.as_bytes()).unwrap();
362
363 assert!(matches!(
364 T::from_file(test_file.path()),
365 Err(Error::InvalidFormat)
366 ));
367 Ok(())
368 }
369
370 pub(crate) fn from_file_with_unicode<T: ObFile>() -> Result<(), Error> {
371 init_test_logger();
372 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
373 let mut test_file = NamedTempFile::new().unwrap();
374 test_file.write_all(data.as_bytes()).unwrap();
375
376 let file = T::from_file(test_file.path())?;
377 let properties = file.properties();
378
379 assert_eq!(properties["data"], "💩");
380 assert_eq!(file.content(), "Super data 💩💩💩");
381 Ok(())
382 }
383
384 pub(crate) fn from_file_space_with_properties<T: ObFile>() -> Result<(), Error> {
385 init_test_logger();
386 let data = " ---\ntest: test-data\n---\n";
387 let mut test_file = NamedTempFile::new().unwrap();
388 test_file.write_all(data.as_bytes()).unwrap();
389
390 let file = T::from_string(data, None::<&str>)?;
391 let properties = file.properties();
392
393 assert_eq!(file.content(), data);
394 assert_eq!(properties.len(), 0);
395 Ok(())
396 }
397
398 macro_rules! impl_test_for_obfile {
399 ($name_test:ident, $fn_test:ident, $impl_obfile:path) => {
400 #[test]
401 fn $name_test() {
402 $fn_test::<$impl_obfile>().unwrap();
403 }
404 };
405 }
406
407 pub(crate) use impl_test_for_obfile;
408
409 macro_rules! impl_all_tests_from_string {
410 ($impl_obfile:path) => {
411 #[allow(unused_imports)]
412 use crate::obfile::impl_tests::*;
413
414 impl_test_for_obfile!(impl_from_string, from_string, $impl_obfile);
415
416 impl_test_for_obfile!(
417 impl_from_string_without_properties,
418 from_string_without_properties,
419 $impl_obfile
420 );
421 impl_test_for_obfile!(
422 impl_from_string_with_invalid_yaml,
423 from_string_with_invalid_yaml,
424 $impl_obfile
425 );
426 impl_test_for_obfile!(
427 impl_from_string_invalid_format,
428 from_string_invalid_format,
429 $impl_obfile
430 );
431 impl_test_for_obfile!(
432 impl_from_string_with_unicode,
433 from_string_with_unicode,
434 $impl_obfile
435 );
436 impl_test_for_obfile!(
437 impl_from_string_space_with_properties,
438 from_string_space_with_properties,
439 $impl_obfile
440 );
441 };
442 }
443
444 macro_rules! impl_all_tests_from_file {
445 ($impl_obfile:path) => {
446 #[allow(unused_imports)]
447 use crate::obfile::impl_tests::*;
448
449 impl_test_for_obfile!(impl_from_file, from_file, $impl_obfile);
450
451 impl_test_for_obfile!(
452 impl_from_file_without_properties,
453 from_file_without_properties,
454 $impl_obfile
455 );
456 impl_test_for_obfile!(
457 impl_from_file_with_invalid_yaml,
458 from_file_with_invalid_yaml,
459 $impl_obfile
460 );
461 impl_test_for_obfile!(
462 impl_from_file_invalid_format,
463 from_file_invalid_format,
464 $impl_obfile
465 );
466 impl_test_for_obfile!(
467 impl_from_file_with_unicode,
468 from_file_with_unicode,
469 $impl_obfile
470 );
471 impl_test_for_obfile!(
472 impl_from_file_space_with_properties,
473 from_file_space_with_properties,
474 $impl_obfile
475 );
476 };
477 }
478
479 pub(crate) use impl_all_tests_from_file;
480 pub(crate) use impl_all_tests_from_string;
481}