use crate::context::ContextRegistry;
use clap::ArgMatches;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use super::dispatch::{render_handler_output, DispatchFn};
use crate::cli::handler::{CommandContext, FnHandler, Handler, HandlerResult};
use crate::cli::hooks::{Hooks, RenderedOutput, TextOutput};
use standout_dispatch::verify::ExpectedArg;
use standout_pipe::PipeTarget;
pub(crate) trait CommandRecipe {
#[allow(dead_code)]
fn template(&self) -> Option<&str>;
#[allow(dead_code)]
fn hooks(&self) -> Option<&Hooks>;
#[allow(dead_code)]
fn take_hooks(&mut self) -> Option<Hooks>;
fn create_dispatch(
&self,
template: &str,
context_registry: &ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn;
fn expected_args(&self) -> Vec<ExpectedArg>;
}
pub(crate) struct ClosureRecipe<F, T>
where
F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T> + 'static,
T: Serialize + 'static,
{
handler: Rc<RefCell<FnHandler<F, T>>>,
template: Option<String>,
hooks: Option<Hooks>,
}
impl<F, T> ClosureRecipe<F, T>
where
F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T> + 'static,
T: Serialize + 'static,
{
pub fn new(handler: FnHandler<F, T>) -> Self {
Self {
handler: Rc::new(RefCell::new(handler)),
template: None,
hooks: None,
}
}
#[allow(dead_code)]
pub fn with_template(mut self, template: String) -> Self {
self.template = Some(template);
self
}
#[allow(dead_code)]
pub fn with_hooks(mut self, hooks: Hooks) -> Self {
self.hooks = Some(hooks);
self
}
}
impl<F, T> CommandRecipe for ClosureRecipe<F, T>
where
F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T> + 'static,
T: Serialize + 'static,
{
fn template(&self) -> Option<&str> {
self.template.as_deref()
}
fn hooks(&self) -> Option<&Hooks> {
self.hooks.as_ref()
}
fn take_hooks(&mut self) -> Option<Hooks> {
self.hooks.take()
}
fn create_dispatch(
&self,
template: &str,
context_registry: &ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn {
let handler = self.handler.clone();
let template = template.to_string();
let context_registry = context_registry.clone();
Rc::new(RefCell::new(
move |matches: &ArgMatches,
ctx: &CommandContext,
hooks: Option<&Hooks>,
output_mode: crate::OutputMode,
theme: &crate::Theme| {
let result = handler
.borrow_mut()
.handle(matches, ctx)
.map_err(|e| e.to_string());
render_handler_output(
result,
matches,
ctx,
hooks,
&template,
theme,
&context_registry,
&**template_engine,
output_mode,
)
},
))
}
fn expected_args(&self) -> Vec<ExpectedArg> {
self.handler.borrow().expected_args()
}
}
pub(crate) struct StructRecipe<H, T>
where
H: Handler<Output = T> + 'static,
T: Serialize + 'static,
{
handler: Rc<RefCell<H>>,
#[allow(dead_code)]
template: Option<String>,
hooks: Option<Hooks>,
_phantom: std::marker::PhantomData<T>,
}
impl<H, T> StructRecipe<H, T>
where
H: Handler<Output = T> + 'static,
T: Serialize + 'static,
{
pub fn new(handler: H) -> Self {
Self {
handler: Rc::new(RefCell::new(handler)),
template: None,
hooks: None,
_phantom: std::marker::PhantomData,
}
}
#[allow(dead_code)]
pub fn with_template(mut self, template: String) -> Self {
self.template = Some(template);
self
}
#[allow(dead_code)]
pub fn with_hooks(mut self, hooks: Hooks) -> Self {
self.hooks = Some(hooks);
self
}
}
impl<H, T> CommandRecipe for StructRecipe<H, T>
where
H: Handler<Output = T> + 'static,
T: Serialize + 'static,
{
fn template(&self) -> Option<&str> {
self.template.as_deref()
}
fn hooks(&self) -> Option<&Hooks> {
self.hooks.as_ref()
}
fn take_hooks(&mut self) -> Option<Hooks> {
self.hooks.take()
}
fn create_dispatch(
&self,
template: &str,
context_registry: &ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn {
let handler = self.handler.clone();
let template = template.to_string();
let context_registry = context_registry.clone();
Rc::new(RefCell::new(
move |matches: &ArgMatches,
ctx: &CommandContext,
hooks: Option<&Hooks>,
output_mode: crate::OutputMode,
theme: &crate::Theme| {
let result = handler
.borrow_mut()
.handle(matches, ctx)
.map_err(|e| e.to_string());
render_handler_output(
result,
matches,
ctx,
hooks,
&template,
theme,
&context_registry,
&**template_engine,
output_mode,
)
},
))
}
fn expected_args(&self) -> Vec<ExpectedArg> {
self.handler.borrow().expected_args()
}
}
pub(crate) struct ErasedConfigRecipe {
config: RefCell<Option<Box<dyn ErasedCommandConfig>>>,
#[allow(dead_code)]
template: Option<String>,
#[allow(dead_code)]
hooks: RefCell<Option<Hooks>>,
}
impl ErasedConfigRecipe {
pub fn from_handler(mut handler: Box<dyn ErasedCommandConfig>) -> Self {
let template = handler.template().map(String::from);
let hooks = handler.take_hooks();
Self {
config: RefCell::new(Some(handler)),
template,
hooks: RefCell::new(hooks),
}
}
}
impl CommandRecipe for ErasedConfigRecipe {
fn template(&self) -> Option<&str> {
self.template.as_deref()
}
fn hooks(&self) -> Option<&Hooks> {
None
}
fn take_hooks(&mut self) -> Option<Hooks> {
self.hooks.borrow_mut().take()
}
fn create_dispatch(
&self,
template: &str,
context_registry: &ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn {
let config = self
.config
.borrow_mut()
.take()
.expect("ErasedConfigRecipe::create_dispatch called more than once");
config.register(
"",
template.to_string(),
context_registry.clone(),
template_engine,
)
}
fn expected_args(&self) -> Vec<ExpectedArg> {
if let Some(config) = self.config.borrow().as_ref() {
config.expected_args()
} else {
Vec::new()
}
}
}
pub(crate) struct PassthroughRecipe<F>
where
F: FnMut(&ArgMatches, &CommandContext) -> Result<(), anyhow::Error> + 'static,
{
handler: Rc<RefCell<F>>,
}
impl<F> PassthroughRecipe<F>
where
F: FnMut(&ArgMatches, &CommandContext) -> Result<(), anyhow::Error> + 'static,
{
pub fn new(handler: F) -> Self {
Self {
handler: Rc::new(RefCell::new(handler)),
}
}
}
impl<F> CommandRecipe for PassthroughRecipe<F>
where
F: FnMut(&ArgMatches, &CommandContext) -> Result<(), anyhow::Error> + 'static,
{
fn template(&self) -> Option<&str> {
None
}
fn hooks(&self) -> Option<&Hooks> {
None
}
fn take_hooks(&mut self) -> Option<Hooks> {
None
}
fn create_dispatch(
&self,
_template: &str,
_context_registry: &ContextRegistry,
_template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn {
let handler = self.handler.clone();
Rc::new(RefCell::new(
move |matches: &ArgMatches,
ctx: &CommandContext,
_hooks: Option<&Hooks>,
_output_mode: crate::OutputMode,
_theme: &crate::Theme| {
let result = (handler.borrow_mut())(matches, ctx);
match result {
Ok(()) => Ok(super::dispatch::DispatchOutput::Silent),
Err(e) => Err(format!("Error: {}", e)),
}
},
))
}
fn expected_args(&self) -> Vec<ExpectedArg> {
Vec::new()
}
}
pub struct CommandConfig<H> {
pub(crate) handler: H,
pub(crate) template: Option<String>,
pub(crate) hooks: Option<Hooks>,
}
impl<H> CommandConfig<H> {
pub fn new(handler: H) -> Self {
Self {
handler,
template: None,
hooks: None,
}
}
pub fn template(mut self, template: impl Into<String>) -> Self {
self.template = Some(template.into());
self
}
pub fn hooks(mut self, hooks: Hooks) -> Self {
self.hooks = Some(hooks);
self
}
pub fn pre_dispatch<F>(mut self, f: F) -> Self
where
F: Fn(&ArgMatches, &mut CommandContext) -> Result<(), crate::cli::hooks::HookError>
+ 'static,
{
let hooks = self.hooks.take().unwrap_or_default();
self.hooks = Some(hooks.pre_dispatch(f));
self
}
pub fn post_dispatch<F>(mut self, f: F) -> Self
where
F: Fn(
&ArgMatches,
&CommandContext,
serde_json::Value,
) -> Result<serde_json::Value, crate::cli::hooks::HookError>
+ 'static,
{
let hooks = self.hooks.take().unwrap_or_default();
self.hooks = Some(hooks.post_dispatch(f));
self
}
pub fn post_output<F>(mut self, f: F) -> Self
where
F: Fn(
&ArgMatches,
&CommandContext,
crate::cli::hooks::RenderedOutput,
)
-> Result<crate::cli::hooks::RenderedOutput, crate::cli::hooks::HookError>
+ 'static,
{
let hooks = self.hooks.take().unwrap_or_default();
self.hooks = Some(hooks.post_output(f));
self
}
pub fn pipe_to(self, command: impl Into<String>) -> Self {
self.pipe_to_with_timeout(command, std::time::Duration::from_secs(30))
}
pub fn pipe_to_with_timeout(
self,
command: impl Into<String>,
timeout: std::time::Duration,
) -> Self {
let command = command.into();
self.post_output(move |_matches, _ctx, output| {
if let RenderedOutput::Text(ref text_output) = output {
let pipe = standout_pipe::SimplePipe::new(command.clone()).with_timeout(timeout);
pipe.pipe(&text_output.raw)
.map_err(|e| crate::cli::hooks::HookError::post_output(e.to_string()))?;
Ok(output)
} else {
Ok(output)
}
})
}
pub fn pipe_through(self, command: impl Into<String>) -> Self {
self.pipe_through_with_timeout(command, std::time::Duration::from_secs(30))
}
pub fn pipe_through_with_timeout(
self,
command: impl Into<String>,
timeout: std::time::Duration,
) -> Self {
let command = command.into();
self.post_output(move |_matches, _ctx, output| {
if let RenderedOutput::Text(ref text_output) = output {
let pipe = standout_pipe::SimplePipe::new(command.clone())
.capture()
.with_timeout(timeout);
let result = pipe
.pipe(&text_output.raw)
.map_err(|e| crate::cli::hooks::HookError::post_output(e.to_string()))?;
Ok(RenderedOutput::Text(TextOutput::plain(result)))
} else {
Ok(output)
}
})
}
pub fn pipe_to_clipboard(self) -> Self {
self.post_output(move |_matches, _ctx, output| {
if let RenderedOutput::Text(ref text_output) = output {
if let Some(pipe) = standout_pipe::clipboard() {
let result = pipe
.pipe(&text_output.raw)
.map_err(|e| crate::cli::hooks::HookError::post_output(e.to_string()))?;
Ok(RenderedOutput::Text(TextOutput::plain(result)))
} else {
Err(crate::cli::hooks::HookError::post_output(
"Clipboard not supported on this platform. \
Use pipe_to() with a platform-specific clipboard command.",
))
}
} else {
Ok(output)
}
})
}
pub fn pipe_with<P>(self, target: P) -> Self
where
P: standout_pipe::PipeTarget + 'static,
{
let target = Rc::new(target);
self.post_output(move |_matches, _ctx, output| {
if let RenderedOutput::Text(ref text_output) = output {
let result = target
.pipe(&text_output.raw)
.map_err(|e| crate::cli::hooks::HookError::post_output(e.to_string()))?;
Ok(RenderedOutput::Text(TextOutput::plain(result)))
} else {
Ok(output)
}
})
}
}
pub(crate) enum GroupEntry {
Command {
handler: Box<dyn ErasedCommandConfig>,
},
Group { builder: GroupBuilder },
}
pub(crate) trait ErasedCommandConfig {
fn template(&self) -> Option<&str>;
#[allow(dead_code)]
fn hooks(&self) -> Option<&Hooks>;
fn take_hooks(&mut self) -> Option<Hooks>;
fn register(
self: Box<Self>,
path: &str,
template: String,
context_registry: ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn;
fn expected_args(&self) -> Vec<ExpectedArg>;
}
#[derive(Default)]
pub struct GroupBuilder {
pub(crate) entries: HashMap<String, GroupEntry>,
pub(crate) default_command: Option<String>,
}
impl GroupBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn contains(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get_default_command(&self) -> Option<&str> {
self.default_command.as_deref()
}
pub fn command<F, T>(self, name: &str, handler: F) -> Self
where
F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T> + 'static,
T: Serialize + 'static,
{
self.command_with(name, handler, |cfg| cfg)
}
pub fn command_with<F, T, C>(mut self, name: &str, handler: F, configure: C) -> Self
where
F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T> + 'static,
T: Serialize + 'static,
C: FnOnce(CommandConfig<FnHandler<F, T>>) -> CommandConfig<FnHandler<F, T>>,
{
let config = CommandConfig::new(FnHandler::new(handler));
let config = configure(config);
self.entries.insert(
name.to_string(),
GroupEntry::Command {
handler: Box::new(ClosureCommandConfig {
handler: Rc::new(RefCell::new(config.handler)),
template: config.template,
hooks: config.hooks,
}),
},
);
self
}
pub fn handler<H, T>(self, name: &str, handler: H) -> Self
where
H: Handler<Output = T> + 'static,
T: Serialize + 'static,
{
self.handler_with(name, handler, |cfg| cfg)
}
pub fn handler_with<H, T, C>(mut self, name: &str, handler: H, configure: C) -> Self
where
H: Handler<Output = T> + 'static,
T: Serialize + 'static,
C: FnOnce(CommandConfig<H>) -> CommandConfig<H>,
{
let config = CommandConfig::new(handler);
let config = configure(config);
self.entries.insert(
name.to_string(),
GroupEntry::Command {
handler: Box::new(StructCommandConfig {
handler: Rc::new(RefCell::new(config.handler)),
template: config.template,
hooks: config.hooks,
}),
},
);
self
}
pub fn passthrough<F>(mut self, name: &str, handler: F) -> Self
where
F: FnMut(&ArgMatches, &CommandContext) -> Result<(), anyhow::Error> + 'static,
{
self.entries.insert(
name.to_string(),
GroupEntry::Command {
handler: Box::new(PassthroughCommandConfig {
handler: Rc::new(RefCell::new(handler)),
}),
},
);
self
}
pub fn group<F>(mut self, name: &str, configure: F) -> Self
where
F: FnOnce(GroupBuilder) -> GroupBuilder,
{
let builder = configure(GroupBuilder::new());
self.entries
.insert(name.to_string(), GroupEntry::Group { builder });
self
}
pub fn default_command(mut self, name: &str) -> Self {
if let Some(existing) = &self.default_command {
panic!(
"Only one default command can be defined. '{}' is already set as default.",
existing
);
}
self.default_command = Some(name.to_string());
self
}
}
struct ClosureCommandConfig<F, T>
where
F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T> + 'static,
T: Serialize + 'static,
{
handler: Rc<RefCell<FnHandler<F, T>>>,
template: Option<String>,
hooks: Option<Hooks>,
}
impl<F, T> ErasedCommandConfig for ClosureCommandConfig<F, T>
where
F: FnMut(&ArgMatches, &CommandContext) -> HandlerResult<T> + 'static,
T: Serialize + 'static,
{
fn template(&self) -> Option<&str> {
self.template.as_deref()
}
fn hooks(&self) -> Option<&Hooks> {
self.hooks.as_ref()
}
fn take_hooks(&mut self) -> Option<Hooks> {
self.hooks.take()
}
fn register(
self: Box<Self>,
_path: &str,
template: String,
context_registry: ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn {
let handler = self.handler;
Rc::new(RefCell::new(
move |matches: &ArgMatches,
ctx: &CommandContext,
hooks: Option<&Hooks>,
output_mode: crate::OutputMode,
theme: &crate::Theme| {
let result = handler
.borrow_mut()
.handle(matches, ctx)
.map_err(|e| e.to_string());
render_handler_output(
result,
matches,
ctx,
hooks,
&template,
theme,
&context_registry,
&**template_engine,
output_mode,
)
},
))
}
fn expected_args(&self) -> Vec<ExpectedArg> {
self.handler.borrow().expected_args()
}
}
struct StructCommandConfig<H, T>
where
H: Handler<Output = T> + 'static,
T: Serialize + 'static,
{
handler: Rc<RefCell<H>>,
template: Option<String>,
hooks: Option<Hooks>,
}
impl<H, T> ErasedCommandConfig for StructCommandConfig<H, T>
where
H: Handler<Output = T> + 'static,
T: Serialize + 'static,
{
fn template(&self) -> Option<&str> {
self.template.as_deref()
}
fn hooks(&self) -> Option<&Hooks> {
self.hooks.as_ref()
}
fn take_hooks(&mut self) -> Option<Hooks> {
self.hooks.take()
}
fn register(
self: Box<Self>,
_path: &str,
template: String,
context_registry: ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn {
let handler = self.handler;
Rc::new(RefCell::new(
move |matches: &ArgMatches,
ctx: &CommandContext,
hooks: Option<&Hooks>,
output_mode: crate::OutputMode,
theme: &crate::Theme| {
let result = handler
.borrow_mut()
.handle(matches, ctx)
.map_err(|e| e.to_string());
render_handler_output(
result,
matches,
ctx,
hooks,
&template,
theme,
&context_registry,
&**template_engine,
output_mode,
)
},
))
}
fn expected_args(&self) -> Vec<ExpectedArg> {
self.handler.borrow().expected_args()
}
}
struct PassthroughCommandConfig<F>
where
F: FnMut(&ArgMatches, &CommandContext) -> Result<(), anyhow::Error> + 'static,
{
handler: Rc<RefCell<F>>,
}
impl<F> ErasedCommandConfig for PassthroughCommandConfig<F>
where
F: FnMut(&ArgMatches, &CommandContext) -> Result<(), anyhow::Error> + 'static,
{
fn template(&self) -> Option<&str> {
None
}
fn hooks(&self) -> Option<&Hooks> {
None
}
fn take_hooks(&mut self) -> Option<Hooks> {
None
}
fn register(
self: Box<Self>,
_path: &str,
_template: String,
_context_registry: ContextRegistry,
_template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
) -> DispatchFn {
let handler = self.handler;
Rc::new(RefCell::new(
move |matches: &ArgMatches,
ctx: &CommandContext,
_hooks: Option<&Hooks>,
_output_mode: crate::OutputMode,
_theme: &crate::Theme| {
let result = (handler.borrow_mut())(matches, ctx);
match result {
Ok(()) => Ok(super::dispatch::DispatchOutput::Silent),
Err(e) => Err(format!("Error: {}", e)),
}
},
))
}
fn expected_args(&self) -> Vec<ExpectedArg> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::handler::Output as HandlerOutput;
use serde_json::json;
#[test]
fn test_group_builder_creation() {
let group = GroupBuilder::new();
assert!(group.entries.is_empty());
}
#[test]
fn test_group_builder_command() {
let group = GroupBuilder::new().command("test", |_m, _ctx| {
Ok(HandlerOutput::Render(json!({"ok": true})))
});
assert!(group.entries.contains_key("test"));
}
#[test]
fn test_group_builder_nested() {
let group = GroupBuilder::new()
.command("top", |_m, _ctx| Ok(HandlerOutput::Render(json!({}))))
.group("nested", |g| {
g.command("inner", |_m, _ctx| Ok(HandlerOutput::Render(json!({}))))
});
assert!(group.entries.contains_key("top"));
assert!(group.entries.contains_key("nested"));
}
#[test]
fn test_command_config_template() {
let config =
CommandConfig::new(FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
Ok(HandlerOutput::Render(json!({})))
}))
.template("custom.j2");
assert_eq!(config.template, Some("custom.j2".to_string()));
}
#[test]
fn test_command_config_hooks() {
let config =
CommandConfig::new(FnHandler::new(|_m: &ArgMatches, _ctx: &CommandContext| {
Ok(HandlerOutput::Render(json!({})))
}))
.pre_dispatch(|_, _| Ok(()));
assert!(config.hooks.is_some());
}
#[test]
fn test_group_builder_default_command() {
let group = GroupBuilder::new()
.command("list", |_m, _ctx| Ok(HandlerOutput::Render(json!({}))))
.command("add", |_m, _ctx| Ok(HandlerOutput::Render(json!({}))))
.default_command("list");
assert_eq!(group.default_command, Some("list".to_string()));
}
#[test]
#[should_panic(expected = "Only one default command can be defined")]
fn test_group_builder_duplicate_default_command_panics() {
let _ = GroupBuilder::new()
.command("list", |_m, _ctx| Ok(HandlerOutput::Render(json!({}))))
.command("add", |_m, _ctx| Ok(HandlerOutput::Render(json!({}))))
.default_command("list")
.default_command("add");
}
}