1use super::Note;
4use serde::de::DeserializeOwned;
5use std::{io::Read, path::Path};
6
7pub trait NoteFromString: Note
9where
10 Self::Properties: DeserializeOwned,
11{
12 fn from_string(raw_text: impl AsRef<str>) -> Result<Self, Self::Error>;
17}
18
19pub trait NoteFromReader: Note
21where
22 Self::Properties: DeserializeOwned,
23 Self::Error: From<std::io::Error>,
24{
25 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 let text = unsafe { String::from_utf8_unchecked(data) };
45
46 Self::from_string(&text)
47 }
48}
49
50#[cfg(not(target_family = "wasm"))]
52pub trait NoteFromFile: Note
53where
54 Self::Properties: DeserializeOwned,
55 Self::Error: From<std::io::Error>,
56{
57 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}