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