obsidian_parser/obfile/
obfile_in_memory.rs

1//! In-memory representation of an Obsidian note file
2
3use crate::error::Error;
4use crate::obfile::{DefaultProperties, ObFile, ResultParse, parse_obfile};
5use serde::de::DeserializeOwned;
6use std::borrow::Cow;
7use std::path::Path;
8use std::path::PathBuf;
9
10/// In-memory representation of an Obsidian note file
11///
12/// This struct provides full access to parsed note content, properties, and path.
13/// It stores the entire file contents in memory, making it suitable for:
14/// - Frequent access to note content
15/// - Transformation or analysis workflows
16/// - Environments with fast storage (SSD/RAM disks)
17///
18/// # Performance Considerations
19/// - Uses ~2x memory of original file size (UTF-8 + deserialized properties)
20/// - Preferred for small-to-medium vaults (<10k notes)
21///
22/// For large vaults or read-heavy workflows, consider [`ObFileOnDisk`].
23///
24/// [`ObFileOnDisk`]: crate::obfile::obfile_on_disk::ObFileOnDisk
25#[derive(Debug, Default, PartialEq, Eq, Clone)]
26pub struct ObFileInMemory<T = DefaultProperties>
27where
28    T: DeserializeOwned + Clone,
29{
30    /// Markdown content body (without frontmatter)
31    content: String,
32
33    /// Source file path (if loaded from disk)
34    path: Option<PathBuf>,
35
36    /// Parsed frontmatter properties
37    properties: Option<T>,
38}
39
40impl<T: DeserializeOwned + Clone> ObFile<T> for ObFileInMemory<T> {
41    #[inline]
42    fn content(&self) -> Result<Cow<'_, str>, Error> {
43        Ok(Cow::Borrowed(&self.content))
44    }
45
46    #[inline]
47    fn path(&self) -> Option<Cow<'_, Path>> {
48        self.path.as_ref().map(|p| Cow::Borrowed(p.as_path()))
49    }
50
51    #[inline]
52    fn properties(&self) -> Result<Option<Cow<'_, T>>, Error> {
53        Ok(self.properties.as_ref().map(|p| Cow::Borrowed(p)))
54    }
55
56    /// Parses a string into an in-memory Obsidian note representation
57    ///
58    /// # Arguments
59    /// * `raw_text` - Full note text including optional frontmatter
60    /// * `path` - Optional source path for reference
61    ///
62    /// # Process
63    /// 1. Splits text into frontmatter/content sections
64    /// 2. Parses YAML frontmatter if present
65    /// 3. Stores content without frontmatter delimiters
66    ///
67    /// # Errors
68    /// - [`Error::InvalidFormat`] for malformed frontmatter
69    /// - [`Error::Yaml`] for invalid YAML syntax
70    ///
71    /// # Example
72    /// ```rust
73    /// use obsidian_parser::prelude::*;
74    /// use serde::Deserialize;
75    ///
76    /// #[derive(Deserialize, Clone, Default)]
77    /// struct NoteProperties {
78    ///     title: String
79    /// }
80    ///
81    /// let note = r#"---
82    /// title: Example
83    /// ---
84    /// Content"#;
85    ///
86    /// let file: ObFileInMemory<NoteProperties> = ObFileInMemory::from_string(note, None::<&str>).unwrap();
87    /// let properties = file.properties().unwrap().unwrap();
88    ///
89    /// assert_eq!(properties.title, "Example");
90    /// assert_eq!(file.content().unwrap(), "Content");
91    /// ```
92    fn from_string<P: AsRef<std::path::Path>>(
93        raw_text: &str,
94        path: Option<P>,
95    ) -> Result<Self, Error> {
96        let path_buf = path.map(|x| x.as_ref().to_path_buf());
97
98        #[cfg(feature = "logging")]
99        log::trace!(
100            "Parsing in-memory note{}",
101            path_buf
102                .as_ref()
103                .map(|p| format!(" from {}", p.display()))
104                .unwrap_or_default()
105        );
106
107        match parse_obfile(raw_text)? {
108            ResultParse::WithProperties {
109                content,
110                properties,
111            } => {
112                #[cfg(feature = "logging")]
113                log::trace!("Frontmatter detected, parsing properties");
114
115                Ok(Self {
116                    content: content.to_string(),
117                    properties: Some(serde_yml::from_str(properties)?),
118                    path: path_buf,
119                })
120            }
121            ResultParse::WithoutProperties => {
122                #[cfg(feature = "logging")]
123                log::trace!("No frontmatter found, storing raw content");
124
125                Ok(Self {
126                    content: raw_text.to_string(),
127                    path: path_buf,
128                    properties: None,
129                })
130            }
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::obfile::impl_tests::{impl_all_tests_from_file, impl_all_tests_from_string};
139
140    impl_all_tests_from_string!(ObFileInMemory);
141    impl_all_tests_from_file!(ObFileInMemory);
142}