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 raw_text = std::fs::read_to_string(&self.path)?;
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 raw_text = std::fs::read_to_string(&self.path)?;
155
156 let result = match parse_note(&raw_text)? {
157 ResultParse::WithProperties {
158 content,
159 properties: _,
160 } => {
161 #[cfg(feature = "tracing")]
162 tracing::trace!("Frontmatter detected, parsing properties");
163
164 content.to_string()
165 }
166 ResultParse::WithoutProperties => {
167 #[cfg(feature = "tracing")]
168 tracing::trace!("No frontmatter found, storing raw content");
169
170 raw_text
171 }
172 };
173
174 let _ = self.content.set(result.clone()); Ok(Cow::Owned(result))
176 }
177
178 #[inline]
180 fn path(&self) -> Option<Cow<'_, Path>> {
181 Some(Cow::Borrowed(&self.path))
182 }
183}
184
185impl<T> NoteOnceCell<T>
186where
187 T: DeserializeOwned + Clone,
188{
189 #[inline]
191 pub fn set_path(&mut self, path: PathBuf) {
192 self.path = path;
193 }
194}
195
196#[cfg(not(target_family = "wasm"))]
197impl<T> crate::prelude::NoteFromFile for NoteOnceCell<T>
198where
199 T: DeserializeOwned + Clone,
200{
201 fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
203 let path = path.as_ref().to_path_buf();
204
205 if !path.is_file() {
206 return Err(Error::IsNotFile(path));
207 }
208
209 Ok(Self {
210 path,
211 content: OnceCell::default(),
212 properties: OnceCell::default(),
213 })
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::note::NoteDefault;
221 use crate::note::impl_tests::impl_test_for_note;
222 use crate::note::note_aliases::tests::{from_file_have_aliases, from_file_have_not_aliases};
223 use crate::note::note_is_todo::tests::{from_file_is_not_todo, from_file_is_todo};
224 use crate::note::note_read::tests::{from_file, from_file_with_unicode};
225 use crate::note::note_tags::tests::from_file_tags;
226 use crate::note::note_write::tests::impl_all_tests_flush;
227 use std::io::Write;
228 use tempfile::NamedTempFile;
229
230 impl_all_tests_flush!(NoteOnceCell);
231 impl_test_for_note!(impl_from_file, from_file, NoteOnceCell);
232 impl_test_for_note!(impl_from_file_tags, from_file_tags, NoteOnceCell);
233
234 impl_test_for_note!(
235 impl_from_file_with_unicode,
236 from_file_with_unicode,
237 NoteOnceCell
238 );
239
240 impl_test_for_note!(impl_from_file_is_todo, from_file_is_todo, NoteOnceCell);
241 impl_test_for_note!(
242 impl_from_file_is_not_todo,
243 from_file_is_not_todo,
244 NoteOnceCell
245 );
246
247 impl_test_for_note!(
248 impl_from_file_have_aliases,
249 from_file_have_aliases,
250 NoteOnceCell
251 );
252 impl_test_for_note!(
253 impl_from_file_have_not_aliases,
254 from_file_have_not_aliases,
255 NoteOnceCell
256 );
257
258 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
259 #[test]
260 #[should_panic]
261 fn use_from_file_with_path_not_file() {
262 let temp_dir = tempfile::tempdir().unwrap();
263
264 NoteOnceCell::from_file_default(temp_dir.path()).unwrap();
265 }
266
267 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
268 #[test]
269 fn get_path() {
270 let test_file = NamedTempFile::new().unwrap();
271 let file = NoteOnceCell::from_file_default(test_file.path()).unwrap();
272
273 assert_eq!(file.path().unwrap(), test_file.path());
274 assert_eq!(file.path, test_file.path());
275 }
276
277 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
278 #[test]
279 fn get_content() {
280 let test_data = "DATA";
281 let mut test_file = NamedTempFile::new().unwrap();
282 test_file.write_all(test_data.as_bytes()).unwrap();
283
284 let file = NoteOnceCell::from_file_default(test_file.path()).unwrap();
285 assert_eq!(file.content().unwrap(), test_data);
286 }
287
288 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
289 #[test]
290 fn get_properties() {
291 let test_data = "---\ntime: now\n---\nDATA";
292 let mut test_file = NamedTempFile::new().unwrap();
293 test_file.write_all(test_data.as_bytes()).unwrap();
294
295 let file = NoteOnceCell::from_file_default(test_file.path()).unwrap();
296 let properties = file.properties().unwrap().unwrap();
297
298 assert_eq!(file.content().unwrap(), "DATA");
299 assert_eq!(properties["time"], "now");
300 }
301}