use crate::config::schema::CommandsConfig;
use crate::context::ExecutionContext;
use crate::executor::CommandHandler;
use crate::help::{DefaultHelpFormatter, HelpFormatter};
use crate::plugin::Plugin;
use crate::Result;
use std::collections::HashMap;
use std::sync::Arc;
pub struct SystemPlugin {
config: Option<CommandsConfig>,
exit_fn: Arc<dyn Fn() + Send + Sync>,
}
impl SystemPlugin {
pub fn new() -> Self {
Self {
config: None,
exit_fn: Arc::new(|| std::process::exit(0)),
}
}
pub fn with_config(mut self, config: CommandsConfig) -> Self {
self.config = Some(config);
self
}
pub fn with_exit_fn<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.exit_fn = Arc::new(f);
self
}
}
impl Default for SystemPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for SystemPlugin {
fn name(&self) -> &str {
"system"
}
fn version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
fn description(&self) -> &str {
"Built-in system commands: help, version, exit"
}
fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
let config = self.config.clone();
let exit_fn = self.exit_fn.clone();
vec![
(
"system_help".to_string(),
Box::new(SystemHelpHandler {
config: config.clone(),
}),
),
(
"system_version".to_string(),
Box::new(SystemVersionHandler { config }),
),
(
"system_exit".to_string(),
Box::new(SystemExitHandler { exit_fn }),
),
]
}
}
struct SystemHelpHandler {
config: Option<CommandsConfig>,
}
impl CommandHandler for SystemHelpHandler {
fn execute(
&self,
_ctx: &mut dyn ExecutionContext,
args: &HashMap<String, String>,
) -> Result<()> {
let formatter = DefaultHelpFormatter::new();
match self.config.as_ref() {
Some(cfg) => {
if let Some(command) = args.get("command") {
print!("{}", formatter.format_command(cfg, command));
} else {
print!("{}", formatter.format_app(cfg));
}
}
None => {
println!("Help is not available (no configuration loaded).");
}
}
Ok(())
}
}
struct SystemVersionHandler {
config: Option<CommandsConfig>,
}
impl CommandHandler for SystemVersionHandler {
fn execute(
&self,
_ctx: &mut dyn ExecutionContext,
_args: &HashMap<String, String>,
) -> Result<()> {
match self.config.as_ref() {
Some(cfg) => println!("{}", cfg.metadata.version),
None => println!("(version unknown)"),
}
Ok(())
}
}
struct SystemExitHandler {
exit_fn: Arc<dyn Fn() + Send + Sync>,
}
impl CommandHandler for SystemExitHandler {
fn execute(
&self,
_ctx: &mut dyn ExecutionContext,
_args: &HashMap<String, String>,
) -> Result<()> {
(self.exit_fn)();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::schema::{CommandsConfig, Metadata};
use std::any::Any;
#[derive(Default)]
struct TestContext;
impl ExecutionContext for TestContext {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
fn test_config() -> CommandsConfig {
CommandsConfig {
metadata: Metadata {
version: "2.0.0".to_string(),
prompt: "testapp".to_string(),
prompt_suffix: " > ".to_string(),
},
commands: vec![],
global_options: vec![],
}
}
#[test]
fn test_system_plugin_metadata() {
let p = SystemPlugin::new();
assert_eq!(p.name(), "system");
assert!(!p.version().is_empty());
assert!(!p.description().is_empty());
}
#[test]
fn test_system_plugin_default() {
let p = SystemPlugin::default();
assert_eq!(p.name(), "system");
}
#[test]
fn test_system_plugin_handler_names() {
let handlers = SystemPlugin::new().handlers();
let names: Vec<&str> = handlers.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"system_help"));
assert!(names.contains(&"system_version"));
assert!(names.contains(&"system_exit"));
assert_eq!(handlers.len(), 3);
}
#[test]
fn test_system_plugin_with_config() {
let plugin = SystemPlugin::new().with_config(test_config());
assert!(plugin.config.is_some());
assert_eq!(plugin.config.unwrap().metadata.version, "2.0.0");
}
#[test]
fn test_system_version_handler_with_config() {
let plugin = SystemPlugin::new().with_config(test_config());
let handlers = plugin.handlers();
let (name, handler) = handlers
.iter()
.find(|(n, _)| n == "system_version")
.unwrap();
assert_eq!(name, "system_version");
let mut ctx = TestContext;
assert!(handler.execute(&mut ctx, &HashMap::new()).is_ok());
}
#[test]
fn test_system_version_handler_without_config() {
let handlers = SystemPlugin::new().handlers();
let (_, handler) = handlers
.iter()
.find(|(n, _)| n == "system_version")
.unwrap();
let mut ctx = TestContext;
assert!(handler.execute(&mut ctx, &HashMap::new()).is_ok());
}
#[test]
fn test_system_help_handler_with_config() {
let plugin = SystemPlugin::new().with_config(test_config());
let handlers = plugin.handlers();
let (_, handler) = handlers.iter().find(|(n, _)| n == "system_help").unwrap();
let mut ctx = TestContext;
assert!(handler.execute(&mut ctx, &HashMap::new()).is_ok());
}
#[test]
fn test_system_help_handler_with_command_arg() {
let plugin = SystemPlugin::new().with_config(test_config());
let handlers = plugin.handlers();
let (_, handler) = handlers.iter().find(|(n, _)| n == "system_help").unwrap();
let mut ctx = TestContext;
let mut args = HashMap::new();
args.insert("command".to_string(), "nonexistent".to_string());
assert!(handler.execute(&mut ctx, &args).is_ok());
}
#[test]
fn test_system_help_handler_without_config() {
let handlers = SystemPlugin::new().handlers();
let (_, handler) = handlers.iter().find(|(n, _)| n == "system_help").unwrap();
let mut ctx = TestContext;
assert!(handler.execute(&mut ctx, &HashMap::new()).is_ok());
}
#[test]
fn test_system_exit_default_callback_is_set() {
let plugin = SystemPlugin::new();
let handlers = plugin.handlers();
assert!(handlers.iter().any(|(n, _)| n == "system_exit"));
}
#[test]
fn test_system_exit_custom_callback_invoked() {
use std::sync::atomic::{AtomicBool, Ordering};
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
let plugin = SystemPlugin::new().with_exit_fn(move || {
called_clone.store(true, Ordering::SeqCst);
});
let handlers = plugin.handlers();
let (_, handler) = handlers.iter().find(|(n, _)| n == "system_exit").unwrap();
let mut ctx = TestContext;
let result = handler.execute(&mut ctx, &HashMap::new());
assert!(result.is_ok());
assert!(
called.load(Ordering::SeqCst),
"shutdown callback was not invoked"
);
}
#[test]
fn test_system_exit_callback_ignores_args() {
use std::sync::atomic::{AtomicBool, Ordering};
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
let plugin = SystemPlugin::new().with_exit_fn(move || {
called_clone.store(true, Ordering::SeqCst);
});
let handlers = plugin.handlers();
let (_, handler) = handlers.iter().find(|(n, _)| n == "system_exit").unwrap();
let mut ctx = TestContext;
let mut args = HashMap::new();
args.insert("unexpected_arg".to_string(), "value".to_string());
assert!(handler.execute(&mut ctx, &args).is_ok());
assert!(called.load(Ordering::SeqCst));
}
#[test]
fn test_with_exit_fn_is_send_sync() {
fn assert_send_sync<T: Send + Sync>(_: T) {}
let plugin = SystemPlugin::new().with_exit_fn(|| {});
assert_send_sync(plugin);
}
}