1pub mod obfile_in_memory;
2pub mod obfile_on_disk;
3
4use crate::error::Error;
5use serde::de::DeserializeOwned;
6use std::{
7 collections::HashMap,
8 path::{Path, PathBuf},
9};
10
11pub trait ObFile<T = HashMap<String, serde_yml::Value>>: Sized
34where
35 T: DeserializeOwned + Default + Clone + Send,
36{
37 fn content(&self) -> String;
43
44 fn path(&self) -> Option<PathBuf>;
48
49 fn properties(&self) -> T;
55
56 fn from_string<P: AsRef<Path>>(raw_text: &str, path: Option<P>) -> Result<Self, Error>;
66
67 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
76 let path_buf = path.as_ref().to_path_buf();
77
78 #[cfg(feature = "logging")]
79 log::trace!("Parse obsidian file from file: {}", path_buf.display());
80 let data = std::fs::read(path)?;
81 let text = String::from_utf8(data)?;
82
83 Self::from_string(&text, Some(path_buf))
84 }
85}
86
87pub trait ObFileDefault: ObFile<HashMap<String, serde_yml::Value>> {
92 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error>;
98
99 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error>;
105}
106
107impl<T> ObFileDefault for T
108where
109 T: ObFile<HashMap<String, serde_yml::Value>>,
110{
111 fn from_string_default<P: AsRef<Path>>(text: &str, path: Option<P>) -> Result<Self, Error> {
112 Self::from_string(text, path)
113 }
114
115 fn from_file_default<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
116 Self::from_file(path)
117 }
118}
119
120#[derive(Debug, PartialEq)]
121enum ResultParse<'a> {
122 WithProperties {
123 content: &'a str,
124 properties: &'a str,
125 },
126 WithoutProperties,
127}
128
129fn parse_obfile(raw_text: &str) -> Result<ResultParse, Error> {
130 let mut lines = raw_text.lines();
131 if lines.next().unwrap_or_default().trim_end() == "---" {
132 let closed = raw_text["---".len()..]
133 .find("---")
134 .ok_or(Error::InvalidFormat)?;
135
136 return Ok(ResultParse::WithProperties {
137 content: raw_text[(closed + 2 * "...".len())..].trim(),
138 properties: raw_text["...".len()..(closed + "...".len())].trim(),
139 });
140 }
141
142 Ok(ResultParse::WithoutProperties)
143}
144
145#[cfg(test)]
146mod tests {
147 use super::{ResultParse, parse_obfile};
148 use crate::test_utils::init_test_logger;
149
150 #[test]
151 fn parse_obfile_without_properties() {
152 init_test_logger();
153 let test_data = "test_data";
154 let result = parse_obfile(test_data).unwrap();
155
156 assert_eq!(result, ResultParse::WithoutProperties);
157 }
158
159 #[test]
160 fn parse_obfile_with_properties() {
161 init_test_logger();
162 let test_data = "---\nproperties data\n---\ntest data";
163 let result = parse_obfile(test_data).unwrap();
164
165 assert_eq!(
166 result,
167 ResultParse::WithProperties {
168 content: "test data",
169 properties: "properties data"
170 }
171 );
172 }
173
174 #[test]
175 fn parse_obfile_without_properties_but_with_closed() {
176 init_test_logger();
177 let test_data1 = "test_data---";
178 let test_data2 = "test_data\n---\n";
179
180 let result1 = parse_obfile(test_data1).unwrap();
181 let result2 = parse_obfile(test_data2).unwrap();
182
183 assert_eq!(result1, ResultParse::WithoutProperties);
184 assert_eq!(result2, ResultParse::WithoutProperties);
185 }
186
187 #[test]
188 #[should_panic]
189 fn parse_obfile_with_properties_but_without_closed() {
190 init_test_logger();
191 let test_data = "---\nproperties data\ntest data";
192 let _ = parse_obfile(test_data).unwrap();
193 }
194
195 #[test]
196 fn parse_obfile_without_properties_but_with_spaces() {
197 init_test_logger();
198 let test_data = " ---\ndata";
199
200 let result = parse_obfile(test_data).unwrap();
201 assert_eq!(result, ResultParse::WithoutProperties);
202 }
203
204 #[test]
205 fn parse_obfile_with_properties_but_check_trim_end() {
206 init_test_logger();
207 let test_data = "---\r\nproperties data\r\n---\r \ntest data";
208 let result = parse_obfile(test_data).unwrap();
209
210 assert_eq!(
211 result,
212 ResultParse::WithProperties {
213 content: "test data",
214 properties: "properties data"
215 }
216 );
217 }
218}
219
220#[cfg(test)]
221pub(crate) mod impl_tests {
222 use super::*;
223 use crate::test_utils::init_test_logger;
224 use serde::Deserialize;
225 use std::io::Write;
226 use tempfile::NamedTempFile;
227
228 pub(crate) static TEST_DATA: &str = "---\n\
229topic: life\n\
230created: 2025-03-16\n\
231---\n\
232Test data\n\
233---\n\
234Two test data";
235
236 #[derive(Debug, Deserialize, Default, PartialEq, Clone)]
237 pub(crate) struct TestProperties {
238 pub(crate) topic: String,
239 pub(crate) created: String,
240 }
241
242 pub(crate) fn from_string<T: ObFile>() -> Result<(), Error> {
243 init_test_logger();
244 let file = T::from_string(TEST_DATA, None::<&str>)?;
245 let properties = file.properties();
246
247 assert_eq!(properties["topic"], "life");
248 assert_eq!(properties["created"], "2025-03-16");
249 assert_eq!(file.content(), "Test data\n---\nTwo test data");
250 Ok(())
251 }
252
253 pub(crate) fn from_string_without_properties<T: ObFile>() -> Result<(), Error> {
254 init_test_logger();
255 let test_data = "TEST_DATA";
256 let file = T::from_string(test_data, None::<&str>)?;
257 let properties = file.properties();
258
259 assert_eq!(properties.len(), 0);
260 assert_eq!(file.content(), test_data);
261 Ok(())
262 }
263
264 pub(crate) fn from_string_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
265 init_test_logger();
266 let broken_data = "---\n\
267 asdfv:--fs\n\
268 sfsf\n\
269 ---\n\
270 TestData";
271
272 assert!(matches!(
273 T::from_string(broken_data, None::<&str>),
274 Err(Error::Yaml(_))
275 ));
276 Ok(())
277 }
278
279 pub(crate) fn from_string_invalid_format<T: ObFile>() -> Result<(), Error> {
280 init_test_logger();
281 let broken_data = "---\n";
282
283 assert!(matches!(
284 T::from_string(broken_data, None::<&str>),
285 Err(Error::InvalidFormat)
286 ));
287 Ok(())
288 }
289
290 pub(crate) fn from_string_with_unicode<T: ObFile>() -> Result<(), Error> {
291 init_test_logger();
292 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
293 let file = T::from_string(data, None::<&str>)?;
294 let properties = file.properties();
295
296 assert_eq!(properties["data"], "💩");
297 assert_eq!(file.content(), "Super data 💩💩💩");
298 Ok(())
299 }
300
301 pub(crate) fn from_string_space_with_properties<T: ObFile>() -> Result<(), Error> {
302 init_test_logger();
303 let data = " ---\ntest: test-data\n---\n";
304 let file = T::from_string(data, None::<&str>)?;
305 let properties = file.properties();
306
307 assert_eq!(file.content(), data);
308 assert_eq!(properties.len(), 0);
309 Ok(())
310 }
311
312 pub(crate) fn from_file<T: ObFile>() -> Result<(), Error> {
313 init_test_logger();
314 let mut temp_file = NamedTempFile::new().unwrap();
315 temp_file.write_all(b"TEST_DATA").unwrap();
316
317 let file = T::from_file(temp_file.path()).unwrap();
318 assert_eq!(file.content(), "TEST_DATA");
319 assert_eq!(file.path().unwrap(), temp_file.path());
320 assert_eq!(file.properties().len(), 0);
321 Ok(())
322 }
323
324 pub(crate) fn from_file_without_properties<T: ObFile>() -> Result<(), Error> {
325 init_test_logger();
326 let test_data = "TEST_DATA";
327 let mut test_file = NamedTempFile::new().unwrap();
328 test_file.write_all(test_data.as_bytes()).unwrap();
329
330 let file = T::from_file(test_file.path())?;
331 let properties = file.properties();
332
333 assert_eq!(properties.len(), 0);
334 assert_eq!(file.content(), test_data);
335 Ok(())
336 }
337
338 pub(crate) fn from_file_with_invalid_yaml<T: ObFile>() -> Result<(), Error> {
339 init_test_logger();
340 let broken_data = "---\n\
341 asdfv:--fs\n\
342 sfsf\n\
343 ---\n\
344 TestData";
345
346 let mut test_file = NamedTempFile::new().unwrap();
347 test_file.write_all(broken_data.as_bytes()).unwrap();
348
349 assert!(matches!(
350 T::from_file(test_file.path()),
351 Err(Error::Yaml(_))
352 ));
353 Ok(())
354 }
355
356 pub(crate) fn from_file_invalid_format<T: ObFile>() -> Result<(), Error> {
357 init_test_logger();
358 let broken_data = "---\n";
359 let mut test_file = NamedTempFile::new().unwrap();
360 test_file.write_all(broken_data.as_bytes()).unwrap();
361
362 assert!(matches!(
363 T::from_file(test_file.path()),
364 Err(Error::InvalidFormat)
365 ));
366 Ok(())
367 }
368
369 pub(crate) fn from_file_with_unicode<T: ObFile>() -> Result<(), Error> {
370 init_test_logger();
371 let data = "---\ndata: 💩\n---\nSuper data 💩💩💩";
372 let mut test_file = NamedTempFile::new().unwrap();
373 test_file.write_all(data.as_bytes()).unwrap();
374
375 let file = T::from_file(test_file.path())?;
376 let properties = file.properties();
377
378 assert_eq!(properties["data"], "💩");
379 assert_eq!(file.content(), "Super data 💩💩💩");
380 Ok(())
381 }
382
383 pub(crate) fn from_file_space_with_properties<T: ObFile>() -> Result<(), Error> {
384 init_test_logger();
385 let data = " ---\ntest: test-data\n---\n";
386 let mut test_file = NamedTempFile::new().unwrap();
387 test_file.write_all(data.as_bytes()).unwrap();
388
389 let file = T::from_string(data, None::<&str>)?;
390 let properties = file.properties();
391
392 assert_eq!(file.content(), data);
393 assert_eq!(properties.len(), 0);
394 Ok(())
395 }
396
397 macro_rules! impl_test_for_obfile {
398 ($name_test:ident, $fn_test:ident, $impl_obfile:path) => {
399 #[test]
400 fn $name_test() {
401 $fn_test::<$impl_obfile>().unwrap();
402 }
403 };
404 }
405
406 pub(crate) use impl_test_for_obfile;
407
408 macro_rules! impl_all_tests_from_string {
409 ($impl_obfile:path) => {
410 #[allow(unused_imports)]
411 use crate::obfile::impl_tests::*;
412
413 impl_test_for_obfile!(impl_from_string, from_string, $impl_obfile);
414
415 impl_test_for_obfile!(
416 impl_from_string_without_properties,
417 from_string_without_properties,
418 $impl_obfile
419 );
420 impl_test_for_obfile!(
421 impl_from_string_with_invalid_yaml,
422 from_string_with_invalid_yaml,
423 $impl_obfile
424 );
425 impl_test_for_obfile!(
426 impl_from_string_invalid_format,
427 from_string_invalid_format,
428 $impl_obfile
429 );
430 impl_test_for_obfile!(
431 impl_from_string_with_unicode,
432 from_string_with_unicode,
433 $impl_obfile
434 );
435 impl_test_for_obfile!(
436 impl_from_string_space_with_properties,
437 from_string_space_with_properties,
438 $impl_obfile
439 );
440 };
441 }
442
443 macro_rules! impl_all_tests_from_file {
444 ($impl_obfile:path) => {
445 #[allow(unused_imports)]
446 use crate::obfile::impl_tests::*;
447
448 impl_test_for_obfile!(impl_from_file, from_file, $impl_obfile);
449
450 impl_test_for_obfile!(
451 impl_from_file_without_properties,
452 from_file_without_properties,
453 $impl_obfile
454 );
455 impl_test_for_obfile!(
456 impl_from_file_with_invalid_yaml,
457 from_file_with_invalid_yaml,
458 $impl_obfile
459 );
460 impl_test_for_obfile!(
461 impl_from_file_invalid_format,
462 from_file_invalid_format,
463 $impl_obfile
464 );
465 impl_test_for_obfile!(
466 impl_from_file_with_unicode,
467 from_file_with_unicode,
468 $impl_obfile
469 );
470 impl_test_for_obfile!(
471 impl_from_file_space_with_properties,
472 from_file_space_with_properties,
473 $impl_obfile
474 );
475 };
476 }
477
478 pub(crate) use impl_all_tests_from_file;
479 pub(crate) use impl_all_tests_from_string;
480}