obsidian_parser/obfile/obfile_in_memory.rs
1use crate::error::Error;
2use crate::obfile::{ObFile, 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, 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 fn content(&self) -> String {
38 self.content.clone()
39 }
40
41 fn path(&self) -> Option<PathBuf> {
42 self.path.clone()
43 }
44
45 fn properties(&self) -> T {
46 self.properties.clone()
47 }
48
49 /// Parses a string into an in-memory Obsidian note representation
50 ///
51 /// # Arguments
52 /// * `raw_text` - Full note text including optional frontmatter
53 /// * `path` - Optional source path for reference
54 ///
55 /// # Process
56 /// 1. Splits text into frontmatter/content sections
57 /// 2. Parses YAML frontmatter if present
58 /// 3. Stores content without frontmatter delimiters
59 ///
60 /// # Errors
61 /// - [`Error::InvalidFormat`] for malformed frontmatter
62 /// - [`Error::Yaml`] for invalid YAML syntax
63 ///
64 /// # Example
65 /// ```rust
66 /// use obsidian_parser::prelude::*;
67 /// use serde::Deserialize;
68 ///
69 /// #[derive(Deserialize, Clone, Default)]
70 /// struct NoteProperties {
71 /// title: String
72 /// }
73 ///
74 /// let note = r#"---
75 /// title: Example
76 /// ---
77 /// Content"#;
78 ///
79 /// let file: ObFileInMemory<NoteProperties> = ObFileInMemory::from_string(note, None::<&str>).unwrap();
80 /// assert_eq!(file.properties().title, "Example");
81 /// assert_eq!(file.content(), "Content");
82 /// ```
83 fn from_string<P: AsRef<std::path::Path>>(
84 raw_text: &str,
85 path: Option<P>,
86 ) -> Result<Self, Error> {
87 let path_buf = path.map(|x| x.as_ref().to_path_buf());
88
89 #[cfg(feature = "logging")]
90 log::trace!(
91 "Parsing in-memory note{}",
92 path_buf
93 .as_ref()
94 .map(|p| format!(" from {}", p.display()))
95 .unwrap_or_default()
96 );
97
98 let (valid_properties, parts) = parse_obfile(raw_text);
99 match (valid_properties, &parts[..]) {
100 (false, _) => {
101 #[cfg(feature = "logging")]
102 log::debug!("No frontmatter found, storing raw content");
103
104 Ok(Self {
105 content: raw_text.to_string(),
106 path: path_buf,
107 properties: T::default(),
108 })
109 }
110 (true, [_, properties, content]) => {
111 #[cfg(feature = "logging")]
112 log::debug!("Frontmatter detected, parsing properties");
113
114 Ok(Self {
115 content: content.trim().to_string(),
116 properties: serde_yml::from_str(properties)?,
117 path: path_buf,
118 })
119 }
120 _ => {
121 #[cfg(feature = "logging")]
122 log::error!(
123 "Invalid frontmatter format{}",
124 path_buf
125 .as_ref()
126 .map(|p| format!(" in {}", p.display()))
127 .unwrap_or_default()
128 );
129
130 Err(Error::InvalidFormat)
131 }
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::obfile::tests::{impl_all_tests_from_file, impl_all_tests_from_string};
140
141 impl_all_tests_from_string!(ObFileInMemory);
142 impl_all_tests_from_file!(ObFileInMemory);
143}