use anyhow::Result; use clap::Subcommand; use serde::{Deserialize, Serialize}; use crate::client::ApiClient; use crate::config::CliConfig; use crate::output::{self, OutputFormat}; #[derive(Subcommand)] pub enum RuleCommands { /// List all rules List { /// Filter by pack name #[arg(long)] pack: Option, /// Filter by enabled status #[arg(short, long)] enabled: Option, }, /// Show details of a specific rule Show { /// Rule reference (pack.rule or ID) rule_ref: String, }, /// Update a rule Update { /// Rule reference (pack.rule or ID) rule_ref: String, /// Update label #[arg(long)] label: Option, /// Update description #[arg(long)] description: Option, /// Update conditions as JSON string #[arg(long)] conditions: Option, /// Update action parameters as JSON string #[arg(long)] action_params: Option, /// Update trigger parameters as JSON string #[arg(long)] trigger_params: Option, /// Update enabled status #[arg(long)] enabled: Option, }, /// Enable a rule Enable { /// Rule reference (pack.rule or ID) rule_ref: String, }, /// Disable a rule Disable { /// Rule reference (pack.rule or ID) rule_ref: String, }, /// Create a new rule Create { /// Rule name #[arg(short, long)] name: String, /// Pack ID or name #[arg(short, long)] pack: String, /// Trigger reference #[arg(short, long)] trigger: String, /// Action reference #[arg(short, long)] action: String, /// Rule description #[arg(short, long)] description: Option, /// Rule criteria as JSON string #[arg(long)] criteria: Option, /// Enable the rule immediately #[arg(long)] enabled: bool, }, /// Delete a rule Delete { /// Rule reference (pack.rule or ID) rule_ref: String, /// Skip confirmation prompt #[arg(short = 'y', long)] yes: bool, }, } #[derive(Debug, Serialize, Deserialize)] struct Rule { id: i64, #[serde(rename = "ref")] rule_ref: String, #[serde(default)] pack: Option, pack_ref: String, label: String, description: String, #[serde(default)] trigger: Option, trigger_ref: String, #[serde(default)] action: Option, action_ref: String, enabled: bool, created: String, updated: String, } #[derive(Debug, Serialize, Deserialize)] struct RuleDetail { id: i64, #[serde(rename = "ref")] rule_ref: String, #[serde(default)] pack: Option, pack_ref: String, label: String, description: String, #[serde(default)] trigger: Option, trigger_ref: String, #[serde(default)] action: Option, action_ref: String, enabled: bool, #[serde(default)] conditions: Option, #[serde(default)] action_params: Option, #[serde(default)] trigger_params: Option, created: String, updated: String, } #[derive(Debug, Serialize)] struct CreateRuleRequest { name: String, pack_id: String, trigger_id: String, action_id: String, description: Option, criteria: Option, enabled: bool, } #[derive(Debug, Serialize)] struct UpdateRuleRequest { enabled: bool, } pub async fn handle_rule_command( profile: &Option, command: RuleCommands, api_url: &Option, output_format: OutputFormat, ) -> Result<()> { match command { RuleCommands::List { pack, enabled } => { handle_list(profile, pack, enabled, api_url, output_format).await } RuleCommands::Show { rule_ref } => { handle_show(profile, rule_ref, api_url, output_format).await } RuleCommands::Update { rule_ref, label, description, conditions, action_params, trigger_params, enabled, } => { handle_update( profile, rule_ref, label, description, conditions, action_params, trigger_params, enabled, api_url, output_format, ) .await } RuleCommands::Enable { rule_ref } => { handle_toggle(profile, rule_ref, true, api_url, output_format).await } RuleCommands::Disable { rule_ref } => { handle_toggle(profile, rule_ref, false, api_url, output_format).await } RuleCommands::Create { name, pack, trigger, action, description, criteria, enabled, } => { handle_create( profile, name, pack, trigger, action, description, criteria, enabled, api_url, output_format, ) .await } RuleCommands::Delete { rule_ref, yes } => { handle_delete(profile, rule_ref, yes, api_url, output_format).await } } } async fn handle_list( profile: &Option, pack: Option, enabled: Option, api_url: &Option, output_format: OutputFormat, ) -> Result<()> { let config = CliConfig::load_with_profile(profile.as_deref())?; let mut client = ApiClient::from_config(&config, api_url); let mut query_params = Vec::new(); if let Some(pack_name) = pack { query_params.push(format!("pack={}", pack_name)); } if let Some(is_enabled) = enabled { query_params.push(format!("enabled={}", is_enabled)); } let path = if query_params.is_empty() { "/rules".to_string() } else { format!("/rules?{}", query_params.join("&")) }; let rules: Vec = client.get(&path).await?; match output_format { OutputFormat::Json | OutputFormat::Yaml => { output::print_output(&rules, output_format)?; } OutputFormat::Table => { if rules.is_empty() { output::print_info("No rules found"); } else { let mut table = output::create_table(); output::add_header( &mut table, vec!["ID", "Pack", "Name", "Trigger", "Action", "Enabled"], ); for rule in rules { table.add_row(vec![ rule.id.to_string(), rule.pack_ref.clone(), rule.label.clone(), rule.trigger_ref.clone(), rule.action_ref.clone(), output::format_bool(rule.enabled), ]); } println!("{}", table); } } } Ok(()) } async fn handle_show( profile: &Option, rule_ref: String, api_url: &Option, output_format: OutputFormat, ) -> Result<()> { let config = CliConfig::load_with_profile(profile.as_deref())?; let mut client = ApiClient::from_config(&config, api_url); let path = format!("/rules/{}", rule_ref); let rule: RuleDetail = client.get(&path).await?; match output_format { OutputFormat::Json | OutputFormat::Yaml => { output::print_output(&rule, output_format)?; } OutputFormat::Table => { output::print_section(&format!("Rule: {}", rule.rule_ref)); output::print_key_value_table(vec![ ("ID", rule.id.to_string()), ("Ref", rule.rule_ref.clone()), ("Pack", rule.pack_ref.clone()), ("Label", rule.label.clone()), ("Description", rule.description.clone()), ("Trigger", rule.trigger_ref.clone()), ("Action", rule.action_ref.clone()), ("Enabled", output::format_bool(rule.enabled)), ("Created", output::format_timestamp(&rule.created)), ("Updated", output::format_timestamp(&rule.updated)), ]); if let Some(conditions) = rule.conditions { if !conditions.is_null() { output::print_section("Conditions"); println!("{}", serde_json::to_string_pretty(&conditions)?); } } if let Some(action_params) = rule.action_params { if !action_params.is_null() { output::print_section("Action Parameters"); println!("{}", serde_json::to_string_pretty(&action_params)?); } } if let Some(trigger_params) = rule.trigger_params { if !trigger_params.is_null() { output::print_section("Trigger Parameters"); println!("{}", serde_json::to_string_pretty(&trigger_params)?); } } } } Ok(()) } async fn handle_update( profile: &Option, rule_ref: String, label: Option, description: Option, conditions: Option, action_params: Option, trigger_params: Option, enabled: Option, api_url: &Option, output_format: OutputFormat, ) -> Result<()> { let config = CliConfig::load_with_profile(profile.as_deref())?; let mut client = ApiClient::from_config(&config, api_url); // Check that at least one field is provided if label.is_none() && description.is_none() && conditions.is_none() && action_params.is_none() && trigger_params.is_none() && enabled.is_none() { anyhow::bail!("At least one field must be provided to update"); } // Parse JSON fields let conditions_json = if let Some(cond) = conditions { Some(serde_json::from_str(&cond)?) } else { None }; let action_params_json = if let Some(params) = action_params { Some(serde_json::from_str(¶ms)?) } else { None }; let trigger_params_json = if let Some(params) = trigger_params { Some(serde_json::from_str(¶ms)?) } else { None }; #[derive(Serialize)] struct UpdateRuleRequestCli { #[serde(skip_serializing_if = "Option::is_none")] label: Option, #[serde(skip_serializing_if = "Option::is_none")] description: Option, #[serde(skip_serializing_if = "Option::is_none")] conditions: Option, #[serde(skip_serializing_if = "Option::is_none")] action_params: Option, #[serde(skip_serializing_if = "Option::is_none")] trigger_params: Option, #[serde(skip_serializing_if = "Option::is_none")] enabled: Option, } let request = UpdateRuleRequestCli { label, description, conditions: conditions_json, action_params: action_params_json, trigger_params: trigger_params_json, enabled, }; let path = format!("/rules/{}", rule_ref); let rule: RuleDetail = client.put(&path, &request).await?; match output_format { OutputFormat::Json | OutputFormat::Yaml => { output::print_output(&rule, output_format)?; } OutputFormat::Table => { output::print_success(&format!("Rule '{}' updated successfully", rule.rule_ref)); output::print_key_value_table(vec![ ("ID", rule.id.to_string()), ("Ref", rule.rule_ref.clone()), ("Pack", rule.pack_ref.clone()), ("Label", rule.label.clone()), ("Description", rule.description.clone()), ("Trigger", rule.trigger_ref.clone()), ("Action", rule.action_ref.clone()), ("Enabled", output::format_bool(rule.enabled)), ("Updated", output::format_timestamp(&rule.updated)), ]); } } Ok(()) } async fn handle_toggle( profile: &Option, rule_ref: String, enabled: bool, api_url: &Option, output_format: OutputFormat, ) -> Result<()> { let config = CliConfig::load_with_profile(profile.as_deref())?; let mut client = ApiClient::from_config(&config, api_url); let request = UpdateRuleRequest { enabled }; let path = format!("/rules/{}", rule_ref); let rule: Rule = client.patch(&path, &request).await?; match output_format { OutputFormat::Json | OutputFormat::Yaml => { output::print_output(&rule, output_format)?; } OutputFormat::Table => { let action = if enabled { "enabled" } else { "disabled" }; output::print_success(&format!("Rule '{}' {}", rule.rule_ref, action)); } } Ok(()) } async fn handle_create( profile: &Option, name: String, pack: String, trigger: String, action: String, description: Option, criteria: Option, enabled: bool, api_url: &Option, output_format: OutputFormat, ) -> Result<()> { let config = CliConfig::load_with_profile(profile.as_deref())?; let mut client = ApiClient::from_config(&config, api_url); let criteria_value = if let Some(criteria_str) = criteria { Some(serde_json::from_str(&criteria_str)?) } else { None }; let request = CreateRuleRequest { name: name.clone(), pack_id: pack, trigger_id: trigger, action_id: action, description, criteria: criteria_value, enabled, }; let rule: Rule = client.post("/rules", &request).await?; match output_format { OutputFormat::Json | OutputFormat::Yaml => { output::print_output(&rule, output_format)?; } OutputFormat::Table => { output::print_success(&format!("Rule '{}' created successfully", rule.rule_ref)); output::print_info(&format!("ID: {}", rule.id)); output::print_info(&format!("Enabled: {}", rule.enabled)); } } Ok(()) } async fn handle_delete( profile: &Option, rule_ref: String, yes: bool, api_url: &Option, output_format: OutputFormat, ) -> Result<()> { let config = CliConfig::load_with_profile(profile.as_deref())?; let mut client = ApiClient::from_config(&config, api_url); // Confirm deletion unless --yes is provided if !yes && matches!(output_format, OutputFormat::Table) { let confirm = dialoguer::Confirm::new() .with_prompt(format!( "Are you sure you want to delete rule '{}'?", rule_ref )) .default(false) .interact()?; if !confirm { output::print_info("Deletion cancelled"); return Ok(()); } } let path = format!("/rules/{}", rule_ref); client.delete_no_response(&path).await?; match output_format { OutputFormat::Json | OutputFormat::Yaml => { let msg = serde_json::json!({"message": "Rule deleted successfully"}); output::print_output(&msg, output_format)?; } OutputFormat::Table => { output::print_success(&format!("Rule '{}' deleted successfully", rule_ref)); } } Ok(()) }