use std::path::PathBuf;
use cached::proc_macro::cached;
use derive_setters::Setters;
use jiff::{Span, Zoned};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use crate::{
ErrorKind, RusticError, RusticResult, StringList,
repofile::{DeleteOption, RusticTime, SnapshotFile},
};
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[serde_as]
#[derive(Debug, Clone, Default, Setters, Serialize, Deserialize)]
#[setters(into)]
#[non_exhaustive]
pub struct SnapshotModification {
#[cfg_attr(feature = "clap", clap(long, value_name = "LABEL"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub set_label: Option<String>,
#[cfg_attr(feature = "clap", clap(long, value_parser = crate::repofile::RusticTime::parse_system))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
#[serde_as(as = "Option<RusticTime>")]
pub set_time: Option<Zoned>,
#[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub set_hostname: Option<String>,
#[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
pub add_tags: Vec<StringList>,
#[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
pub set_tags: Vec<StringList>,
#[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
pub remove_tags: Vec<StringList>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DESCRIPTION"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub set_description: Option<String>,
#[cfg_attr(
feature = "clap",
clap(long, value_name = "FILE", conflicts_with = "set_description", value_hint = clap::ValueHint::FilePath)
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub set_description_from: Option<PathBuf>,
#[cfg_attr(feature = "clap", clap(long, conflicts_with_all = &["set_description", "set_description_from"]))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub remove_description: bool,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
#[serde_as(as = "Option<DisplayFromStr>")]
pub set_delete_after: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, conflicts_with = "set_delete_after"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub set_delete_never: bool,
#[cfg_attr(feature = "clap", clap(long, conflicts_with_all = &["set_delete_never", "set_delete_after"]))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub remove_delete: bool,
}
#[cached(size = 1)]
fn get_description_from_file(path: PathBuf) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|err| format!("{err:?}"))
}
impl SnapshotModification {
pub fn apply_to(&self, sn: &mut SnapshotFile) -> RusticResult<bool> {
let delete = match (
self.remove_delete,
self.set_delete_never,
self.set_delete_after,
) {
(true, _, _) => Some(DeleteOption::NotSet),
(_, true, _) => Some(DeleteOption::Never),
(_, _, Some(d)) => Some(DeleteOption::After(Zoned::now() + d)),
(false, false, None) => None,
};
let description = match (self.remove_description, &self.set_description_from) {
(true, _) => Some(None),
(false, Some(path)) => Some(Some(get_description_from_file(path.clone()).map_err(
|err| {
RusticError::with_source(
ErrorKind::Other,
"Failed to read description from file {path}.",
err,
)
.attach_context("path", path.to_string_lossy())
},
)?)),
(false, None) => self
.set_description
.as_ref()
.map(|description| Some(description.clone())),
};
let mut changed = false;
if !self.set_tags.is_empty() {
changed |= sn.set_tags(self.set_tags.clone());
}
changed |= sn.add_tags(self.add_tags.clone());
changed |= sn.remove_tags(&self.remove_tags);
changed |= set_check(&mut sn.delete, &delete);
changed |= set_check(&mut sn.label, &self.set_label);
changed |= set_check(&mut sn.description, &description);
changed |= set_check(&mut sn.time, &self.set_time);
changed |= set_check(&mut sn.hostname, &self.set_hostname);
Ok(changed)
}
}
#[allow(clippy::ref_option)]
fn set_check<T: PartialEq + Clone>(a: &mut T, b: &Option<T>) -> bool {
if let Some(b) = b
&& *a != *b
{
*a = b.clone();
return true;
}
false
}