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