use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum FileEntry {
Missing,
Text(String),
Unsupported(String),
}
const OUTPUT_INDEX: usize = 2;
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct EntriesToCompare(pub std::collections::BTreeMap<PathBuf, [FileEntry; 3]>);
#[derive(Error, Debug)]
pub enum DataSaveError {
#[error("IO Error while saving {0}: {1}")]
IOError(PathBuf, std::io::Error),
#[error("Cannot save the demo fake data")]
CannotSaveFakeData,
#[error("Failed to retrieve valid paths for saving: {0}")]
ValidationIOError(#[from] DataReadError),
#[error(
"Security error: got request to save to a file that wasn't one of the files being merged: \
'{0}'\nPerhaps this client is now connected to a different server than the one it was \
started from?"
)]
ValidationFailError(String),
}
#[derive(Error, Debug)]
pub enum DataReadError {
#[error("IO Error while reading: {0}")]
IOError(#[from] std::io::Error),
}
impl serde::Serialize for DataSaveError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
impl serde::Serialize for DataReadError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub trait DataInterface: Send + Sync + 'static {
fn scan(&self) -> Result<EntriesToCompare, DataReadError>;
fn save_unchecked(
&mut self,
result: indexmap::IndexMap<String, String>,
) -> Result<(), DataSaveError>;
fn get_valid_entries(&mut self) -> Result<std::collections::HashSet<PathBuf>, DataReadError> {
let entries = self.scan()?;
Ok(entries.0.keys().cloned().collect())
}
fn save(&mut self, result: indexmap::IndexMap<String, String>) -> Result<(), DataSaveError> {
let valid_entries = self.get_valid_entries()?;
if let Some(unsafe_path) = result
.keys()
.find(|x| !valid_entries.contains::<PathBuf>(&x.into()))
{
return Err(DataSaveError::ValidationFailError(unsafe_path.to_string()));
}
self.save_unchecked(result)
}
}
impl DataInterface for EntriesToCompare {
fn scan(&self) -> Result<EntriesToCompare, DataReadError> {
Ok(self.clone())
}
fn save_unchecked(
&mut self,
result: indexmap::IndexMap<String, String>,
) -> Result<(), DataSaveError> {
for (path, new_value) in result.into_iter() {
self.0
.get_mut(&PathBuf::from(path))
.expect("At this point, `save()` should have verified that the path is valid")
[OUTPUT_INDEX] = FileEntry::Text(new_value);
}
Ok(())
}
}
pub struct FakeData;
impl DataInterface for FakeData {
fn scan(&self) -> Result<EntriesToCompare, DataReadError> {
#[rustfmt::skip]
let two_sides_map = vec![
(
"edited_file",
[
FileEntry::Text(
"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"
.to_string(),
),
FileEntry::Text(
"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"
.to_string(),
),
],
),
(
"deleted_file",
[FileEntry::Text("deleted".to_string()), FileEntry::Missing],
),
(
"added file",
[FileEntry::Missing, FileEntry::Text("added".to_string())],
),
(
"unsupported-left",
[
FileEntry::Unsupported("demo of an unsupported file".to_string()),
FileEntry::Text("text".to_string()),
],
),
(
"unsupported-right",
[
FileEntry::Text("text".to_string()),
FileEntry::Unsupported("demo of an unsupported file".to_string()),
],
),
];
Ok(EntriesToCompare(
two_sides_map
.into_iter()
.map(|(key, [left, right])| (PathBuf::from(key), [left, right.clone(), right]))
.collect(),
))
}
fn save_unchecked(
&mut self,
result: indexmap::IndexMap<String, String>,
) -> Result<(), DataSaveError> {
eprintln!("Can't save fake demo data. Here it is as TOML");
eprintln!();
eprintln!(
"{}",
toml::to_string(&result).unwrap_or_else(|err| format!("Failed to parse TOML: {err}"))
);
Err(DataSaveError::CannotSaveFakeData)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fake_data() {
insta::assert_yaml_snapshot!(FakeData.scan().unwrap(),
@r###"
---
added file:
- type: Missing
- type: Text
value: added
- type: Text
value: added
deleted_file:
- type: Text
value: deleted
- type: Missing
- type: Missing
edited_file:
- type: Text
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"
- type: Text
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"
- type: Text
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"
unsupported-left:
- type: Unsupported
value: demo of an unsupported file
- type: Text
value: text
- type: Text
value: text
unsupported-right:
- type: Text
value: text
- type: Unsupported
value: demo of an unsupported file
- type: Unsupported
value: demo of an unsupported file
"###);
}
}