1use std::path::PathBuf;
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
6#[serde(tag = "type", content = "value")]
7pub enum FileEntry {
8 Missing,
9 Text(String),
11 Unsupported(String),
12}
13
14const OUTPUT_INDEX: usize = 2;
16#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
17pub struct EntriesToCompare(pub std::collections::BTreeMap<PathBuf, [FileEntry; 3]>);
20
21#[derive(Error, Debug)]
22pub enum DataSaveError {
23 #[error("IO Error while saving {0}: {1}")]
25 IOError(PathBuf, std::io::Error),
26 #[error("Cannot save the demo fake data")]
27 CannotSaveFakeData,
28 #[error("Failed to retrieve valid paths for saving: {0}")]
29 ValidationIOError(#[from] DataReadError),
30 #[error(
31 "Security error: got request to save to a file that wasn't one of the files being merged: \
32 '{0}'\nPerhaps this client is now connected to a different server than the one it was \
33 started from?"
34 )]
35 ValidationFailError(String),
36}
37
38#[derive(Error, Debug)]
39pub enum DataReadError {
40 #[error("IO Error while reading: {0}")]
41 IOError(#[from] std::io::Error),
42}
43
44impl serde::Serialize for DataSaveError {
45 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46 where
47 S: serde::Serializer,
48 {
49 serializer.serialize_str(self.to_string().as_ref())
50 }
51}
52
53impl serde::Serialize for DataReadError {
54 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
55 where
56 S: serde::Serializer,
57 {
58 serializer.serialize_str(self.to_string().as_ref())
59 }
60}
61
62pub trait DataInterface: Send + Sync + 'static {
64 fn scan(&self) -> Result<EntriesToCompare, DataReadError>;
69 fn save_unchecked(
73 &mut self,
74 result: indexmap::IndexMap<String, String>,
75 ) -> Result<(), DataSaveError>;
76
77 fn get_valid_entries(&mut self) -> Result<std::collections::HashSet<PathBuf>, DataReadError> {
81 let entries = self.scan()?;
82 Ok(entries.0.keys().cloned().collect())
83 }
84
85 fn save(&mut self, result: indexmap::IndexMap<String, String>) -> Result<(), DataSaveError> {
95 let valid_entries = self.get_valid_entries()?;
96 if let Some(unsafe_path) = result
97 .keys()
98 .find(|x| !valid_entries.contains::<PathBuf>(&x.into()))
99 {
100 return Err(DataSaveError::ValidationFailError(unsafe_path.to_string()));
104 }
105 self.save_unchecked(result)
106 }
107}
108
109impl DataInterface for EntriesToCompare {
114 fn scan(&self) -> Result<EntriesToCompare, DataReadError> {
115 Ok(self.clone())
116 }
117
118 fn save_unchecked(
119 &mut self,
120 result: indexmap::IndexMap<String, String>,
121 ) -> Result<(), DataSaveError> {
122 for (path, new_value) in result.into_iter() {
123 self.0
124 .get_mut(&PathBuf::from(path))
125 .expect("At this point, `save()` should have verified that the path is valid")
126 [OUTPUT_INDEX] = FileEntry::Text(new_value);
127 }
128 Ok(())
129 }
130}
131
132pub struct FakeData;
133
134impl DataInterface for FakeData {
135 fn scan(&self) -> Result<EntriesToCompare, DataReadError> {
136 #[rustfmt::skip]
145 let two_sides_map = vec![
146 (
147 "edited_file",
148 [
149 FileEntry::Text(
150 "Long line, a long line, a quite long line. Long line, a long line, a \
151 quite long line. Long line, a long line, a quite long \
152 line.\nFirst\nThird\nFourth\nFourthAndAHalf\nSame\nSame\nSame\nSame\
153 \nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\
154 \nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none two"
155 .to_string(),
156 ),
157 FileEntry::Text(
158 "Long line, a long line, a quite long line. Long line, a long line, a \
159 quite long line. Something new. Long line, a long line, a quite long \
160 line.\nFirst\nSecond\nThird\nSame\nSame\nSame\nSame\nSame\nSame\nSame\
161 \nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\
162 \nSame\nSame\nFifth\nSixth\n----\none\n"
163 .to_string(),
164 ),
165 ],
166 ),
167 (
168 "deleted_file",
169 [FileEntry::Text("deleted".to_string()), FileEntry::Missing],
170 ),
171 (
172 "added file",
173 [FileEntry::Missing, FileEntry::Text("added".to_string())],
174 ),
175 (
176 "unsupported-left",
177 [
178 FileEntry::Unsupported("demo of an unsupported file".to_string()),
179 FileEntry::Text("text".to_string()),
180 ],
181 ),
182 (
183 "unsupported-right",
184 [
185 FileEntry::Text("text".to_string()),
186 FileEntry::Unsupported("demo of an unsupported file".to_string()),
187 ],
188 ),
189 ];
190 Ok(EntriesToCompare(
191 two_sides_map
192 .into_iter()
193 .map(|(key, [left, right])| (PathBuf::from(key), [left, right.clone(), right]))
194 .collect(),
195 ))
196 }
197
198 fn save_unchecked(
199 &mut self,
200 result: indexmap::IndexMap<String, String>,
201 ) -> Result<(), DataSaveError> {
202 eprintln!("Can't save fake demo data. Here it is as TOML");
203 eprintln!();
204 eprintln!(
205 "{}",
206 toml::to_string(&result).unwrap_or_else(|err| format!("Failed to parse TOML: {err}"))
207 );
208 Err(DataSaveError::CannotSaveFakeData)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn fake_data() {
218 insta::assert_yaml_snapshot!(FakeData.scan().unwrap(),
219 @r###"
220 ---
221 added file:
222 - type: Missing
223 - type: Text
224 value: added
225 - type: Text
226 value: added
227 deleted_file:
228 - type: Text
229 value: deleted
230 - type: Missing
231 - type: Missing
232 edited_file:
233 - type: Text
234 value: "Long line, a long line, a quite long line. Long line, a long line, a quite long line. Long line, a long line, a quite long line.\nFirst\nThird\nFourth\nFourthAndAHalf\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none two"
235 - type: Text
236 value: "Long line, a long line, a quite long line. Long line, a long line, a quite long line. Something new. Long line, a long line, a quite long line.\nFirst\nSecond\nThird\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none\n"
237 - type: Text
238 value: "Long line, a long line, a quite long line. Long line, a long line, a quite long line. Something new. Long line, a long line, a quite long line.\nFirst\nSecond\nThird\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none\n"
239 unsupported-left:
240 - type: Unsupported
241 value: demo of an unsupported file
242 - type: Text
243 value: text
244 - type: Text
245 value: text
246 unsupported-right:
247 - type: Text
248 value: text
249 - type: Unsupported
250 value: demo of an unsupported file
251 - type: Unsupported
252 value: demo of an unsupported file
253 "###);
254 }
255}