pub mod filter;
pub mod gate;
pub mod homebrew;
pub mod install;
pub mod path;
pub mod shell;
pub mod symlink;
use std::collections::HashMap;
use std::path::Path;
use serde::Serialize;
use crate::datastore::DataStore;
use crate::fs::Fs;
use crate::operations::HandlerIntent;
use crate::paths::Pather;
use crate::rules::RuleMatch;
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub enum HandlerCategory {
Configuration,
CodeExecution,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub enum ExecutionPhase {
Filter,
Provision,
Setup,
PathExport,
ShellInit,
Link,
}
impl ExecutionPhase {
pub fn category(self) -> HandlerCategory {
match self {
Self::Provision | Self::Setup => HandlerCategory::CodeExecution,
Self::Filter | Self::PathExport | Self::ShellInit | Self::Link => {
HandlerCategory::Configuration
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchMode {
Precise,
Catchall,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HandlerScope {
Exclusive,
Shared,
}
#[derive(Debug, Clone, Serialize)]
pub struct HandlerStatus {
pub file: String,
pub handler: String,
pub deployed: bool,
pub message: String,
}
pub trait Handler: Send + Sync {
fn name(&self) -> &str;
fn phase(&self) -> ExecutionPhase;
fn category(&self) -> HandlerCategory {
self.phase().category()
}
fn match_mode(&self) -> MatchMode {
MatchMode::Precise
}
fn scope(&self) -> HandlerScope {
HandlerScope::Exclusive
}
fn to_intents(
&self,
matches: &[RuleMatch],
config: &HandlerConfig,
paths: &dyn Pather,
fs: &dyn Fs,
) -> Result<Vec<HandlerIntent>>;
fn warnings_for_matches(
&self,
_matches: &[RuleMatch],
_config: &HandlerConfig,
_paths: &dyn Pather,
) -> Vec<String> {
Vec::new()
}
fn check_status(
&self,
file: &Path,
pack: &str,
datastore: &dyn DataStore,
) -> Result<HandlerStatus>;
}
#[derive(Debug, Clone, Serialize)]
pub struct HandlerConfig {
pub force_home: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub force_app: Vec<String>,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub app_aliases: std::collections::HashMap<String, String>,
pub protected_paths: Vec<String>,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub targets: std::collections::HashMap<String, String>,
pub auto_chmod_exec: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pack_ignore: Vec<String>,
}
impl Default for HandlerConfig {
fn default() -> Self {
Self {
force_home: Vec::new(),
force_app: Vec::new(),
app_aliases: std::collections::HashMap::new(),
protected_paths: Vec::new(),
targets: std::collections::HashMap::new(),
auto_chmod_exec: true,
pack_ignore: Vec::new(),
}
}
}
pub const HANDLER_SYMLINK: &str = "symlink";
pub const HANDLER_SHELL: &str = "shell";
pub const HANDLER_PATH: &str = "path";
pub const HANDLER_INSTALL: &str = "install";
pub const HANDLER_HOMEBREW: &str = "homebrew";
pub const HANDLER_IGNORE: &str = "ignore";
pub const HANDLER_SKIP: &str = "skip";
pub const HANDLER_GATE: &str = "gate";
pub fn configuration_handler_names(fs: &dyn Fs) -> Vec<String> {
create_registry(fs)
.iter()
.filter(|(_, h)| h.category() == HandlerCategory::Configuration)
.map(|(name, _)| name.clone())
.collect()
}
pub fn create_registry(fs: &dyn Fs) -> HashMap<String, Box<dyn Handler + '_>> {
let mut registry: HashMap<String, Box<dyn Handler>> = HashMap::new();
registry.insert(HANDLER_IGNORE.into(), Box::new(filter::IgnoreHandler));
registry.insert(HANDLER_SKIP.into(), Box::new(filter::SkipHandler));
registry.insert(HANDLER_GATE.into(), Box::new(gate::GateHandler));
registry.insert(HANDLER_SYMLINK.into(), Box::new(symlink::SymlinkHandler));
registry.insert(HANDLER_SHELL.into(), Box::new(shell::ShellHandler));
registry.insert(HANDLER_PATH.into(), Box::new(path::PathHandler));
registry.insert(
HANDLER_INSTALL.into(),
Box::new(install::InstallHandler::new(fs)),
);
registry.insert(
HANDLER_HOMEBREW.into(),
Box::new(homebrew::HomebrewHandler::new(fs)),
);
validate_registry(®istry);
registry
}
fn validate_registry(registry: &HashMap<String, Box<dyn Handler + '_>>) {
let exclusive_catchalls: Vec<&str> = registry
.values()
.filter(|h| h.match_mode() == MatchMode::Catchall && h.scope() == HandlerScope::Exclusive)
.map(|h| h.name())
.collect();
debug_assert!(
exclusive_catchalls.len() <= 1,
"at most one exclusive catchall handler allowed, found: {exclusive_catchalls:?}"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn assert_object_safe(_: &dyn Handler) {}
#[allow(dead_code)]
fn assert_boxable(_: Box<dyn Handler>) {}
#[test]
fn handler_category_eq() {
assert_eq!(
HandlerCategory::Configuration,
HandlerCategory::Configuration
);
assert_ne!(
HandlerCategory::Configuration,
HandlerCategory::CodeExecution
);
}
#[test]
fn execution_phase_declaration_order_drives_ord() {
assert!(ExecutionPhase::Filter < ExecutionPhase::Provision);
assert!(ExecutionPhase::Provision < ExecutionPhase::Setup);
assert!(ExecutionPhase::Setup < ExecutionPhase::PathExport);
assert!(ExecutionPhase::PathExport < ExecutionPhase::ShellInit);
assert!(ExecutionPhase::ShellInit < ExecutionPhase::Link);
}
#[test]
fn execution_phase_category_mapping() {
assert_eq!(
ExecutionPhase::Filter.category(),
HandlerCategory::Configuration
);
assert_eq!(
ExecutionPhase::Provision.category(),
HandlerCategory::CodeExecution
);
assert_eq!(
ExecutionPhase::Setup.category(),
HandlerCategory::CodeExecution
);
assert_eq!(
ExecutionPhase::PathExport.category(),
HandlerCategory::Configuration
);
assert_eq!(
ExecutionPhase::ShellInit.category(),
HandlerCategory::Configuration
);
assert_eq!(
ExecutionPhase::Link.category(),
HandlerCategory::Configuration
);
}
#[test]
fn builtin_handler_phases() {
let fs = crate::fs::OsFs::new();
let registry = create_registry(&fs);
assert_eq!(registry[HANDLER_IGNORE].phase(), ExecutionPhase::Filter);
assert_eq!(registry[HANDLER_SKIP].phase(), ExecutionPhase::Filter);
assert_eq!(registry[HANDLER_GATE].phase(), ExecutionPhase::Filter);
assert_eq!(
registry[HANDLER_HOMEBREW].phase(),
ExecutionPhase::Provision
);
assert_eq!(registry[HANDLER_INSTALL].phase(), ExecutionPhase::Setup);
assert_eq!(registry[HANDLER_PATH].phase(), ExecutionPhase::PathExport);
assert_eq!(registry[HANDLER_SHELL].phase(), ExecutionPhase::ShellInit);
assert_eq!(registry[HANDLER_SYMLINK].phase(), ExecutionPhase::Link);
}
#[test]
fn handler_status_serializes() {
let status = HandlerStatus {
file: "vimrc".into(),
handler: "symlink".into(),
deployed: true,
message: "linked to ~/.vimrc".into(),
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("deployed"));
assert!(json.contains("linked to ~/.vimrc"));
}
#[test]
fn handler_config_default() {
let config = HandlerConfig::default();
assert!(config.force_home.is_empty());
assert!(config.protected_paths.is_empty());
}
#[test]
fn default_registry_has_exactly_one_exclusive_catchall() {
let fs = crate::fs::OsFs::new();
let registry = create_registry(&fs);
let exclusive_catchalls: Vec<&str> = registry
.values()
.filter(|h| {
h.match_mode() == MatchMode::Catchall && h.scope() == HandlerScope::Exclusive
})
.map(|h| h.name())
.collect();
assert_eq!(exclusive_catchalls, vec!["symlink"]);
}
#[test]
#[should_panic(expected = "at most one exclusive catchall handler")]
fn two_exclusive_catchalls_panic() {
struct FakeCatchall;
impl Handler for FakeCatchall {
fn name(&self) -> &str {
"fake"
}
fn phase(&self) -> ExecutionPhase {
ExecutionPhase::Link
}
fn match_mode(&self) -> MatchMode {
MatchMode::Catchall
}
fn scope(&self) -> HandlerScope {
HandlerScope::Exclusive
}
fn to_intents(
&self,
_matches: &[RuleMatch],
_config: &HandlerConfig,
_paths: &dyn Pather,
_fs: &dyn Fs,
) -> Result<Vec<HandlerIntent>> {
Ok(Vec::new())
}
fn check_status(
&self,
_file: &Path,
_pack: &str,
_datastore: &dyn DataStore,
) -> Result<HandlerStatus> {
unreachable!()
}
}
let fs = crate::fs::OsFs::new();
let mut registry = create_registry(&fs);
registry.insert("fake".into(), Box::new(FakeCatchall));
validate_registry(®istry);
}
}