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, 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 match parse_obfile(raw_text)? {
99 ResultParse::WithProperties {
100 content,
101 properties,
102 } => {
103 #[cfg(feature = "logging")]
104 log::trace!("Frontmatter detected, parsing properties");
105
106 Ok(Self {
107 content: content.to_string(),
108 properties: serde_yml::from_str(properties)?,
109 path: path_buf,
110 })
111 }
112 ResultParse::WithoutProperties => {
113 #[cfg(feature = "logging")]
114 log::trace!("No frontmatter found, storing raw content");
115
116 Ok(Self {
117 content: raw_text.to_string(),
118 path: path_buf,
119 properties: T::default(),
120 })
121 }
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::obfile::impl_tests::{impl_all_tests_from_file, impl_all_tests_from_string};
130
131 impl_all_tests_from_string!(ObFileInMemory);
132 impl_all_tests_from_file!(ObFileInMemory);
133}