obsidian_parser/note/
note_write.rs

1//! Impl trait [`NoteWrite`]
2
3use super::{Note, OpenOptions};
4use crate::note::parser;
5use serde::Serialize;
6use std::io::Write;
7
8/// [`Note`] support write operation
9pub trait NoteWrite: Note
10where
11    Self::Properties: Serialize,
12    Self::Error: From<std::io::Error> + From<serde_yml::Error> + From<parser::Error>,
13{
14    /// Flush only `content`
15    ///
16    /// Ignore if path is `None`
17    fn flush_content(&self, open_option: &OpenOptions) -> Result<(), Self::Error> {
18        if let Some(path) = self.path() {
19            let text = std::fs::read_to_string(&path)?;
20            let parsed = parser::parse_note(&text)?;
21
22            let mut file = open_option.open(path)?;
23
24            match parsed {
25                parser::ResultParse::WithProperties {
26                    content: _,
27                    properties,
28                } => file.write_all(
29                    format!("---\n{}\n---\n{}", properties, self.content()?).as_bytes(),
30                )?,
31                parser::ResultParse::WithoutProperties => {
32                    file.write_all(self.content()?.as_bytes())?;
33                }
34            }
35        }
36
37        Ok(())
38    }
39
40    /// Flush only `content`
41    ///
42    /// Ignore if path is `None`
43    fn flush_properties(&self, open_option: &OpenOptions) -> Result<(), Self::Error> {
44        if let Some(path) = self.path() {
45            let text = std::fs::read_to_string(&path)?;
46            let parsed = parser::parse_note(&text)?;
47
48            let mut file = open_option.open(path)?;
49
50            match parsed {
51                parser::ResultParse::WithProperties {
52                    content,
53                    properties: _,
54                } => match self.properties()? {
55                    Some(properties) => file.write_all(
56                        format!(
57                            "---\n{}\n---\n{}",
58                            serde_yml::to_string(&properties)?,
59                            content
60                        )
61                        .as_bytes(),
62                    )?,
63                    None => file.write_all(self.content()?.as_bytes())?,
64                },
65                parser::ResultParse::WithoutProperties => {
66                    file.write_all(self.content()?.as_bytes())?;
67                }
68            }
69        }
70
71        Ok(())
72    }
73
74    /// Flush [`Note`] to [`Note::path`]
75    ///
76    /// Ignore if path is `None`
77    fn flush(&self, open_option: &OpenOptions) -> Result<(), Self::Error> {
78        if let Some(path) = self.path() {
79            let mut file = open_option.open(path)?;
80
81            match self.properties()? {
82                Some(properties) => file.write_all(
83                    format!(
84                        "---\n{}\n---\n{}",
85                        serde_yml::to_string(&properties)?,
86                        self.content()?
87                    )
88                    .as_bytes(),
89                )?,
90                None => file.write_all(self.content()?.as_bytes())?,
91            }
92        }
93
94        Ok(())
95    }
96}
97
98impl<T: Note> NoteWrite for T
99where
100    T::Properties: Serialize,
101    Self::Error: From<std::io::Error> + From<serde_yml::Error> + From<super::parser::Error>,
102{
103}
104
105#[cfg(test)]
106pub(crate) mod tests {
107    use super::*;
108    use crate::note::{DefaultProperties, NoteFromFile};
109    use tempfile::NamedTempFile;
110
111    const TEST_DATA: &str = "---\n\
112topic: life\n\
113created: 2025-03-16\n\
114---\n\
115Test data\n\
116---\n\
117Two test data";
118
119    pub(crate) fn flush_properties<T>() -> Result<(), T::Error>
120    where
121        T: NoteFromFile<Properties = DefaultProperties> + NoteWrite,
122        T::Error: From<std::io::Error> + From<serde_yml::Error> + From<parser::Error>,
123    {
124        let mut test_file = NamedTempFile::new().unwrap();
125        test_file.write_all(TEST_DATA.as_bytes()).unwrap();
126
127        let file = T::from_file(test_file.path())?;
128        let open_options = OpenOptions::new().write(true).create(false).clone();
129        file.flush_properties(&open_options)?;
130        drop(file);
131
132        let file = T::from_file(test_file.path())?;
133
134        let properties = file.properties()?.unwrap();
135        assert_eq!(properties["topic"], "life");
136        assert_eq!(properties["created"], "2025-03-16");
137        assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
138
139        Ok(())
140    }
141
142    pub(crate) fn flush_content<T>() -> Result<(), T::Error>
143    where
144        T: NoteFromFile<Properties = DefaultProperties> + NoteWrite,
145        T::Error: From<std::io::Error> + From<serde_yml::Error> + From<parser::Error>,
146    {
147        let mut test_file = NamedTempFile::new().unwrap();
148        test_file.write_all(TEST_DATA.as_bytes()).unwrap();
149
150        let file = T::from_file(test_file.path())?;
151        let open_options = OpenOptions::new().write(true).create(false).clone();
152        file.flush_content(&open_options)?;
153        drop(file);
154
155        let file = T::from_file(test_file.path())?;
156        let properties = file.properties()?.unwrap();
157        assert_eq!(properties["topic"], "life");
158        assert_eq!(properties["created"], "2025-03-16");
159        assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
160
161        Ok(())
162    }
163
164    pub(crate) fn flush<T>() -> Result<(), T::Error>
165    where
166        T: NoteFromFile<Properties = DefaultProperties> + NoteWrite,
167        T::Error: From<std::io::Error> + From<serde_yml::Error> + From<parser::Error>,
168    {
169        let mut test_file = NamedTempFile::new().unwrap();
170        test_file.write_all(TEST_DATA.as_bytes()).unwrap();
171
172        let file = T::from_file(test_file.path())?;
173        let open_options = OpenOptions::new().write(true).create(false).clone();
174        file.flush(&open_options)?;
175        drop(file);
176
177        let file = T::from_file(test_file.path())?;
178        let properties = file.properties()?.unwrap();
179        assert_eq!(properties["topic"], "life");
180        assert_eq!(properties["created"], "2025-03-16");
181        assert_eq!(file.content().unwrap(), "Test data\n---\nTwo test data");
182
183        Ok(())
184    }
185
186    macro_rules! impl_all_tests_flush {
187        ($impl_note:path) => {
188            #[allow(unused_imports)]
189            use $crate::note::note_write::tests::*;
190
191            impl_test_for_note!(impl_flush, flush, $impl_note);
192            impl_test_for_note!(impl_flush_content, flush_content, $impl_note);
193            impl_test_for_note!(impl_flush_properties, flush_properties, $impl_note);
194        };
195    }
196
197    pub(crate) use impl_all_tests_flush;
198}