Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 168 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -387,4 +387,171 @@ impl GodonClient {

self.create_credential(credential_data).await
}

pub async fn list_targets(&self) -> ApiResponse<Vec<Target>> {
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<Target> = 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<Target> {
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::<Target>(&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<Target> {
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::<Target>(&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<serde_json::Value> {
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<Target> {
let yaml_data: std::collections::HashMap<String, serde_yaml::Value> = 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
}
}
21 changes: 21 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ pub struct Credential {
pub content: Option<String>,
}

#[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<String>,
#[serde(rename = "credentialId")]
pub credential_id: Option<String>,
#[serde(rename = "credentialName")]
pub credential_name: Option<String>,
pub description: Option<String>,
#[serde(rename = "allowsDowntime")]
pub allows_downtime: Option<bool>,
#[serde(rename = "createdAt")]
pub created_at: Option<String>,
#[serde(rename = "lastUsedAt")]
pub last_used_at: Option<String>,
}

#[derive(Debug, Clone)]
pub struct ApiConfig {
pub hostname: String,
Expand Down
130 changes: 129 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -46,6 +46,10 @@ enum Commands {
#[command(subcommand)]
subcommand: CredentialCommands,
},
Target {
#[command(subcommand)]
subcommand: TargetCommands,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -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);
Expand All @@ -124,6 +148,73 @@ fn format_output<T: serde::Serialize>(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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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,
}
}

Expand Down
Loading