obsidian_parser/note/
note_read.rs

1//! Impl traits for reading notes
2
3use super::Note;
4use serde::de::DeserializeOwned;
5use std::{io::Read, path::Path};
6
7/// Trait for parses an Obsidian note from a string
8pub trait NoteFromString: Note
9where
10    Self::Properties: DeserializeOwned,
11{
12    /// Parses an Obsidian note from a string
13    ///
14    /// # Arguments
15    /// - `raw_text`: Raw markdown content with optional YAML frontmatter
16    fn from_string(raw_text: impl AsRef<str>) -> Result<Self, Self::Error>;
17}
18
19/// Trait for parses an Obsidian note from a reader
20pub trait NoteFromReader: Note
21where
22    Self::Properties: DeserializeOwned,
23    Self::Error: From<std::io::Error>,
24{
25    /// Parses an Obsidian note from a reader
26    fn from_reader(read: &mut impl Read) -> Result<Self, Self::Error>;
27}
28
29impl<N> NoteFromReader for N
30where
31    N: NoteFromString,
32    N::Properties: DeserializeOwned,
33    N::Error: From<std::io::Error>,
34{
35    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
36    fn from_reader(read: &mut impl Read) -> Result<Self, Self::Error> {
37        #[cfg(feature = "tracing")]
38        tracing::trace!("Parse obsidian file from reader");
39
40        let mut data = Vec::new();
41        read.read_to_end(&mut data)?;
42
43        // SAFETY: Notes files in Obsidian (`*.md`) ensure that the file is encoded in UTF-8
44        let text = unsafe { String::from_utf8_unchecked(data) };
45
46        Self::from_string(&text)
47    }
48}
49
50/// Trait for parses an Obsidian note from a file
51#[cfg(not(target_family = "wasm"))]
52pub trait NoteFromFile: Note
53where
54    Self::Properties: DeserializeOwned,
55    Self::Error: From<std::io::Error>,
56{
57    /// Parses an Obsidian note from a file
58    ///
59    /// # Arguments
60    /// - `path`: Filesystem path to markdown file
61    fn from_file(path: impl AsRef<Path>) -> Result<Self, Self::Error>;
62}
63
64#[cfg(test)]
65pub(crate) mod tests {
66    use super::*;
67    use crate::{
68        note::{DefaultProperties, parser},
69        test_utils::is_error,
70    };
71    use std::{
72        borrow::Cow,
73        io::{Cursor, Write},
74        path::PathBuf,
75    };
76    use tempfile::NamedTempFile;
77
78    const TEST_DATA: &str = "---\n\
79topic: life\n\
80created: 2025-03-16\n\
81---\n\
82Test data\n\
83---\n\
84Two test data";
85
86    const BROKEN_DATA: &str = "---\n\
87    asdfv:--fs\n\
88    sfsf\n\
89    ---\n\
90    TestData";
91
92    const UNICODE_DATA: &str = "---\ndata: 💩\n---\nSuper data 💩💩💩";
93
94    const SPACE_DATA: &str = "  ---\ntest: test-data\n---\n";
95
96    fn test_data<T>(note: T, path: Option<PathBuf>) -> Result<(), T::Error>
97    where
98        T: Note<Properties = DefaultProperties>,
99        T::Error: From<std::io::Error>,
100    {
101        let path = path.map(|p| Cow::Owned(p));
102        let properties = note.properties()?.unwrap();
103
104        assert_eq!(properties["topic"], "life");
105        assert_eq!(properties["created"], "2025-03-16");
106        assert_eq!(note.content()?, "Test data\n---\nTwo test data");
107        assert_eq!(note.path(), path);
108
109        Ok(())
110    }
111
112    fn without_properties<T>(file: T, text: &str) -> Result<(), T::Error>
113    where
114        T: Note<Properties = DefaultProperties>,
115        T::Error: From<std::io::Error>,
116    {
117        assert_eq!(file.properties().unwrap(), None);
118        assert_eq!(file.content().unwrap(), text);
119
120        Ok(())
121    }
122
123    fn invalid_yaml<T>(result: Result<T, T::Error>) -> Result<(), T::Error>
124    where
125        T: Note<Properties = DefaultProperties>,
126        T::Error: From<std::io::Error>,
127    {
128        let error = result.err().unwrap();
129
130        assert!(is_error::<serde_yml::Error>(error));
131        Ok(())
132    }
133
134    fn invalid_format<T>(result: Result<T, T::Error>) -> Result<(), T::Error>
135    where
136        T: Note<Properties = DefaultProperties>,
137        T::Error: From<std::io::Error>,
138    {
139        let error = result.err().unwrap();
140
141        assert!(is_error::<parser::Error>(error));
142        Ok(())
143    }
144
145    fn with_unicode<T>(file: T) -> Result<(), T::Error>
146    where
147        T: Note<Properties = DefaultProperties>,
148    {
149        let properties = file.properties()?.unwrap();
150
151        assert_eq!(properties["data"], "💩");
152        assert_eq!(file.content().unwrap(), "Super data 💩💩💩");
153
154        Ok(())
155    }
156
157    fn space_with_properties<T>(file: T, content: &str) -> Result<(), T::Error>
158    where
159        T: Note<Properties = DefaultProperties>,
160    {
161        let properties = file.properties()?;
162
163        assert_eq!(file.content().unwrap(), content);
164        assert_eq!(properties, None);
165
166        Ok(())
167    }
168
169    pub(crate) fn from_reader<T>() -> Result<(), T::Error>
170    where
171        T: NoteFromReader<Properties = DefaultProperties>,
172        T::Error: From<std::io::Error>,
173    {
174        let mut reader = Cursor::new(TEST_DATA);
175        let file = T::from_reader(&mut reader)?;
176
177        test_data(file, None)?;
178        Ok(())
179    }
180
181    pub(crate) fn from_reader_without_properties<T>() -> Result<(), T::Error>
182    where
183        T: NoteFromReader<Properties = DefaultProperties>,
184        T::Error: From<std::io::Error>,
185    {
186        let test_data = "TEST_DATA";
187        let file = T::from_reader(&mut Cursor::new(test_data))?;
188
189        without_properties(file, test_data)?;
190        Ok(())
191    }
192
193    pub(crate) fn from_reader_invalid_yaml<T>() -> Result<(), T::Error>
194    where
195        T: NoteFromReader<Properties = DefaultProperties>,
196        T::Error: From<std::io::Error>,
197    {
198        let result = T::from_reader(&mut Cursor::new(BROKEN_DATA));
199
200        invalid_yaml(result)?;
201        Ok(())
202    }
203
204    pub(crate) fn from_reader_invalid_format<T>() -> Result<(), T::Error>
205    where
206        T: NoteFromReader<Properties = DefaultProperties>,
207        T::Error: From<std::io::Error>,
208    {
209        let broken_data = "---\n";
210        let result = T::from_reader(&mut Cursor::new(broken_data));
211
212        invalid_format(result)?;
213        Ok(())
214    }
215
216    pub(crate) fn from_reader_with_unicode<T>() -> Result<(), T::Error>
217    where
218        T: NoteFromReader<Properties = DefaultProperties>,
219        T::Error: From<std::io::Error>,
220    {
221        let file = T::from_reader(&mut Cursor::new(UNICODE_DATA))?;
222
223        with_unicode(file)?;
224        Ok(())
225    }
226
227    pub(crate) fn from_reader_space_with_properties<T>() -> Result<(), T::Error>
228    where
229        T: NoteFromReader<Properties = DefaultProperties>,
230        T::Error: From<std::io::Error>,
231    {
232        let file = T::from_reader(&mut Cursor::new(SPACE_DATA))?;
233
234        space_with_properties(file, SPACE_DATA)?;
235        Ok(())
236    }
237
238    pub(crate) fn from_string<T>() -> Result<(), T::Error>
239    where
240        T: NoteFromString<Properties = DefaultProperties>,
241        T::Error: From<std::io::Error>,
242    {
243        let file = T::from_string(TEST_DATA)?;
244
245        test_data(file, None)?;
246        Ok(())
247    }
248
249    pub(crate) fn from_string_without_properties<T>() -> Result<(), T::Error>
250    where
251        T: NoteFromString<Properties = DefaultProperties>,
252        T::Error: From<std::io::Error>,
253    {
254        let test_data = "TEST_DATA";
255        let file = T::from_string(test_data)?;
256
257        without_properties(file, test_data)?;
258        Ok(())
259    }
260
261    pub(crate) fn from_string_with_invalid_yaml<T>() -> Result<(), T::Error>
262    where
263        T: NoteFromString<Properties = DefaultProperties>,
264        T::Error: From<std::io::Error> + From<serde_yml::Error> + 'static,
265    {
266        let result = T::from_string(BROKEN_DATA);
267
268        invalid_yaml(result)?;
269        Ok(())
270    }
271
272    pub(crate) fn from_string_invalid_format<T>() -> Result<(), T::Error>
273    where
274        T: NoteFromString<Properties = DefaultProperties>,
275        T::Error: From<std::io::Error> + From<parser::Error>,
276    {
277        let broken_data = "---\n";
278
279        let result = T::from_string(broken_data);
280        invalid_format(result)?;
281
282        Ok(())
283    }
284
285    pub(crate) fn from_string_with_unicode<T>() -> Result<(), T::Error>
286    where
287        T: NoteFromString<Properties = DefaultProperties>,
288    {
289        let file = T::from_string(UNICODE_DATA)?;
290
291        with_unicode(file)?;
292        Ok(())
293    }
294
295    pub(crate) fn from_string_space_with_properties<T>() -> Result<(), T::Error>
296    where
297        T: NoteFromString<Properties = DefaultProperties>,
298    {
299        let file = T::from_string(SPACE_DATA)?;
300
301        space_with_properties(file, SPACE_DATA)?;
302        Ok(())
303    }
304
305    pub(crate) fn from_file<T>() -> Result<(), T::Error>
306    where
307        T: NoteFromFile<Properties = DefaultProperties>,
308        T::Error: From<std::io::Error>,
309    {
310        let mut temp_file = NamedTempFile::new().unwrap();
311        temp_file.write_all(TEST_DATA.as_bytes()).unwrap();
312
313        let file = T::from_file(temp_file.path()).unwrap();
314
315        test_data(file, Some(temp_file.path().to_path_buf()))?;
316        Ok(())
317    }
318
319    pub(crate) fn from_file_note_name<T>() -> Result<(), T::Error>
320    where
321        T: NoteFromFile<Properties = DefaultProperties>,
322        T::Error: From<std::io::Error>,
323    {
324        let mut temp_file = NamedTempFile::new().unwrap();
325        temp_file.write_all(b"TEST_DATA").unwrap();
326
327        let name_temp_file = temp_file
328            .path()
329            .file_stem()
330            .unwrap()
331            .to_string_lossy()
332            .to_string();
333
334        let file = T::from_file(temp_file.path())?;
335
336        assert_eq!(file.note_name(), Some(name_temp_file));
337        Ok(())
338    }
339
340    pub(crate) fn from_file_without_properties<T>() -> Result<(), T::Error>
341    where
342        T: NoteFromFile<Properties = DefaultProperties>,
343        T::Error: From<std::io::Error>,
344    {
345        let test_data = "TEST_DATA";
346        let mut test_file = NamedTempFile::new().unwrap();
347        test_file.write_all(test_data.as_bytes()).unwrap();
348
349        let file = T::from_file(test_file.path())?;
350
351        without_properties(file, test_data)?;
352        Ok(())
353    }
354
355    pub(crate) fn from_file_with_invalid_yaml<T>() -> Result<(), T::Error>
356    where
357        T: NoteFromFile<Properties = DefaultProperties>,
358        T::Error: From<std::io::Error> + From<serde_yml::Error>,
359    {
360        let mut test_file = NamedTempFile::new().unwrap();
361        test_file.write_all(BROKEN_DATA.as_bytes()).unwrap();
362
363        let result = T::from_file(test_file.path());
364
365        invalid_yaml(result)?;
366        Ok(())
367    }
368
369    pub(crate) fn from_file_invalid_format<T>() -> Result<(), T::Error>
370    where
371        T: NoteFromFile<Properties = DefaultProperties>,
372        T::Error: From<std::io::Error> + From<parser::Error>,
373    {
374        let broken_data = "---\n";
375        let mut test_file = NamedTempFile::new().unwrap();
376        test_file.write_all(broken_data.as_bytes()).unwrap();
377
378        let result = T::from_file(test_file.path());
379
380        invalid_format(result)?;
381        Ok(())
382    }
383
384    pub(crate) fn from_file_with_unicode<T>() -> Result<(), T::Error>
385    where
386        T: NoteFromFile<Properties = DefaultProperties>,
387        T::Error: From<std::io::Error>,
388    {
389        let mut test_file = NamedTempFile::new().unwrap();
390        test_file.write_all(UNICODE_DATA.as_bytes()).unwrap();
391
392        let file = T::from_file(test_file.path())?;
393
394        with_unicode(file)?;
395        Ok(())
396    }
397
398    pub(crate) fn from_file_space_with_properties<T>() -> Result<(), T::Error>
399    where
400        T: NoteFromFile<Properties = DefaultProperties>,
401        T::Error: From<std::io::Error>,
402    {
403        let data = "  ---\ntest: test-data\n---\n";
404        let mut test_file = NamedTempFile::new().unwrap();
405        test_file.write_all(data.as_bytes()).unwrap();
406
407        let file = T::from_file(test_file.path())?;
408
409        space_with_properties(file, data)?;
410        Ok(())
411    }
412
413    macro_rules! impl_all_tests_from_reader {
414        ($impl_note:path) => {
415            #[allow(unused_imports)]
416            use $crate::note::note_read::tests::*;
417
418            impl_test_for_note!(impl_from_reader, from_reader, $impl_note);
419
420            impl_test_for_note!(
421                impl_from_reader_without_properties,
422                from_reader_without_properties,
423                $impl_note
424            );
425            impl_test_for_note!(
426                impl_from_reader_with_invalid_yaml,
427                from_reader_invalid_yaml,
428                $impl_note
429            );
430            impl_test_for_note!(
431                impl_from_reader_invalid_format,
432                from_reader_invalid_format,
433                $impl_note
434            );
435            impl_test_for_note!(
436                impl_from_reader_with_unicode,
437                from_reader_with_unicode,
438                $impl_note
439            );
440            impl_test_for_note!(
441                impl_from_reader_space_with_properties,
442                from_reader_space_with_properties,
443                $impl_note
444            );
445        };
446    }
447
448    macro_rules! impl_all_tests_from_string {
449        ($impl_note:path) => {
450            #[allow(unused_imports)]
451            use $crate::note::note_read::tests::*;
452
453            impl_test_for_note!(impl_from_string, from_string, $impl_note);
454
455            impl_test_for_note!(
456                impl_from_string_without_properties,
457                from_string_without_properties,
458                $impl_note
459            );
460            impl_test_for_note!(
461                impl_from_string_with_invalid_yaml,
462                from_string_with_invalid_yaml,
463                $impl_note
464            );
465            impl_test_for_note!(
466                impl_from_string_invalid_format,
467                from_string_invalid_format,
468                $impl_note
469            );
470            impl_test_for_note!(
471                impl_from_string_with_unicode,
472                from_string_with_unicode,
473                $impl_note
474            );
475            impl_test_for_note!(
476                impl_from_string_space_with_properties,
477                from_string_space_with_properties,
478                $impl_note
479            );
480        };
481    }
482
483    macro_rules! impl_all_tests_from_file {
484        ($impl_note:path) => {
485            #[allow(unused_imports)]
486            use $crate::note::impl_tests::*;
487
488            impl_test_for_note!(impl_from_file, from_file, $impl_note);
489            impl_test_for_note!(impl_from_file_note_name, from_file_note_name, $impl_note);
490
491            impl_test_for_note!(
492                impl_from_file_without_properties,
493                from_file_without_properties,
494                $impl_note
495            );
496            impl_test_for_note!(
497                impl_from_file_with_invalid_yaml,
498                from_file_with_invalid_yaml,
499                $impl_note
500            );
501            impl_test_for_note!(
502                impl_from_file_invalid_format,
503                from_file_invalid_format,
504                $impl_note
505            );
506            impl_test_for_note!(
507                impl_from_file_with_unicode,
508                from_file_with_unicode,
509                $impl_note
510            );
511            impl_test_for_note!(
512                impl_from_file_space_with_properties,
513                from_file_space_with_properties,
514                $impl_note
515            );
516        };
517    }
518
519    pub(crate) use impl_all_tests_from_file;
520    pub(crate) use impl_all_tests_from_reader;
521    pub(crate) use impl_all_tests_from_string;
522}