obsidian_parser/obfile/
mod.rs

1//! Represents an Obsidian note file with frontmatter properties and content
2
3pub mod obfile_in_memory;
4pub mod obfile_on_disk;
5
6use crate::error::Error;
7use serde::{Serialize, de::DeserializeOwned};
8use std::{borrow::Cow, collections::HashMap, fs::OpenOptions, io::Write, path::Path};
9
10pub(crate) type DefaultProperties = HashMap<String, serde_yml::Value>;
11
12/// Represents an Obsidian note file with frontmatter properties and content
13///
14/// This trait provides a standardized interface for working with Obsidian markdown files,
15/// handling frontmatter parsing, content extraction, and file operations.
16///
17/// # Type Parameters
18/// - `T`: Frontmatter properties type
19///
20/// # Example
21/// ```no_run
22/// use obsidian_parser::prelude::*;
23/// use serde::Deserialize;
24///
25/// #[derive(Deserialize, Clone)]
26/// struct NoteProperties {
27///     topic: String,
28///     created: String,
29/// }
30///
31/// let note: ObFileInMemory<NoteProperties> = ObFile::from_file("note.md").unwrap();
32/// let properties = note.properties().unwrap().unwrap();
33/// println!("Note topic: {}", properties.topic);
34/// ```
35///
36/// # Other
37/// To write and modify [`ObFile`] to a file, use the [`ObFileFlush`] trait.
38pub trait ObFile<T = DefaultProperties>: Sized
39where
40    T: DeserializeOwned + Clone,
41{
42    /// Returns the main content body of the note (excluding frontmatter)
43    ///
44    /// # Implementation Notes
45    /// - Strips YAML frontmatter if present
46    /// - Preserves original formatting and whitespace
47    ///
48    /// # Errors
49    /// Usually errors are related to [`Error::Io`]
50    fn content(&self) -> Result<Cow<'_, str>, Error>;
51
52    /// Returns the source file path if available
53    ///
54    /// Returns [`None`] for in-memory notes without physical storage
55    fn path(&self) -> Option<Cow<'_, Path>>;
56
57    /// Returns the parsed properties of frontmatter
58    ///
59    /// Returns [`None`] if the note has no properties
60    ///
61    /// # Errors
62    /// Usually errors are related to [`Error::Io`]
63    fn properties(&self) -> Result<Option<Cow<'_, T>>, Error>;
64
65    /// Get note name
66    fn note_name(&self) -> Option<String> {
67        self.path().as_ref().map(|path| {
68            path.file_stem()
69                .expect("Path is not file")
70                .to_string_lossy()
71                .to_string()
72        })
73    }
74
75    /// Parses an Obsidian note from a string
76    ///
77    /// # Arguments
78    /// - `raw_text`: Raw markdown content with optional YAML frontmatter
79    /// - `path`: Optional source path for reference
80    ///
81    /// # Errors
82    /// - [`Error::InvalidFormat`] for malformed frontmatter
83    /// - [`Error::Yaml`] for invalid YAML syntax
84    fn from_string<P: AsRef<Path>>(raw_text: &str, path: Option<P>) -> Result<Self, Error>;
85
86    /// Parses an Obsidian note from a file
87    ///
88    /// # Arguments
89    /// - `path`: Filesystem path to markdown file
90    ///
91    /// # Errors
92    /// - [`Error::Io`] for filesystem errors
93    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
94        let path_buf = path.as_ref().to_path_buf();
95
96        #[cfg(feature = "logging")]
97        log::trace!("Parse obsidian file from file: {}", path_buf.display());
98
99        let data = std::fs::read(path)?;
100
101        // SAFETY: Notes files in Obsidian (`*.md`) ensure that the file is encoded in UTF-8
102        let text = unsafe { String::from_utf8_unchecked(data) };
103
104        Self::from_string(&text, Some(path_buf))
105    }
106}
107
108/// Default implementation using [`HashMap`] for properties
109///
110/// Automatically implemented for all `ObFile<HashMap<..>>` types.
111/// Provides identical interface with explicitly named methods.
112pub trait ObFileDefault: ObFile<DefaultProperties> {
113    /// Same as [`ObFile::from_string`] with default properties type
114    ///
115    /// # Errors
116    /// - [`Error::InvalidFormat`] for malformed frontmatter
117    /// - [`Error::Yaml`] for invalid YAML syntax
118    fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error>;
119
120    /// Same as [`ObFile::from_file`] with default properties type
121    ///
122    /// # Errors
123    /// - [`Error::Io`] for filesystem errors
124    fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error>;
125}
126
127/// Represents an Obsidian note file with frontmatter properties and content
128/// for flush to file
129///
130/// To use this trait, `T` must implement [`serde::Serialize`]
131pub trait ObFileFlush<T = DefaultProperties>: ObFile<T>
132where
133    T: DeserializeOwned + Serialize + Clone,
134{
135    /// Flush only `content`
136    ///
137    /// Ignore if path is `None`
138    ///
139    /// # Errors
140    /// - [`Error::Io`] for filesystem errors
141    fn flush_content(&self, open_option: &OpenOptions) -> Result<(), Error> {
142        if let Some(path) = self.path() {
143            let text = std::fs::read_to_string(&path)?;
144            let parsed = parse_obfile(&text)?;
145
146            let mut file = open_option.open(path)?;
147
148            match parsed {
149                ResultParse::WithProperties {
150                    content: _,
151                    properties,
152                } => file.write_all(
153                    format!("---\n{}\n---\n{}", properties, self.content()?).as_bytes(),
154                )?,
155                ResultParse::WithoutProperties => file.write_all(self.content()?.as_bytes())?,
156            }
157        }
158
159        Ok(())
160    }
161
162    /// Flush only `content`
163    ///
164    /// Ignore if path is `None`
165    /// # Errors
166    /// - [`Error::Io`] for filesystem errors
167    fn flush_properties(&self, open_option: &OpenOptions) -> Result<(), Error> {
168        if let Some(path) = self.path() {
169            let text = std::fs::read_to_string(&path)?;
170            let parsed = parse_obfile(&text)?;
171
172            let mut file = open_option.open(path)?;
173
174            match parsed {
175                ResultParse::WithProperties {
176                    content,
177                    properties: _,
178                } => match self.properties()? {
179                    Some(properties) => file.write_all(
180                        format!(
181                            "---\n{}\n---\n{}",
182                            serde_yml::to_string(&properties)?,
183                            content
184                        )
185                        .as_bytes(),
186                    )?,
187                    None => file.write_all(self.content()?.as_bytes())?,
188                },
189                ResultParse::WithoutProperties => file.write_all(self.content()?.as_bytes())?,
190            }
191        }
192
193        Ok(())
194    }
195
196    /// Flush [`ObFile`] to [`self.path()`]
197    ///
198    /// Ignore if path is `None`
199    /// # Errors
200    /// - [`Error::Io`] for filesystem errors
201    fn flush(&self, open_option: &OpenOptions) -> Result<(), Error> {
202        if let Some(path) = self.path() {
203            let mut file = open_option.open(path)?;
204
205            match self.properties()? {
206                Some(properties) => file.write_all(
207                    format!(
208                        "---\n{}\n---\n{}",
209                        serde_yml::to_string(&properties)?,
210                        self.content()?
211                    )
212                    .as_bytes(),
213                )?,
214                None => file.write_all(self.content()?.as_bytes())?,
215            }
216        }
217
218        Ok(())
219    }
220}
221
222/// Parses Obsidian-style links in note content
223///
224/// Handles all link formats:
225/// - `[[Note]]`
226/// - `[[Note|Alias]]`
227/// - `[[Note^block]]`
228/// - `[[Note#heading]]`
229/// - `[[Note#heading|Alias]]`
230///
231/// # Example
232/// ```
233/// # use obsidian_parser::obfile::parse_links;
234/// let content = "[[Physics]] and [[Math|Mathematics]]";
235/// let links: Vec<_> = parse_links(content).collect();
236/// assert_eq!(links, vec!["Physics", "Math"]);
237/// ```
238pub fn parse_links(text: &str) -> impl Iterator<Item = &str> {
239    text.match_indices("[[").filter_map(move |(start_pos, _)| {
240        let end_pos = text[start_pos + 2..].find("]]")?;
241        let inner = &text[start_pos + 2..start_pos + 2 + end_pos];
242
243        let note_name = inner
244            .split('#')
245            .next()?
246            .split('^')
247            .next()?
248            .split('|')
249            .next()?
250            .trim();
251
252        Some(note_name)
253    })
254}
255
256impl<T> ObFileDefault for T
257where
258    T: ObFile<DefaultProperties>,
259{
260    fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error> {
261        Self::from_string(text, path)
262    }
263
264    fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
265        Self::from_file(path)
266    }
267}
268
269#[derive(Debug, PartialEq)]
270enum ResultParse<'a> {
271    WithProperties {
272        content: &'a str,
273        properties: &'a str,
274    },
275    WithoutProperties,
276}
277
278fn parse_obfile(raw_text: &str) -> Result<ResultParse<'_>, Error> {
279    let have_start_properties = raw_text
280        .lines()
281        .next()
282        .is_some_and(|line| line.trim_end() == "---");
283
284    if have_start_properties {
285        let closed = raw_text["---".len()..]
286            .find("---")
287            .ok_or(Error::InvalidFormat)?;
288
289        return Ok(ResultParse::WithProperties {
290            content: raw_text[(closed + 2 * "...".len())..].trim(),
291            properties: raw_text["...".len()..(closed + "...".len())].trim(),
292        });
293    }
294
295    Ok(ResultParse::WithoutProperties)
296}
297
298#[cfg(test)]
299mod tests {
300    use super::{ResultParse, parse_obfile};
301    use crate::test_utils::init_test_logger;
302
303    #[test]
304    fn parse_obfile_without_properties() {
305        init_test_logger();
306        let test_data = "test_data";
307        let result = parse_obfile(test_data).unwrap();
308
309        assert_eq!(result, ResultParse::WithoutProperties);
310    }
311
312    #[test]
313    fn parse_obfile_with_properties() {
314        init_test_logger();
315        let test_data = "---\nproperties data\n---\ntest data";
316        let result = parse_obfile(test_data).unwrap();
317
318        assert_eq!(
319            result,
320            ResultParse::WithProperties {
321                content: "test data",
322                properties: "properties data"
323            }
324        );
325    }
326
327    #[test]
328    fn parse_obfile_without_properties_but_with_closed() {
329        init_test_logger();
330        let test_data1 = "test_data---";
331        let test_data2 = "test_data\n---\n";
332
333        let result1 = parse_obfile(test_data1).unwrap();
334        let result2 = parse_obfile(test_data2).unwrap();
335
336        assert_eq!(result1, ResultParse::WithoutProperties);
337        assert_eq!(result2, ResultParse::WithoutProperties);
338    }
339
340    #[test]
341    #[should_panic]
342    fn parse_obfile_with_properties_but_without_closed() {
343        init_test_logger();
344        let test_data = "---\nproperties data\ntest data";
345        let _ = parse_obfile(test_data).unwrap();
346    }
347
348    #[test]
349    fn parse_obfile_with_() {
350        init_test_logger();
351        let test_data = "---properties data";
352
353        let result = parse_obfile(test_data).unwrap();
354        assert_eq!(result, ResultParse::WithoutProperties);
355    }
356
357    #[test]
358    fn parse_obfile_without_properties_but_with_spaces() {
359        init_test_logger();
360        let test_data = "   ---\ndata";
361
362        let result = parse_obfile(test_data).unwrap();
363        assert_eq!(result, ResultParse::WithoutProperties);
364    }
365
366    #[test]
367    fn parse_obfile_with_properties_but_check_trim_end() {
368        init_test_logger();
369        let test_data = "---\r\nproperties data\r\n---\r   \ntest data";
370        let result = parse_obfile(test_data).unwrap();
371
372        assert_eq!(
373            result,
374            ResultParse::WithProperties {
375                content: "test data",
376                properties: "properties data"
377            }
378        );
379    }
380
381    #[test]
382    fn test_parse_links() {
383        init_test_logger();
384        let test_data =
385            "[[Note]] [[Note|Alias]] [[Note^block]] [[Note#Heading|Alias]] [[Note^block|Alias]]";
386
387        let ds: Vec<_> = super::parse_links(test_data).collect();
388
389        assert!(ds.iter().all(|x| *x == "Note"))
390    }
391}
392
393#[cfg(test)]
394pub(crate) mod impl_tests {
395    use super::*;
396    use crate::test_utils::init_test_logger;
397    use std::io::Write;
398    use tempfile::NamedTempFile;
399
400    pub(crate) static TEST_DATA: &str = "---\n\
401topic: life\n\
402created: 2025-03-16\n\
403---\n\
404Test data\n\
405---\n\
406Two test data";
407
408    pub(crate) fn from_string<T: ObFile>() -> Result<(), Error> {
409        init_test_logger();
410        let file = T::from_string(TEST_DATA, None::<&str>)?;
411        let properties = file.properties().unwrap().unwrap();
412
413        assert_eq!(properties["topic"], "life");
414        assert_eq!(properties["created"], "2025-03-16");
415        assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
416        Ok(())
417    }
418
419    pub(crate) fn from_string_note_name<T: ObFile>() -> Result<(), Error> {
420        init_test_logger();
421        let file1 = T::from_string(TEST_DATA, None::<&str>)?;
422        let file2 = T::from_string(TEST_DATA, Some("Super node.md"))?;
423
424        assert_eq!(file1.note_name(), None);
425        assert_eq!(file2.note_name(), Some("Super node".to_string()));
426        Ok(())
427    }
428
429    pub(crate) fn from_string_without_properties<T: ObFile>() -> Result<(), Error> {
430        init_test_logger();
431        let test_data = "TEST_DATA";
432        let file = T::from_string(test_data, None::<&str>)?;
433
434        assert_eq!(file.properties().unwrap(), None);
435        assert_eq!(file.content().unwrap(), test_data);
436        Ok(())
437    }
438
439    pub(crate) fn from_string_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
440        init_test_logger();
441        let broken_data = "---\n\
442    asdfv:--fs\n\
443    sfsf\n\
444    ---\n\
445    TestData";
446
447        assert!(matches!(
448            T::from_string(broken_data, None::<&str>),
449            Err(Error::Yaml(_))
450        ));
451        Ok(())
452    }
453
454    pub(crate) fn from_string_invalid_format<T: ObFile>() -> Result<(), Error> {
455        init_test_logger();
456        let broken_data = "---\n";
457
458        assert!(matches!(
459            T::from_string(broken_data, None::<&str>),
460            Err(Error::InvalidFormat)
461        ));
462        Ok(())
463    }
464
465    pub(crate) fn from_string_with_unicode<T: ObFile>() -> Result<(), Error> {
466        init_test_logger();
467        let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
468        let file = T::from_string(data, None::<&str>)?;
469        let properties = file.properties().unwrap().unwrap();
470
471        assert_eq!(properties["data"], "💩");
472        assert_eq!(file.content().unwrap(), "Super data 💩💩💩");
473        Ok(())
474    }
475
476    pub(crate) fn from_string_space_with_properties<T: ObFile>() -> Result<(), Error> {
477        init_test_logger();
478        let data = "  ---\ntest: test-data\n---\n";
479        let file = T::from_string(data, None::<&str>)?;
480        let properties = file.properties().unwrap();
481
482        assert_eq!(file.content().unwrap(), data);
483        assert_eq!(properties, None);
484        Ok(())
485    }
486
487    pub(crate) fn from_file<T: ObFile>() -> Result<(), Error> {
488        init_test_logger();
489        let mut temp_file = NamedTempFile::new().unwrap();
490        temp_file.write_all(b"TEST_DATA").unwrap();
491
492        let file = T::from_file(temp_file.path()).unwrap();
493        assert_eq!(file.content().unwrap(), "TEST_DATA");
494        assert_eq!(file.path().unwrap(), temp_file.path());
495        assert_eq!(file.properties().unwrap(), None);
496        Ok(())
497    }
498
499    pub(crate) fn from_file_note_name<T: ObFile>() -> Result<(), Error> {
500        init_test_logger();
501        let mut temp_file = NamedTempFile::new().unwrap();
502        temp_file.write_all(b"TEST_DATA").unwrap();
503
504        let name_temp_file = temp_file
505            .path()
506            .file_stem()
507            .unwrap()
508            .to_string_lossy()
509            .to_string();
510
511        let file = T::from_file(temp_file.path()).unwrap();
512
513        assert_eq!(file.note_name(), Some(name_temp_file));
514        Ok(())
515    }
516
517    pub(crate) fn from_file_without_properties<T: ObFile>() -> Result<(), Error> {
518        init_test_logger();
519        let test_data = "TEST_DATA";
520        let mut test_file = NamedTempFile::new().unwrap();
521        test_file.write_all(test_data.as_bytes()).unwrap();
522
523        let file = T::from_file(test_file.path())?;
524
525        assert_eq!(file.properties().unwrap(), None);
526        assert_eq!(file.content().unwrap(), test_data);
527        Ok(())
528    }
529
530    pub(crate) fn from_file_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
531        init_test_logger();
532        let broken_data = "---\n\
533    asdfv:--fs\n\
534    sfsf\n\
535    ---\n\
536    TestData";
537
538        let mut test_file = NamedTempFile::new().unwrap();
539        test_file.write_all(broken_data.as_bytes()).unwrap();
540
541        assert!(matches!(
542            T::from_file(test_file.path()),
543            Err(Error::Yaml(_))
544        ));
545        Ok(())
546    }
547
548    pub(crate) fn from_file_invalid_format<T: ObFile>() -> Result<(), Error> {
549        init_test_logger();
550        let broken_data = "---\n";
551        let mut test_file = NamedTempFile::new().unwrap();
552        test_file.write_all(broken_data.as_bytes()).unwrap();
553
554        assert!(matches!(
555            T::from_file(test_file.path()),
556            Err(Error::InvalidFormat)
557        ));
558        Ok(())
559    }
560
561    pub(crate) fn from_file_with_unicode<T: ObFile>() -> Result<(), Error> {
562        init_test_logger();
563        let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
564        let mut test_file = NamedTempFile::new().unwrap();
565        test_file.write_all(data.as_bytes()).unwrap();
566
567        let file = T::from_file(test_file.path())?;
568        let properties = file.properties().unwrap().unwrap();
569
570        assert_eq!(properties["data"], "💩");
571        assert_eq!(file.content().unwrap(), "Super data 💩💩💩");
572        Ok(())
573    }
574
575    pub(crate) fn from_file_space_with_properties<T: ObFile>() -> Result<(), Error> {
576        init_test_logger();
577        let data = "  ---\ntest: test-data\n---\n";
578        let mut test_file = NamedTempFile::new().unwrap();
579        test_file.write_all(data.as_bytes()).unwrap();
580
581        let file = T::from_string(data, None::<&str>)?;
582
583        assert_eq!(file.content().unwrap(), data);
584        assert_eq!(file.properties().unwrap(), None);
585        Ok(())
586    }
587
588    pub(crate) fn flush_properties<T: ObFileFlush>() -> Result<(), Error> {
589        let mut test_file = NamedTempFile::new().unwrap();
590        test_file.write_all(TEST_DATA.as_bytes()).unwrap();
591
592        let file = T::from_file_default(test_file.path())?;
593        let open_options = OpenOptions::new().write(true).create(false).clone();
594        file.flush_properties(&open_options)?;
595        drop(file);
596
597        let file = T::from_file_default(test_file.path())?;
598        let properties = file.properties()?.unwrap();
599        assert_eq!(properties["topic"], "life");
600        assert_eq!(properties["created"], "2025-03-16");
601        assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
602
603        Ok(())
604    }
605
606    pub(crate) fn flush_content<T: ObFileFlush>() -> Result<(), Error> {
607        let mut test_file = NamedTempFile::new().unwrap();
608        test_file.write_all(TEST_DATA.as_bytes()).unwrap();
609
610        let file = T::from_file_default(test_file.path())?;
611        let open_options = OpenOptions::new().write(true).create(false).clone();
612        file.flush_content(&open_options)?;
613        drop(file);
614
615        let file = T::from_file_default(test_file.path())?;
616        let properties = file.properties()?.unwrap();
617        assert_eq!(properties["topic"], "life");
618        assert_eq!(properties["created"], "2025-03-16");
619        assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
620
621        Ok(())
622    }
623
624    pub(crate) fn flush<T: ObFileFlush>() -> Result<(), Error> {
625        let mut test_file = NamedTempFile::new().unwrap();
626        test_file.write_all(TEST_DATA.as_bytes()).unwrap();
627
628        let file = T::from_file_default(test_file.path())?;
629        let open_options = OpenOptions::new().write(true).create(false).clone();
630        file.flush(&open_options)?;
631        drop(file);
632
633        let file = T::from_file_default(test_file.path())?;
634        let properties = file.properties()?.unwrap();
635        assert_eq!(properties["topic"], "life");
636        assert_eq!(properties["created"], "2025-03-16");
637        assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
638
639        Ok(())
640    }
641
642    macro_rules! impl_test_for_obfile {
643        ($name_test:ident, $fn_test:ident, $impl_obfile:path) => {
644            #[test]
645            fn $name_test() {
646                $fn_test::<$impl_obfile>().unwrap();
647            }
648        };
649    }
650
651    pub(crate) use impl_test_for_obfile;
652
653    macro_rules! impl_all_tests_from_string {
654        ($impl_obfile:path) => {
655            #[allow(unused_imports)]
656            use $crate::obfile::impl_tests::*;
657
658            impl_test_for_obfile!(impl_from_string, from_string, $impl_obfile);
659
660            impl_test_for_obfile!(
661                impl_from_string_note_name,
662                from_string_note_name,
663                $impl_obfile
664            );
665            impl_test_for_obfile!(
666                impl_from_string_without_properties,
667                from_string_without_properties,
668                $impl_obfile
669            );
670            impl_test_for_obfile!(
671                impl_from_string_with_invalid_yaml,
672                from_string_with_invalid_yaml,
673                $impl_obfile
674            );
675            impl_test_for_obfile!(
676                impl_from_string_invalid_format,
677                from_string_invalid_format,
678                $impl_obfile
679            );
680            impl_test_for_obfile!(
681                impl_from_string_with_unicode,
682                from_string_with_unicode,
683                $impl_obfile
684            );
685            impl_test_for_obfile!(
686                impl_from_string_space_with_properties,
687                from_string_space_with_properties,
688                $impl_obfile
689            );
690        };
691    }
692
693    macro_rules! impl_all_tests_from_file {
694        ($impl_obfile:path) => {
695            #[allow(unused_imports)]
696            use $crate::obfile::impl_tests::*;
697
698            impl_test_for_obfile!(impl_from_file, from_file, $impl_obfile);
699            impl_test_for_obfile!(impl_from_file_note_name, from_file_note_name, $impl_obfile);
700
701            impl_test_for_obfile!(
702                impl_from_file_without_properties,
703                from_file_without_properties,
704                $impl_obfile
705            );
706            impl_test_for_obfile!(
707                impl_from_file_with_invalid_yaml,
708                from_file_with_invalid_yaml,
709                $impl_obfile
710            );
711            impl_test_for_obfile!(
712                impl_from_file_invalid_format,
713                from_file_invalid_format,
714                $impl_obfile
715            );
716            impl_test_for_obfile!(
717                impl_from_file_with_unicode,
718                from_file_with_unicode,
719                $impl_obfile
720            );
721            impl_test_for_obfile!(
722                impl_from_file_space_with_properties,
723                from_file_space_with_properties,
724                $impl_obfile
725            );
726        };
727    }
728
729    macro_rules! impl_all_tests_flush {
730        ($impl_obfile:path) => {
731            #[allow(unused_imports)]
732            use $crate::obfile::impl_tests::*;
733
734            impl_test_for_obfile!(impl_flush, flush, $impl_obfile);
735            impl_test_for_obfile!(impl_flush_content, flush_content, $impl_obfile);
736            impl_test_for_obfile!(impl_flush_properties, flush_properties, $impl_obfile);
737        };
738    }
739
740    pub(crate) use impl_all_tests_flush;
741    pub(crate) use impl_all_tests_from_file;
742    pub(crate) use impl_all_tests_from_string;
743}