use std::fmt;
use std::path::PathBuf;
use crate::shell_kind::ShellKind;
#[derive(Debug, Clone, PartialEq)]
#[must_use = "this report contains the actions taken"]
pub struct Report {
pub actions: Vec<Action>,
pub dry_run: bool,
}
impl Report {
pub(crate) fn new(dry_run: bool) -> Self {
Self {
actions: Vec::new(),
dry_run,
}
}
pub(crate) fn push(&mut self, action: Action) {
self.actions.push(action);
}
#[must_use]
pub fn has(&self, kind: ActionKind) -> bool {
self.actions.iter().any(|a| a.kind() == kind)
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.dry_run {
writeln!(f, "[dry run] The following actions would be taken:")?;
}
for action in &self.actions {
writeln!(f, " {action}")?;
}
if self.actions.is_empty() {
writeln!(f, " (no changes needed)")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
EnvScriptWritten {
shell: ShellKind,
path: PathBuf,
},
EnvScriptAlreadyExists {
shell: ShellKind,
path: PathBuf,
},
EnvScriptRemoved {
shell: ShellKind,
path: PathBuf,
},
SourceLineAdded {
shell: ShellKind,
rc_file: PathBuf,
},
SourceLineAlreadyPresent {
shell: ShellKind,
rc_file: PathBuf,
},
SourceLineRemoved {
shell: ShellKind,
rc_file: PathBuf,
},
BackupCreated {
original: PathBuf,
backup: PathBuf,
},
ShellSkipped {
shell: ShellKind,
reason: String,
},
RegistryModified {
old_value: String,
new_value: String,
},
RegistryAlreadyContains,
RegistryEntryRemoved {
old_value: String,
new_value: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActionKind {
EnvScriptWritten,
EnvScriptAlreadyExists,
EnvScriptRemoved,
SourceLineAdded,
SourceLineAlreadyPresent,
SourceLineRemoved,
BackupCreated,
ShellSkipped,
RegistryModified,
RegistryAlreadyContains,
RegistryEntryRemoved,
}
impl Action {
#[must_use]
pub fn kind(&self) -> ActionKind {
match self {
Self::EnvScriptWritten { .. } => ActionKind::EnvScriptWritten,
Self::EnvScriptAlreadyExists { .. } => ActionKind::EnvScriptAlreadyExists,
Self::EnvScriptRemoved { .. } => ActionKind::EnvScriptRemoved,
Self::SourceLineAdded { .. } => ActionKind::SourceLineAdded,
Self::SourceLineAlreadyPresent { .. } => ActionKind::SourceLineAlreadyPresent,
Self::SourceLineRemoved { .. } => ActionKind::SourceLineRemoved,
Self::BackupCreated { .. } => ActionKind::BackupCreated,
Self::ShellSkipped { .. } => ActionKind::ShellSkipped,
Self::RegistryModified { .. } => ActionKind::RegistryModified,
Self::RegistryAlreadyContains => ActionKind::RegistryAlreadyContains,
Self::RegistryEntryRemoved { .. } => ActionKind::RegistryEntryRemoved,
}
}
}
impl fmt::Display for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EnvScriptWritten { shell, path } => {
write!(f, "[{shell}] wrote env script: {}", path.display())
}
Self::EnvScriptAlreadyExists { shell, path } => {
write!(f, "[{shell}] env script already exists: {}", path.display())
}
Self::EnvScriptRemoved { shell, path } => {
write!(f, "[{shell}] removed env script: {}", path.display())
}
Self::SourceLineAdded { shell, rc_file } => {
write!(f, "[{shell}] added source line to {}", rc_file.display())
}
Self::SourceLineAlreadyPresent { shell, rc_file } => {
write!(
f,
"[{shell}] source line already present in {}",
rc_file.display()
)
}
Self::SourceLineRemoved { shell, rc_file } => {
write!(
f,
"[{shell}] removed source line from {}",
rc_file.display()
)
}
Self::BackupCreated { original, backup } => {
write!(
f,
"backed up {} -> {}",
original.display(),
backup.display()
)
}
Self::ShellSkipped { shell, reason } => {
write!(f, "[{shell}] skipped: {reason}")
}
Self::RegistryModified { .. } => {
write!(f, "[Windows] modified HKCU\\Environment\\PATH")
}
Self::RegistryAlreadyContains => {
write!(f, "[Windows] PATH already contains the directory")
}
Self::RegistryEntryRemoved { .. } => {
write!(f, "[Windows] removed entry from HKCU\\Environment\\PATH")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_display_multiple_actions() {
let report = Report {
actions: vec![
Action::EnvScriptWritten {
shell: ShellKind::Bash,
path: PathBuf::from("/home/user/.myapp/env"),
},
Action::SourceLineAdded {
shell: ShellKind::Bash,
rc_file: PathBuf::from("/home/user/.bashrc"),
},
Action::BackupCreated {
original: PathBuf::from("/home/user/.bashrc"),
backup: PathBuf::from("/home/user/.bashrc.onpath.bak"),
},
],
dry_run: false,
};
let output = format!("{report}");
assert!(output.contains("[Bash] wrote env script"));
assert!(output.contains("[Bash] added source line"));
assert!(output.contains("backed up"));
}
#[test]
fn report_display_empty_actions() {
let report = Report {
actions: vec![],
dry_run: false,
};
let output = format!("{report}");
assert!(output.contains("no changes needed"));
}
#[test]
fn report_display_dry_run_prefix() {
let report = Report {
actions: vec![Action::RegistryAlreadyContains],
dry_run: true,
};
let output = format!("{report}");
assert!(output.contains("[dry run]"));
}
#[test]
fn action_display_env_script_variants() {
let written = Action::EnvScriptWritten {
shell: ShellKind::Zsh,
path: PathBuf::from("/home/user/.myapp/env"),
};
assert!(format!("{written}").contains("[Zsh] wrote env script"));
let exists = Action::EnvScriptAlreadyExists {
shell: ShellKind::Fish,
path: PathBuf::from("/home/user/.config/fish/conf.d/onpath-myapp.fish"),
};
assert!(format!("{exists}").contains("[Fish] env script already exists"));
let removed = Action::EnvScriptRemoved {
shell: ShellKind::Nushell,
path: PathBuf::from("/home/user/.myapp/env.nu"),
};
assert!(format!("{removed}").contains("[Nushell] removed env script"));
}
#[test]
fn action_display_source_line_and_registry_variants() {
let added = Action::SourceLineAdded {
shell: ShellKind::Posix,
rc_file: PathBuf::from("/home/user/.profile"),
};
assert!(format!("{added}").contains("[sh] added source line"));
let present = Action::SourceLineAlreadyPresent {
shell: ShellKind::Bash,
rc_file: PathBuf::from("/home/user/.bashrc"),
};
assert!(format!("{present}").contains("already present"));
let removed = Action::SourceLineRemoved {
shell: ShellKind::Tcsh,
rc_file: PathBuf::from("/home/user/.cshrc"),
};
assert!(format!("{removed}").contains("[Tcsh] removed source line"));
let skipped = Action::ShellSkipped {
shell: ShellKind::Xonsh,
reason: "no RC files found".to_owned(),
};
assert!(format!("{skipped}").contains("[Xonsh] skipped"));
let reg_mod = Action::RegistryModified {
old_value: "old".to_owned(),
new_value: "new".to_owned(),
};
assert!(format!("{reg_mod}").contains("[Windows]"));
let reg_contains = Action::RegistryAlreadyContains;
assert!(format!("{reg_contains}").contains("already contains"));
let reg_removed = Action::RegistryEntryRemoved {
old_value: "old".to_owned(),
new_value: "new".to_owned(),
};
assert!(format!("{reg_removed}").contains("removed entry"));
}
}