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}