obsidian_parser/note/
note_once_cell.rs1use crate::note::parser::{self, ResultParse, parse_note};
10use crate::note::{DefaultProperties, Note};
11use serde::de::DeserializeOwned;
12use std::borrow::Cow;
13use std::cell::OnceCell;
14use std::path::Path;
15use std::path::PathBuf;
16use thiserror::Error;
17
18#[derive(Debug, Default, PartialEq, Eq, Clone)]
26pub struct NoteOnceCell<T = DefaultProperties>
27where
28 T: Clone + DeserializeOwned,
29{
30 path: PathBuf,
32
33 content: OnceCell<String>,
35
36 properties: OnceCell<Option<T>>,
38}
39
40#[derive(Debug, Error)]
42pub enum Error {
43 #[error("IO error: {0}")]
45 IO(#[from] std::io::Error),
46
47 #[error("Invalid frontmatter format")]
61 InvalidFormat(#[from] parser::Error),
62
63 #[error("YAML parsing error: {0}")]
73 Yaml(#[from] serde_yml::Error),
74
75 #[error("Path: `{0}` is not a directory")]
85 IsNotFile(PathBuf),
86}
87
88impl<T> Note for NoteOnceCell<T>
89where
90 T: DeserializeOwned + Clone,
91{
92 type Properties = T;
93 type Error = self::Error;
94
95 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(path = %self.path.display())))]
102 fn properties(&self) -> Result<Option<Cow<'_, T>>, Error> {
103 #[cfg(feature = "tracing")]
104 tracing::trace!("Get properties from file");
105
106 if let Some(properties) = self.properties.get() {
107 return Ok(properties.as_ref().map(|value| Cow::Borrowed(value)));
108 }
109
110 let data = std::fs::read(&self.path)?;
111
112 let raw_text = unsafe { String::from_utf8_unchecked(data) };
114
115 let result = match parse_note(&raw_text)? {
116 ResultParse::WithProperties {
117 content: _,
118 properties,
119 } => {
120 #[cfg(feature = "tracing")]
121 tracing::trace!("Frontmatter detected, parsing properties");
122
123 Some(serde_yml::from_str(properties)?)
124 }
125 ResultParse::WithoutProperties => {
126 #[cfg(feature = "tracing")]
127 tracing::trace!("No frontmatter found, storing raw content");
128
129 None
130 }
131 };
132
133 let _ = self.properties.set(result.clone()); Ok(result.map(|value| Cow::Owned(value)))
135 }
136
137 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(path = %self.path.display())))]
149 fn content(&self) -> Result<Cow<'_, str>, Error> {
150 #[cfg(feature = "tracing")]
151 tracing::trace!("Get content from file");
152
153 if let Some(content) = self.content.get() {
154 return Ok(Cow::Borrowed(content));
155 }
156
157 let data = std::fs::read(&self.path)?;
158
159 let raw_text = unsafe { String::from_utf8_unchecked(data) };
161
162 let result = match parse_note(&raw_text)? {
163 ResultParse::WithProperties {
164 content,
165 properties: _,
166 } => {
167 #[cfg(feature = "tracing")]
168 tracing::trace!("Frontmatter detected, parsing properties");
169
170 content.to_string()
171 }
172 ResultParse::WithoutProperties => {
173 #[cfg(feature = "tracing")]
174 tracing::trace!("No frontmatter found, storing raw content");
175
176 raw_text
177 }
178 };
179
180 let _ = self.content.set(result.clone()); Ok(Cow::Owned(result))
182 }
183
184 #[inline]
186 fn path(&self) -> Option<Cow<'_, Path>> {
187 Some(Cow::Borrowed(&self.path))
188 }
189}
190
191impl<T> NoteOnceCell<T>
192where
193 T: DeserializeOwned + Clone,
194{
195 #[inline]
197 pub fn set_path(&mut self, path: PathBuf) {
198 self.path = path;
199 }
200}
201
202#[cfg(not(target_family = "wasm"))]
203impl<T> crate::prelude::NoteFromFile for NoteOnceCell<T>
204where
205 T: DeserializeOwned + Clone,
206{
207 fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
209 let path = path.as_ref().to_path_buf();
210
211 if !path.is_file() {
212 return Err(Error::IsNotFile(path));
213 }
214
215 Ok(Self {
216 path,
217 content: OnceCell::default(),
218 properties: OnceCell::default(),
219 })
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::note::NoteDefault;
227 use crate::note::impl_tests::impl_test_for_note;
228 use crate::note::note_aliases::tests::{from_file_have_aliases, from_file_have_not_aliases};
229 use crate::note::note_is_todo::tests::{from_file_is_not_todo, from_file_is_todo};
230 use crate::note::note_read::tests::{from_file, from_file_with_unicode};
231 use crate::note::note_write::tests::impl_all_tests_flush;
232 use std::io::Write;
233 use tempfile::NamedTempFile;
234
235 impl_all_tests_flush!(NoteOnceCell);
236 impl_test_for_note!(impl_from_file, from_file, NoteOnceCell);
237
238 impl_test_for_note!(
239 impl_from_file_with_unicode,
240 from_file_with_unicode,
241 NoteOnceCell
242 );
243
244 impl_test_for_note!(impl_from_file_is_todo, from_file_is_todo, NoteOnceCell);
245 impl_test_for_note!(
246 impl_from_file_is_not_todo,
247 from_file_is_not_todo,
248 NoteOnceCell
249 );
250
251 impl_test_for_note!(
252 impl_from_file_have_aliases,
253 from_file_have_aliases,
254 NoteOnceCell
255 );
256 impl_test_for_note!(
257 impl_from_file_have_not_aliases,
258 from_file_have_not_aliases,
259 NoteOnceCell
260 );
261
262 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
263 #[test]
264 #[should_panic]
265 fn use_from_file_with_path_not_file() {
266 let temp_dir = tempfile::tempdir().unwrap();
267
268 NoteOnceCell::from_file_default(temp_dir.path()).unwrap();
269 }
270
271 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
272 #[test]
273 fn get_path() {
274 let test_file = NamedTempFile::new().unwrap();
275 let file = NoteOnceCell::from_file_default(test_file.path()).unwrap();
276
277 assert_eq!(file.path().unwrap(), test_file.path());
278 assert_eq!(file.path, test_file.path());
279 }
280
281 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
282 #[test]
283 fn get_content() {
284 let test_data = "DATA";
285 let mut test_file = NamedTempFile::new().unwrap();
286 test_file.write_all(test_data.as_bytes()).unwrap();
287
288 let file = NoteOnceCell::from_file_default(test_file.path()).unwrap();
289 assert_eq!(file.content().unwrap(), test_data);
290 }
291
292 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
293 #[test]
294 fn get_properties() {
295 let test_data = "---\ntime: now\n---\nDATA";
296 let mut test_file = NamedTempFile::new().unwrap();
297 test_file.write_all(test_data.as_bytes()).unwrap();
298
299 let file = NoteOnceCell::from_file_default(test_file.path()).unwrap();
300 let properties = file.properties().unwrap().unwrap();
301
302 assert_eq!(file.content().unwrap(), "DATA");
303 assert_eq!(properties["time"], "now");
304 }
305}