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