Skip to main content

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