Skip to main content

dynamic_cli/plugin/builtin/
exit.rs

1//! Standalone `ExitPlugin`.
2//!
3//! Contributes the same `system_exit` handler as [`SystemPlugin`], reusing
4//! [`SystemExitHandler`]'s logic internally — no duplicated logic (#44 /
5//! DD-025). Supports the same shutdown-callback mechanism as
6//! [`SystemPlugin::with_exit_fn`][crate::builtin::system::SystemPlugin::with_exit_fn].
7//!
8//! [`SystemPlugin`]: crate::builtin::system::SystemPlugin
9//! [`SystemExitHandler`]: crate::builtin::system::SystemExitHandler
10
11use crate::executor::CommandHandler;
12use crate::plugin::system::SystemExitHandler;
13use crate::plugin::Plugin;
14use std::sync::Arc;
15
16/// Standalone builtin providing only the `exit` command.
17///
18/// Use this instead of [`SystemPlugin`][crate::builtin::system::SystemPlugin]
19/// when an application wants `exit` without also registering `help` and
20/// `version`.
21///
22/// # Shutdown callback
23///
24/// Same mechanism as `SystemPlugin`: the callback runs **before** the
25/// process exits. The default callback calls `std::process::exit(0)`
26/// directly.
27///
28/// ```no_run
29/// use dynamic_cli::plugin::ExitPlugin;
30///
31/// let builtin = ExitPlugin::new()
32///     .with_exit_fn(|| {
33///         eprintln!("Goodbye.");
34///         std::process::exit(0);
35///     });
36/// ```
37///
38/// # YAML config
39///
40/// ```yaml
41/// commands:
42///   - name: exit
43///     implementation: system_exit
44///     description: "Exit the application"
45///     aliases: ["quit", "q"]
46///     required: false
47///     arguments: []
48///     options: []
49/// ```
50///
51/// # Example
52///
53/// ```
54/// use dynamic_cli::plugin::{ExitPlugin, Plugin};
55///
56/// let builtin = ExitPlugin::new();
57/// assert_eq!(builtin.name(), "exit");
58///
59/// let handlers = builtin.handlers();
60/// assert_eq!(handlers.len(), 1);
61/// assert_eq!(handlers[0].0, "system_exit");
62/// ```
63pub struct ExitPlugin {
64    /// Shutdown callback invoked by `system_exit`.
65    ///
66    /// Defaults to `|| std::process::exit(0)`.
67    /// Override with [`ExitPlugin::with_exit_fn`] for a clean shutdown
68    /// sequence (flush buffers, close connections, save state, etc.).
69    exit_fn: Arc<dyn Fn() + Send + Sync>,
70}
71
72impl ExitPlugin {
73    /// Create a new `ExitPlugin` with the default shutdown behaviour.
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// use dynamic_cli::plugin::{ExitPlugin, Plugin};
79    ///
80    /// let builtin = ExitPlugin::new();
81    /// assert_eq!(builtin.name(), "exit");
82    /// ```
83    pub fn new() -> Self {
84        Self {
85            exit_fn: Arc::new(|| std::process::exit(0)),
86        }
87    }
88
89    /// Supply a custom shutdown callback for `system_exit`.
90    ///
91    /// The callback must be `Fn() + Send + Sync + 'static`.
92    ///
93    /// # Example
94    ///
95    /// ```no_run
96    /// use dynamic_cli::plugin::ExitPlugin;
97    ///
98    /// let builtin = ExitPlugin::new()
99    ///     .with_exit_fn(|| {
100    ///         eprintln!("Saving session…");
101    ///         std::process::exit(0);
102    ///     });
103    /// ```
104    pub fn with_exit_fn<F>(mut self, f: F) -> Self
105    where
106        F: Fn() + Send + Sync + 'static,
107    {
108        self.exit_fn = Arc::new(f);
109        self
110    }
111}
112
113impl Default for ExitPlugin {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl Plugin for ExitPlugin {
120    fn name(&self) -> &str {
121        "exit"
122    }
123
124    fn version(&self) -> &str {
125        env!("CARGO_PKG_VERSION")
126    }
127
128    fn description(&self) -> &str {
129        "Standalone exit command (split out of SystemPlugin, #44)"
130    }
131
132    fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
133        vec![(
134            "system_exit".to_string(),
135            Box::new(SystemExitHandler::new(self.exit_fn.clone())),
136        )]
137    }
138}
139
140// ============================================================================
141// Tests
142// ============================================================================
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::context::ExecutionContext;
148    use crate::parser::ParsedArgs;
149    use std::any::Any;
150    use std::collections::HashMap;
151    use std::sync::atomic::{AtomicBool, Ordering};
152
153    #[derive(Default)]
154    struct TestContext;
155
156    impl ExecutionContext for TestContext {
157        fn as_any(&self) -> &dyn Any {
158            self
159        }
160        fn as_any_mut(&mut self) -> &mut dyn Any {
161            self
162        }
163    }
164
165    #[test]
166    fn test_exit_plugin_metadata() {
167        let p = ExitPlugin::new();
168        assert_eq!(p.name(), "exit");
169        assert!(!p.version().is_empty());
170        assert!(!p.description().is_empty());
171    }
172
173    #[test]
174    fn test_exit_plugin_default() {
175        let p = ExitPlugin::default();
176        assert_eq!(p.name(), "exit");
177    }
178
179    #[test]
180    fn test_exit_plugin_handler_name() {
181        let handlers = ExitPlugin::new().handlers();
182        assert_eq!(handlers.len(), 1);
183        assert_eq!(handlers[0].0, "system_exit");
184    }
185
186    #[test]
187    fn test_exit_default_callback_is_set() {
188        // The default callback (process::exit) cannot be invoked in tests;
189        // only check that the builtin builds and exposes the handler.
190        let plugin = ExitPlugin::new();
191        let handlers = plugin.handlers();
192        assert_eq!(handlers.len(), 1);
193    }
194
195    #[test]
196    fn test_exit_custom_callback_invoked() {
197        let called = Arc::new(AtomicBool::new(false));
198        let called_clone = called.clone();
199
200        let plugin = ExitPlugin::new().with_exit_fn(move || {
201            called_clone.store(true, Ordering::SeqCst);
202            // Does NOT call std::process::exit — safe in tests.
203        });
204
205        let handlers = plugin.handlers();
206        let (_, handler) = &handlers[0];
207
208        let mut ctx = TestContext;
209        let result = handler.execute(&mut ctx, &ParsedArgs::from_scalars(HashMap::new()));
210
211        assert!(result.is_ok());
212        assert!(
213            called.load(Ordering::SeqCst),
214            "shutdown callback was not invoked"
215        );
216    }
217
218    #[test]
219    fn test_exit_plugin_is_send_sync() {
220        fn assert_send_sync<T: Send + Sync>(_: T) {}
221        assert_send_sync(ExitPlugin::new().with_exit_fn(|| {}));
222    }
223}