diff --git a/src/client.rs b/src/client.rs index eae552e..e5d003b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,4 @@ -use crate::{ApiConfig, ApiResponse, Breeder, BreederCreateRequest, BreederSummary, BreederUpdateRequest, Credential}; +use crate::{ApiConfig, ApiResponse, Breeder, BreederCreateRequest, BreederSummary, BreederUpdateRequest, Credential, Target}; use anyhow::{Context, Result}; use reqwest::Client; use std::time::Duration; @@ -387,4 +387,171 @@ impl GodonClient { self.create_credential(credential_data).await } + + pub async fn list_targets(&self) -> ApiResponse> { + let url = format!("{}/targets", self.base_url()); + + match self.client.get(&url).send().await { + Ok(response) => { + let status = response.status(); + + if status.is_success() { + match response.text().await { + Ok(body) => { + let json: serde_json::Value = match serde_json::from_str(&body) { + Ok(j) => j, + Err(e) => return ApiResponse::error(format!("JSON parse error: {}", e)), + }; + + let targets: Vec = if json.is_array() { + match serde_json::from_value(json) { + Ok(t) => t, + Err(e) => return ApiResponse::error(format!("Parse error: {}", e)), + } + } else if let Some(arr) = json.get("targets") { + match serde_json::from_value(arr.clone()) { + Ok(t) => t, + Err(e) => return ApiResponse::error(format!("Parse error: {}", e)), + } + } else { + return ApiResponse::error("Unexpected response format"); + }; + + ApiResponse::success(targets) + } + Err(e) => ApiResponse::error(e.to_string()), + } + } else { + ApiResponse::error(format!("HTTP Error: {}", status)) + } + } + Err(e) => ApiResponse::error(e.to_string()), + } + } + + pub async fn create_target(&self, target_data: serde_json::Value) -> ApiResponse { + let url = format!("{}/targets", self.base_url()); + + match self.client + .post(&url) + .json(&target_data) + .send() + .await + { + Ok(response) => { + let status = response.status(); + + if status.is_success() { + match response.text().await { + Ok(body) => { + match serde_json::from_str::(&body) { + Ok(target) => ApiResponse::success(target), + Err(e) => ApiResponse::error(format!("Parse error: {}", e)), + } + } + Err(e) => ApiResponse::error(e.to_string()), + } + } else { + ApiResponse::error(format!("HTTP Error: {}", status)) + } + } + Err(e) => ApiResponse::error(e.to_string()), + } + } + + pub async fn get_target(&self, target_id: &str) -> ApiResponse { + let url = format!("{}/targets/{}", self.base_url(), urlencoding::encode(target_id)); + + match self.client.get(&url).send().await { + Ok(response) => { + let status = response.status(); + + if status.is_success() { + match response.text().await { + Ok(body) => { + match serde_json::from_str::(&body) { + Ok(target) => ApiResponse::success(target), + Err(e) => ApiResponse::error(format!("Parse error: {}", e)), + } + } + Err(e) => ApiResponse::error(e.to_string()), + } + } else { + ApiResponse::error(format!("HTTP Error: {}", status)) + } + } + Err(e) => ApiResponse::error(e.to_string()), + } + } + + pub async fn delete_target(&self, target_id: &str) -> ApiResponse { + let url = format!("{}/targets/{}", self.base_url(), urlencoding::encode(target_id)); + + match self.client.delete(&url).send().await { + Ok(response) => { + let status = response.status(); + + if status.is_success() { + match response.text().await { + Ok(body) => { + match serde_json::from_str(&body) { + Ok(v) => ApiResponse::success(v), + Err(e) => ApiResponse::error(format!("Parse error: {}", e)), + } + } + Err(e) => ApiResponse::error(e.to_string()), + } + } else { + ApiResponse::error(format!("HTTP Error: {}", status)) + } + } + Err(e) => ApiResponse::error(e.to_string()), + } + } + + pub async fn create_target_from_yaml(&self, yaml_content: &str) -> ApiResponse { + let yaml_data: std::collections::HashMap = match serde_yaml::from_str(yaml_content) { + Ok(d) => d, + Err(e) => return ApiResponse::error(format!("YAML parse error: {}", e)), + }; + + let name = match yaml_data.get("name").and_then(|v| v.as_str()) { + Some(n) => n.to_string(), + None => return ApiResponse::error("Missing required field: name"), + }; + + let target_type = match yaml_data.get("targetType").and_then(|v| v.as_str()) { + Some(t) => t.to_string(), + None => return ApiResponse::error("Missing required field: targetType"), + }; + + let address = match yaml_data.get("address").and_then(|v| v.as_str()) { + Some(a) => a.to_string(), + None => return ApiResponse::error("Missing required field: address"), + }; + + let mut target_data = serde_json::json!({ + "name": name, + "targetType": target_type, + "address": address + }); + + if let Some(v) = yaml_data.get("username").and_then(|v| v.as_str()) { + target_data["username"] = serde_json::Value::String(v.to_string()); + } + if let Some(v) = yaml_data.get("credentialId").and_then(|v| v.as_str()) { + target_data["credentialId"] = serde_json::Value::String(v.to_string()); + } + if let Some(v) = yaml_data.get("credentialName").and_then(|v| v.as_str()) { + target_data["credentialName"] = serde_json::Value::String(v.to_string()); + } + if let Some(v) = yaml_data.get("description").and_then(|v| v.as_str()) { + target_data["description"] = serde_json::Value::String(v.to_string()); + } + if let Some(v) = yaml_data.get("allowsDowntime").and_then(|v| v.as_bool()) { + target_data["allowsDowntime"] = serde_json::Value::Bool(v); + } + + self.create_target(target_data).await + } } diff --git a/src/lib.rs b/src/lib.rs index a7c2100..56e0e4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,27 @@ pub struct Credential { pub content: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Target { + pub id: String, + pub name: String, + #[serde(rename = "targetType")] + pub target_type: String, + pub address: String, + pub username: Option, + #[serde(rename = "credentialId")] + pub credential_id: Option, + #[serde(rename = "credentialName")] + pub credential_name: Option, + pub description: Option, + #[serde(rename = "allowsDowntime")] + pub allows_downtime: Option, + #[serde(rename = "createdAt")] + pub created_at: Option, + #[serde(rename = "lastUsedAt")] + pub last_used_at: Option, +} + #[derive(Debug, Clone)] pub struct ApiConfig { pub hostname: String, diff --git a/src/main.rs b/src/main.rs index 0ac82bf..d599936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use clap::{Parser, Subcommand}; -use godon_cli::{Breeder, BreederSummary, Credential, GodonClient}; +use godon_cli::{Breeder, BreederSummary, Credential, GodonClient, Target}; use std::path::PathBuf; #[derive(Parser)] @@ -46,6 +46,10 @@ enum Commands { #[command(subcommand)] subcommand: CredentialCommands, }, + Target { + #[command(subcommand)] + subcommand: TargetCommands, + }, } #[derive(Subcommand)] @@ -107,6 +111,26 @@ enum CredentialCommands { }, } +#[derive(Subcommand)] +enum TargetCommands { + List, + + Create { + #[arg(long)] + file: PathBuf, + }, + + Show { + #[arg(long)] + id: String, + }, + + Delete { + #[arg(long)] + id: String, + }, +} + fn write_error(message: &str) -> ! { eprintln!("Error: {}", message); std::process::exit(1); @@ -124,6 +148,73 @@ fn format_output(data: &T, format: &OutputFormat) { } } +async fn handle_target_command(client: &GodonClient, cmd: TargetCommands, output: &OutputFormat) { + match cmd { + TargetCommands::List => { + let response = client.list_targets().await; + if response.success { + if let Some(targets) = response.data { + if matches!(output, OutputFormat::Text) { + format_target_list(&targets); + } else { + format_output(&targets, output); + } + } + } else { + write_error(response.error.as_deref().unwrap_or("Unknown error")); + } + } + + TargetCommands::Create { file } => { + let content = match std::fs::read_to_string(&file) { + Ok(c) => c, + Err(e) => write_error(&format!("Failed to read file: {}", e)), + }; + + let response = client.create_target_from_yaml(&content).await; + if response.success { + if let Some(target) = response.data { + if matches!(output, OutputFormat::Text) { + format_target_created(&target); + } else { + format_output(&target, output); + } + } + } else { + write_error(response.error.as_deref().unwrap_or("Unknown error")); + } + } + + TargetCommands::Show { id } => { + let response = client.get_target(&id).await; + if response.success { + if let Some(target) = response.data { + if matches!(output, OutputFormat::Text) { + format_target(&target); + } else { + format_output(&target, output); + } + } + } else { + write_error(response.error.as_deref().unwrap_or("Unknown error")); + } + } + + TargetCommands::Delete { id } => { + let response = client.delete_target(&id).await; + if response.success { + if matches!(output, OutputFormat::Text) { + println!("Target deleted successfully: {}", id); + } else if let Some(data) = response.data { + format_output(&data, output); + } + } else { + write_error(response.error.as_deref().unwrap_or("Unknown error")); + } + } + } +} + fn format_breeder_list(breeders: &[BreederSummary]) { println!("Breeders:"); for breeder in breeders { @@ -185,6 +276,42 @@ fn format_credential_created(credential: &Credential) { println!(" windmillVariable: {}", credential.windmill_variable); } +fn format_target_list(targets: &[Target]) { + println!("Targets:"); + for target in targets { + println!(" ID: {}", target.id); + println!(" Name: {}", target.name); + println!(" Type: {}", target.target_type); + println!(" Address: {}", target.address); + println!(" Description: {}", target.description.as_deref().unwrap_or("")); + println!(" Created: {}", target.created_at.as_deref().unwrap_or("")); + println!(" ---"); + } +} + +fn format_target(target: &Target) { + println!("Target Details:"); + println!(" ID: {}", target.id); + println!(" Name: {}", target.name); + println!(" Type: {}", target.target_type); + println!(" Address: {}", target.address); + println!(" Username: {}", target.username.as_deref().unwrap_or("")); + println!(" Credential ID: {}", target.credential_id.as_deref().unwrap_or("")); + println!(" Credential Name: {}", target.credential_name.as_deref().unwrap_or("")); + println!(" Description: {}", target.description.as_deref().unwrap_or("")); + println!(" Allows Downtime: {}", target.allows_downtime.map_or("N/A".to_string(), |v| v.to_string())); + println!(" Created: {}", target.created_at.as_deref().unwrap_or("")); + println!(" Last Used: {}", target.last_used_at.as_deref().unwrap_or("")); +} + +fn format_target_created(target: &Target) { + println!("Target created successfully:"); + println!(" ID: {}", target.id); + println!(" Name: {}", target.name); + println!(" Type: {}", target.target_type); + println!(" Address: {}", target.address); +} + #[tokio::main] async fn main() { let cli = Cli::parse(); @@ -203,6 +330,7 @@ async fn main() { match cli.command { Commands::Breeder { subcommand } => handle_breeder_command(&client, subcommand, &cli.output).await, Commands::Credential { subcommand } => handle_credential_command(&client, subcommand, &cli.output).await, + Commands::Target { subcommand } => handle_target_command(&client, subcommand, &cli.output).await, } }