use super::{CommandContext, CommandOutput};
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[async_trait]
pub trait Command: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
async fn validate(&self, _ctx: &CommandContext) -> Result<()> {
Ok(())
}
async fn execute(&self, ctx: &mut CommandContext) -> Result<CommandOutput>;
fn metadata(&self) -> CommandMetadata {
CommandMetadata {
name: self.name().to_string(),
description: self.description().to_string(),
category: None,
tags: Vec::new(),
examples: Vec::new(),
requires_config: true,
is_dangerous: false,
}
}
fn requires_config(&self) -> bool {
true
}
fn is_dangerous(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandMetadata {
pub name: String,
pub description: String,
pub category: Option<String>,
pub tags: Vec<String>,
pub examples: Vec<CommandExample>,
pub requires_config: bool,
pub is_dangerous: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandExample {
pub description: String,
pub command: String,
pub output: Option<String>,
}
impl CommandExample {
pub fn new(description: impl Into<String>, command: impl Into<String>) -> Self {
Self {
description: description.into(),
command: command.into(),
output: None,
}
}
pub fn with_output(
description: impl Into<String>,
command: impl Into<String>,
output: impl Into<String>,
) -> Self {
Self {
description: description.into(),
command: command.into(),
output: Some(output.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestCommand {
name: String,
}
#[async_trait]
impl Command for TestCommand {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Test command"
}
async fn execute(&self, _ctx: &mut CommandContext) -> Result<CommandOutput> {
Ok(CommandOutput::success("Test passed"))
}
}
#[tokio::test]
async fn test_command_trait() {
let cmd = TestCommand {
name: "test".to_string(),
};
assert_eq!(cmd.name(), "test");
assert_eq!(cmd.description(), "Test command");
assert!(cmd.requires_config());
assert!(!cmd.is_dangerous());
let mut ctx = CommandContext::mock();
let result = cmd.execute(&mut ctx).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validation() {
struct ValidatingCommand;
#[async_trait]
impl Command for ValidatingCommand {
fn name(&self) -> &str {
"validator"
}
fn description(&self) -> &str {
"Validates"
}
async fn validate(&self, ctx: &CommandContext) -> Result<()> {
if ctx.get_arg("required").is_none() {
anyhow::bail!("Missing required argument");
}
Ok(())
}
async fn execute(&self, _ctx: &mut CommandContext) -> Result<CommandOutput> {
Ok(CommandOutput::success("Executed"))
}
}
let cmd = ValidatingCommand;
let ctx = CommandContext::mock();
let result = cmd.validate(&ctx).await;
assert!(result.is_err());
}
#[test]
fn test_command_example() {
let example = CommandExample::new("List models", "inferno models list");
assert_eq!(example.description, "List models");
assert_eq!(example.command, "inferno models list");
assert!(example.output.is_none());
let example_with_output = CommandExample::with_output(
"Count models",
"inferno models list --json",
r#"{"count": 5}"#,
);
assert!(example_with_output.output.is_some());
}
}