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