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}