dodot_lib/handlers/
mod.rs1pub mod homebrew;
11pub mod install;
12pub mod path;
13pub mod shell;
14pub mod symlink;
15
16use std::collections::HashMap;
17use std::path::Path;
18
19use serde::Serialize;
20
21use crate::datastore::DataStore;
22use crate::fs::Fs;
23use crate::operations::HandlerIntent;
24use crate::paths::Pather;
25use crate::rules::RuleMatch;
26use crate::Result;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
34pub enum HandlerCategory {
35 Configuration,
36 CodeExecution,
37}
38
39#[derive(Debug, Clone, Serialize)]
41pub struct HandlerStatus {
42 pub file: String,
44 pub handler: String,
46 pub deployed: bool,
48 pub message: String,
50}
51
52pub trait Handler: Send + Sync {
63 fn name(&self) -> &str;
65
66 fn category(&self) -> HandlerCategory;
68
69 fn to_intents(
74 &self,
75 matches: &[RuleMatch],
76 config: &HandlerConfig,
77 paths: &dyn Pather,
78 ) -> Result<Vec<HandlerIntent>>;
79
80 fn check_status(
82 &self,
83 file: &Path,
84 pack: &str,
85 datastore: &dyn DataStore,
86 ) -> Result<HandlerStatus>;
87}
88
89#[derive(Debug, Clone, Serialize)]
94pub struct HandlerConfig {
95 pub force_home: Vec<String>,
97 pub protected_paths: Vec<String>,
99 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
102 pub targets: std::collections::HashMap<String, String>,
103 pub auto_chmod_exec: bool,
106}
107
108impl Default for HandlerConfig {
109 fn default() -> Self {
110 Self {
111 force_home: Vec::new(),
112 protected_paths: Vec::new(),
113 targets: std::collections::HashMap::new(),
114 auto_chmod_exec: true,
115 }
116 }
117}
118
119pub const HANDLER_SYMLINK: &str = "symlink";
121pub const HANDLER_SHELL: &str = "shell";
122pub const HANDLER_PATH: &str = "path";
123pub const HANDLER_INSTALL: &str = "install";
124pub const HANDLER_HOMEBREW: &str = "homebrew";
125
126pub fn create_registry(fs: &dyn Fs) -> HashMap<String, Box<dyn Handler + '_>> {
132 let mut registry: HashMap<String, Box<dyn Handler>> = HashMap::new();
133 registry.insert(HANDLER_SYMLINK.into(), Box::new(symlink::SymlinkHandler));
134 registry.insert(HANDLER_SHELL.into(), Box::new(shell::ShellHandler));
135 registry.insert(HANDLER_PATH.into(), Box::new(path::PathHandler));
136 registry.insert(
137 HANDLER_INSTALL.into(),
138 Box::new(install::InstallHandler::new(fs)),
139 );
140 registry.insert(
141 HANDLER_HOMEBREW.into(),
142 Box::new(homebrew::HomebrewHandler::new(fs)),
143 );
144 registry
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[allow(dead_code)]
153 fn assert_object_safe(_: &dyn Handler) {}
154
155 #[allow(dead_code)]
156 fn assert_boxable(_: Box<dyn Handler>) {}
157
158 #[test]
159 fn handler_category_eq() {
160 assert_eq!(
161 HandlerCategory::Configuration,
162 HandlerCategory::Configuration
163 );
164 assert_ne!(
165 HandlerCategory::Configuration,
166 HandlerCategory::CodeExecution
167 );
168 }
169
170 #[test]
171 fn handler_status_serializes() {
172 let status = HandlerStatus {
173 file: "vimrc".into(),
174 handler: "symlink".into(),
175 deployed: true,
176 message: "linked to ~/.vimrc".into(),
177 };
178 let json = serde_json::to_string(&status).unwrap();
179 assert!(json.contains("deployed"));
180 assert!(json.contains("linked to ~/.vimrc"));
181 }
182
183 #[test]
184 fn handler_config_default() {
185 let config = HandlerConfig::default();
186 assert!(config.force_home.is_empty());
187 assert!(config.protected_paths.is_empty());
188 }
189}