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