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) -> String {
41 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) -> Option<T> {
51 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 /// assert_eq!(file.properties().unwrap().title, "Example");
86 /// assert_eq!(file.content(), "Content");
87 /// ```
88 fn from_string<P: AsRef<std::path::Path>>(
89 raw_text: &str,
90 path: Option<P>,
91 ) -> Result<Self, Error> {
92 let path_buf = path.map(|x| x.as_ref().to_path_buf());
93
94 #[cfg(feature = "logging")]
95 log::trace!(
96 "Parsing in-memory note{}",
97 path_buf
98 .as_ref()
99 .map(|p| format!(" from {}", p.display()))
100 .unwrap_or_default()
101 );
102
103 match parse_obfile(raw_text)? {
104 ResultParse::WithProperties {
105 content,
106 properties,
107 } => {
108 #[cfg(feature = "logging")]
109 log::trace!("Frontmatter detected, parsing properties");
110
111 Ok(Self {
112 content: content.to_string(),
113 properties: Some(serde_yml::from_str(properties)?),
114 path: path_buf,
115 })
116 }
117 ResultParse::WithoutProperties => {
118 #[cfg(feature = "logging")]
119 log::trace!("No frontmatter found, storing raw content");
120
121 Ok(Self {
122 content: raw_text.to_string(),
123 path: path_buf,
124 properties: None,
125 })
126 }
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::obfile::impl_tests::{impl_all_tests_from_file, impl_all_tests_from_string};
135
136 impl_all_tests_from_string!(ObFileInMemory);
137 impl_all_tests_from_file!(ObFileInMemory);
138}