obsidian_parser/obfile/
obfile_in_memory.rs

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