1use std::borrow::Cow;
34use std::collections::HashMap;
35
36use crate::execute::{ExecCtx, ExecError, ExecStep};
37use crate::pack::Action;
38
39pub mod pack_type;
40
41#[cfg(feature = "plugin-inventory")]
42pub use pack_type::PackTypePluginSubmission;
43pub use pack_type::{PackTypePlugin, PackTypeRegistry};
44
45pub trait ActionPlugin: Send + Sync {
56 fn name(&self) -> &str;
59
60 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError>;
68}
69
70#[derive(Default)]
77pub struct Registry {
78 actions: HashMap<Cow<'static, str>, Box<dyn ActionPlugin>>,
79}
80
81impl std::fmt::Debug for Registry {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("Registry")
86 .field("actions", &self.actions.keys().collect::<Vec<_>>())
87 .finish()
88 }
89}
90
91impl Registry {
92 #[must_use]
95 pub fn new() -> Self {
96 Self { actions: HashMap::new() }
97 }
98
99 pub fn register<P: ActionPlugin + 'static>(&mut self, plugin: P) {
104 let name: Cow<'static, str> = Cow::Owned(plugin.name().to_owned());
105 self.actions.insert(name, Box::new(plugin));
106 }
107
108 #[must_use]
111 pub fn get(&self, name: &str) -> Option<&dyn ActionPlugin> {
112 self.actions.get(name).map(std::convert::AsRef::as_ref)
113 }
114
115 #[must_use]
118 pub fn len(&self) -> usize {
119 self.actions.len()
120 }
121
122 #[must_use]
124 pub fn is_empty(&self) -> bool {
125 self.actions.is_empty()
126 }
127
128 #[must_use]
137 pub fn bootstrap() -> Self {
138 let mut reg = Self::new();
139 register_builtins(&mut reg);
140 reg
141 }
142
143 #[cfg(feature = "plugin-inventory")]
151 pub fn register_from_inventory(&mut self) {
152 for sub in inventory::iter::<PluginSubmission> {
153 let plugin = (sub.factory)();
154 let name: Cow<'static, str> = Cow::Owned(plugin.name().to_owned());
155 self.actions.insert(name, plugin);
156 }
157 }
158
159 #[cfg(feature = "plugin-inventory")]
165 #[must_use]
166 pub fn bootstrap_from_inventory() -> Self {
167 let mut reg = Self::new();
168 reg.register_from_inventory();
169 reg
170 }
171}
172
173#[cfg(feature = "plugin-inventory")]
180#[non_exhaustive]
181pub struct PluginSubmission {
182 pub factory: fn() -> Box<dyn ActionPlugin>,
185}
186
187#[cfg(feature = "plugin-inventory")]
188impl PluginSubmission {
189 #[must_use]
194 pub const fn new(factory: fn() -> Box<dyn ActionPlugin>) -> Self {
195 Self { factory }
196 }
197}
198
199#[cfg(feature = "plugin-inventory")]
200inventory::collect!(PluginSubmission);
201
202pub fn register_builtins(reg: &mut Registry) {
208 reg.register(SymlinkPlugin);
209 reg.register(UnlinkPlugin);
210 reg.register(EnvPlugin);
211 reg.register(MkdirPlugin);
212 reg.register(RmdirPlugin);
213 reg.register(RequirePlugin);
214 reg.register(WhenPlugin);
215 reg.register(ExecPlugin);
216}
217
218#[derive(Debug, Default, Clone, Copy)]
228pub struct SymlinkPlugin;
229
230impl ActionPlugin for SymlinkPlugin {
231 fn name(&self) -> &str {
232 "symlink"
233 }
234
235 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
236 match action {
237 Action::Symlink(s) => crate::execute::fs_executor::fs_symlink(s, ctx),
238 _ => Err(ExecError::ExecInvalid(format!(
239 "symlink plugin dispatched with non-symlink action `{}`",
240 action.name()
241 ))),
242 }
243 }
244}
245
246#[cfg(feature = "plugin-inventory")]
247inventory::submit!(PluginSubmission::new(|| Box::new(SymlinkPlugin)));
248
249#[derive(Debug, Default, Clone, Copy)]
255pub struct UnlinkPlugin;
256
257impl ActionPlugin for UnlinkPlugin {
258 fn name(&self) -> &str {
259 "unlink"
260 }
261
262 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
263 match action {
264 Action::Unlink(u) => crate::execute::fs_executor::fs_unlink(u, ctx),
265 _ => Err(ExecError::ExecInvalid(format!(
266 "unlink plugin dispatched with non-unlink action `{}`",
267 action.name()
268 ))),
269 }
270 }
271}
272
273#[cfg(feature = "plugin-inventory")]
274inventory::submit!(PluginSubmission::new(|| Box::new(UnlinkPlugin)));
275
276#[derive(Debug, Default, Clone, Copy)]
278pub struct EnvPlugin;
279
280impl ActionPlugin for EnvPlugin {
281 fn name(&self) -> &str {
282 "env"
283 }
284
285 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
286 match action {
287 Action::Env(e) => crate::execute::fs_executor::fs_env(e, ctx),
288 _ => Err(ExecError::ExecInvalid(format!(
289 "env plugin dispatched with non-env action `{}`",
290 action.name()
291 ))),
292 }
293 }
294}
295
296#[cfg(feature = "plugin-inventory")]
297inventory::submit!(PluginSubmission::new(|| Box::new(EnvPlugin)));
298
299#[derive(Debug, Default, Clone, Copy)]
301pub struct MkdirPlugin;
302
303impl ActionPlugin for MkdirPlugin {
304 fn name(&self) -> &str {
305 "mkdir"
306 }
307
308 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
309 match action {
310 Action::Mkdir(m) => crate::execute::fs_executor::fs_mkdir(m, ctx),
311 _ => Err(ExecError::ExecInvalid(format!(
312 "mkdir plugin dispatched with non-mkdir action `{}`",
313 action.name()
314 ))),
315 }
316 }
317}
318
319#[cfg(feature = "plugin-inventory")]
320inventory::submit!(PluginSubmission::new(|| Box::new(MkdirPlugin)));
321
322#[derive(Debug, Default, Clone, Copy)]
324pub struct RmdirPlugin;
325
326impl ActionPlugin for RmdirPlugin {
327 fn name(&self) -> &str {
328 "rmdir"
329 }
330
331 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
332 match action {
333 Action::Rmdir(r) => crate::execute::fs_executor::fs_rmdir(r, ctx),
334 _ => Err(ExecError::ExecInvalid(format!(
335 "rmdir plugin dispatched with non-rmdir action `{}`",
336 action.name()
337 ))),
338 }
339 }
340}
341
342#[cfg(feature = "plugin-inventory")]
343inventory::submit!(PluginSubmission::new(|| Box::new(RmdirPlugin)));
344
345#[derive(Debug, Default, Clone, Copy)]
347pub struct RequirePlugin;
348
349impl ActionPlugin for RequirePlugin {
350 fn name(&self) -> &str {
351 "require"
352 }
353
354 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
355 match action {
356 Action::Require(r) => crate::execute::fs_executor::fs_require(r, ctx),
357 _ => Err(ExecError::ExecInvalid(format!(
358 "require plugin dispatched with non-require action `{}`",
359 action.name()
360 ))),
361 }
362 }
363}
364
365#[cfg(feature = "plugin-inventory")]
366inventory::submit!(PluginSubmission::new(|| Box::new(RequirePlugin)));
367
368#[derive(Debug, Default, Clone, Copy)]
370pub struct WhenPlugin;
371
372impl ActionPlugin for WhenPlugin {
373 fn name(&self) -> &str {
374 "when"
375 }
376
377 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
378 match action {
379 Action::When(w) => {
380 crate::execute::fs_executor::fs_when(w, ctx)
387 }
388 _ => Err(ExecError::ExecInvalid(format!(
389 "when plugin dispatched with non-when action `{}`",
390 action.name()
391 ))),
392 }
393 }
394}
395
396#[cfg(feature = "plugin-inventory")]
397inventory::submit!(PluginSubmission::new(|| Box::new(WhenPlugin)));
398
399#[derive(Debug, Default, Clone, Copy)]
401pub struct ExecPlugin;
402
403impl ActionPlugin for ExecPlugin {
404 fn name(&self) -> &str {
405 "exec"
406 }
407
408 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
409 match action {
410 Action::Exec(x) => crate::execute::fs_executor::fs_exec(x, ctx),
411 _ => Err(ExecError::ExecInvalid(format!(
412 "exec plugin dispatched with non-exec action `{}`",
413 action.name()
414 ))),
415 }
416 }
417}
418
419#[cfg(feature = "plugin-inventory")]
420inventory::submit!(PluginSubmission::new(|| Box::new(ExecPlugin)));
421
422#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn registry_new_is_empty() {
430 let reg = Registry::new();
431 assert!(reg.is_empty());
432 assert_eq!(reg.len(), 0);
433 assert!(reg.get("symlink").is_none());
434 }
435
436 #[test]
437 fn registry_register_is_last_writer_wins() {
438 let mut reg = Registry::new();
442 reg.register(SymlinkPlugin);
443 reg.register(SymlinkPlugin);
444 assert_eq!(reg.len(), 1);
445 assert!(reg.get("symlink").is_some());
446 }
447
448 #[test]
449 fn bootstrap_registers_all_eight_builtins() {
450 let reg = Registry::bootstrap();
451 assert_eq!(reg.len(), 8);
452 for name in ["symlink", "unlink", "env", "mkdir", "rmdir", "require", "when", "exec"] {
453 let plugin = reg.get(name).unwrap_or_else(|| panic!("missing built-in `{name}`"));
454 assert_eq!(plugin.name(), name);
455 }
456 assert!(reg.get("unknown").is_none());
457 }
458
459 #[cfg(feature = "plugin-inventory")]
460 #[test]
461 fn bootstrap_from_inventory_registers_all_eight_builtins() {
462 let reg = Registry::bootstrap_from_inventory();
463 assert_eq!(reg.len(), 8);
464 for name in ["symlink", "unlink", "env", "mkdir", "rmdir", "require", "when", "exec"] {
465 let plugin = reg.get(name).unwrap_or_else(|| panic!("missing built-in `{name}`"));
466 assert_eq!(plugin.name(), name);
467 }
468 }
469
470 #[cfg(feature = "plugin-inventory")]
471 #[test]
472 fn register_from_inventory_on_empty_registry_produces_eight_entries() {
473 let mut reg = Registry::new();
474 assert!(reg.is_empty());
475 reg.register_from_inventory();
476 assert_eq!(reg.len(), 8);
477 for name in ["symlink", "unlink", "env", "mkdir", "rmdir", "require", "when", "exec"] {
478 assert!(reg.get(name).is_some(), "missing built-in `{name}`");
479 }
480 }
481
482 #[cfg(feature = "plugin-inventory")]
483 #[test]
484 fn register_from_inventory_twice_dedups_to_eight() {
485 let mut reg = Registry::new();
486 reg.register_from_inventory();
487 reg.register_from_inventory();
488 assert_eq!(reg.len(), 8);
489 }
490}