Skip to content

Commit 7afe44f

Browse files
authored
Merge pull request #25 from godon-dev/feature_targets
feat: add target subcommands (list, create, show, delete)
2 parents 0a99550 + 061b4f8 commit 7afe44f

3 files changed

Lines changed: 318 additions & 2 deletions

File tree

src/client.rs

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{ApiConfig, ApiResponse, Breeder, BreederCreateRequest, BreederSummary, BreederUpdateRequest, Credential};
1+
use crate::{ApiConfig, ApiResponse, Breeder, BreederCreateRequest, BreederSummary, BreederUpdateRequest, Credential, Target};
22
use anyhow::{Context, Result};
33
use reqwest::Client;
44
use std::time::Duration;
@@ -387,4 +387,171 @@ impl GodonClient {
387387

388388
self.create_credential(credential_data).await
389389
}
390+
391+
pub async fn list_targets(&self) -> ApiResponse<Vec<Target>> {
392+
let url = format!("{}/targets", self.base_url());
393+
394+
match self.client.get(&url).send().await {
395+
Ok(response) => {
396+
let status = response.status();
397+
398+
if status.is_success() {
399+
match response.text().await {
400+
Ok(body) => {
401+
let json: serde_json::Value = match serde_json::from_str(&body) {
402+
Ok(j) => j,
403+
Err(e) => return ApiResponse::error(format!("JSON parse error: {}", e)),
404+
};
405+
406+
let targets: Vec<Target> = if json.is_array() {
407+
match serde_json::from_value(json) {
408+
Ok(t) => t,
409+
Err(e) => return ApiResponse::error(format!("Parse error: {}", e)),
410+
}
411+
} else if let Some(arr) = json.get("targets") {
412+
match serde_json::from_value(arr.clone()) {
413+
Ok(t) => t,
414+
Err(e) => return ApiResponse::error(format!("Parse error: {}", e)),
415+
}
416+
} else {
417+
return ApiResponse::error("Unexpected response format");
418+
};
419+
420+
ApiResponse::success(targets)
421+
}
422+
Err(e) => ApiResponse::error(e.to_string()),
423+
}
424+
} else {
425+
ApiResponse::error(format!("HTTP Error: {}", status))
426+
}
427+
}
428+
Err(e) => ApiResponse::error(e.to_string()),
429+
}
430+
}
431+
432+
pub async fn create_target(&self, target_data: serde_json::Value) -> ApiResponse<Target> {
433+
let url = format!("{}/targets", self.base_url());
434+
435+
match self.client
436+
.post(&url)
437+
.json(&target_data)
438+
.send()
439+
.await
440+
{
441+
Ok(response) => {
442+
let status = response.status();
443+
444+
if status.is_success() {
445+
match response.text().await {
446+
Ok(body) => {
447+
match serde_json::from_str::<Target>(&body) {
448+
Ok(target) => ApiResponse::success(target),
449+
Err(e) => ApiResponse::error(format!("Parse error: {}", e)),
450+
}
451+
}
452+
Err(e) => ApiResponse::error(e.to_string()),
453+
}
454+
} else {
455+
ApiResponse::error(format!("HTTP Error: {}", status))
456+
}
457+
}
458+
Err(e) => ApiResponse::error(e.to_string()),
459+
}
460+
}
461+
462+
pub async fn get_target(&self, target_id: &str) -> ApiResponse<Target> {
463+
let url = format!("{}/targets/{}", self.base_url(), urlencoding::encode(target_id));
464+
465+
match self.client.get(&url).send().await {
466+
Ok(response) => {
467+
let status = response.status();
468+
469+
if status.is_success() {
470+
match response.text().await {
471+
Ok(body) => {
472+
match serde_json::from_str::<Target>(&body) {
473+
Ok(target) => ApiResponse::success(target),
474+
Err(e) => ApiResponse::error(format!("Parse error: {}", e)),
475+
}
476+
}
477+
Err(e) => ApiResponse::error(e.to_string()),
478+
}
479+
} else {
480+
ApiResponse::error(format!("HTTP Error: {}", status))
481+
}
482+
}
483+
Err(e) => ApiResponse::error(e.to_string()),
484+
}
485+
}
486+
487+
pub async fn delete_target(&self, target_id: &str) -> ApiResponse<serde_json::Value> {
488+
let url = format!("{}/targets/{}", self.base_url(), urlencoding::encode(target_id));
489+
490+
match self.client.delete(&url).send().await {
491+
Ok(response) => {
492+
let status = response.status();
493+
494+
if status.is_success() {
495+
match response.text().await {
496+
Ok(body) => {
497+
match serde_json::from_str(&body) {
498+
Ok(v) => ApiResponse::success(v),
499+
Err(e) => ApiResponse::error(format!("Parse error: {}", e)),
500+
}
501+
}
502+
Err(e) => ApiResponse::error(e.to_string()),
503+
}
504+
} else {
505+
ApiResponse::error(format!("HTTP Error: {}", status))
506+
}
507+
}
508+
Err(e) => ApiResponse::error(e.to_string()),
509+
}
510+
}
511+
512+
pub async fn create_target_from_yaml(&self, yaml_content: &str) -> ApiResponse<Target> {
513+
let yaml_data: std::collections::HashMap<String, serde_yaml::Value> = match serde_yaml::from_str(yaml_content) {
514+
Ok(d) => d,
515+
Err(e) => return ApiResponse::error(format!("YAML parse error: {}", e)),
516+
};
517+
518+
let name = match yaml_data.get("name").and_then(|v| v.as_str()) {
519+
Some(n) => n.to_string(),
520+
None => return ApiResponse::error("Missing required field: name"),
521+
};
522+
523+
let target_type = match yaml_data.get("targetType").and_then(|v| v.as_str()) {
524+
Some(t) => t.to_string(),
525+
None => return ApiResponse::error("Missing required field: targetType"),
526+
};
527+
528+
let address = match yaml_data.get("address").and_then(|v| v.as_str()) {
529+
Some(a) => a.to_string(),
530+
None => return ApiResponse::error("Missing required field: address"),
531+
};
532+
533+
let mut target_data = serde_json::json!({
534+
"name": name,
535+
"targetType": target_type,
536+
"address": address
537+
});
538+
539+
if let Some(v) = yaml_data.get("username").and_then(|v| v.as_str()) {
540+
target_data["username"] = serde_json::Value::String(v.to_string());
541+
}
542+
if let Some(v) = yaml_data.get("credentialId").and_then(|v| v.as_str()) {
543+
target_data["credentialId"] = serde_json::Value::String(v.to_string());
544+
}
545+
if let Some(v) = yaml_data.get("credentialName").and_then(|v| v.as_str()) {
546+
target_data["credentialName"] = serde_json::Value::String(v.to_string());
547+
}
548+
if let Some(v) = yaml_data.get("description").and_then(|v| v.as_str()) {
549+
target_data["description"] = serde_json::Value::String(v.to_string());
550+
}
551+
if let Some(v) = yaml_data.get("allowsDowntime").and_then(|v| v.as_bool()) {
552+
target_data["allowsDowntime"] = serde_json::Value::Bool(v);
553+
}
554+
555+
self.create_target(target_data).await
556+
}
390557
}

src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ pub struct Credential {
5353
pub content: Option<String>,
5454
}
5555

56+
#[derive(Debug, Clone, Serialize, Deserialize)]
57+
pub struct Target {
58+
pub id: String,
59+
pub name: String,
60+
#[serde(rename = "targetType")]
61+
pub target_type: String,
62+
pub address: String,
63+
pub username: Option<String>,
64+
#[serde(rename = "credentialId")]
65+
pub credential_id: Option<String>,
66+
#[serde(rename = "credentialName")]
67+
pub credential_name: Option<String>,
68+
pub description: Option<String>,
69+
#[serde(rename = "allowsDowntime")]
70+
pub allows_downtime: Option<bool>,
71+
#[serde(rename = "createdAt")]
72+
pub created_at: Option<String>,
73+
#[serde(rename = "lastUsedAt")]
74+
pub last_used_at: Option<String>,
75+
}
76+
5677
#[derive(Debug, Clone)]
5778
pub struct ApiConfig {
5879
pub hostname: String,

src/main.rs

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clap::{Parser, Subcommand};
2-
use godon_cli::{Breeder, BreederSummary, Credential, GodonClient};
2+
use godon_cli::{Breeder, BreederSummary, Credential, GodonClient, Target};
33
use std::path::PathBuf;
44

55
#[derive(Parser)]
@@ -46,6 +46,10 @@ enum Commands {
4646
#[command(subcommand)]
4747
subcommand: CredentialCommands,
4848
},
49+
Target {
50+
#[command(subcommand)]
51+
subcommand: TargetCommands,
52+
},
4953
}
5054

5155
#[derive(Subcommand)]
@@ -107,6 +111,26 @@ enum CredentialCommands {
107111
},
108112
}
109113

114+
#[derive(Subcommand)]
115+
enum TargetCommands {
116+
List,
117+
118+
Create {
119+
#[arg(long)]
120+
file: PathBuf,
121+
},
122+
123+
Show {
124+
#[arg(long)]
125+
id: String,
126+
},
127+
128+
Delete {
129+
#[arg(long)]
130+
id: String,
131+
},
132+
}
133+
110134
fn write_error(message: &str) -> ! {
111135
eprintln!("Error: {}", message);
112136
std::process::exit(1);
@@ -124,6 +148,73 @@ fn format_output<T: serde::Serialize>(data: &T, format: &OutputFormat) {
124148
}
125149
}
126150

151+
async fn handle_target_command(client: &GodonClient, cmd: TargetCommands, output: &OutputFormat) {
152+
match cmd {
153+
TargetCommands::List => {
154+
let response = client.list_targets().await;
155+
if response.success {
156+
if let Some(targets) = response.data {
157+
if matches!(output, OutputFormat::Text) {
158+
format_target_list(&targets);
159+
} else {
160+
format_output(&targets, output);
161+
}
162+
}
163+
} else {
164+
write_error(response.error.as_deref().unwrap_or("Unknown error"));
165+
}
166+
}
167+
168+
TargetCommands::Create { file } => {
169+
let content = match std::fs::read_to_string(&file) {
170+
Ok(c) => c,
171+
Err(e) => write_error(&format!("Failed to read file: {}", e)),
172+
};
173+
174+
let response = client.create_target_from_yaml(&content).await;
175+
if response.success {
176+
if let Some(target) = response.data {
177+
if matches!(output, OutputFormat::Text) {
178+
format_target_created(&target);
179+
} else {
180+
format_output(&target, output);
181+
}
182+
}
183+
} else {
184+
write_error(response.error.as_deref().unwrap_or("Unknown error"));
185+
}
186+
}
187+
188+
TargetCommands::Show { id } => {
189+
let response = client.get_target(&id).await;
190+
if response.success {
191+
if let Some(target) = response.data {
192+
if matches!(output, OutputFormat::Text) {
193+
format_target(&target);
194+
} else {
195+
format_output(&target, output);
196+
}
197+
}
198+
} else {
199+
write_error(response.error.as_deref().unwrap_or("Unknown error"));
200+
}
201+
}
202+
203+
TargetCommands::Delete { id } => {
204+
let response = client.delete_target(&id).await;
205+
if response.success {
206+
if matches!(output, OutputFormat::Text) {
207+
println!("Target deleted successfully: {}", id);
208+
} else if let Some(data) = response.data {
209+
format_output(&data, output);
210+
}
211+
} else {
212+
write_error(response.error.as_deref().unwrap_or("Unknown error"));
213+
}
214+
}
215+
}
216+
}
217+
127218
fn format_breeder_list(breeders: &[BreederSummary]) {
128219
println!("Breeders:");
129220
for breeder in breeders {
@@ -185,6 +276,42 @@ fn format_credential_created(credential: &Credential) {
185276
println!(" windmillVariable: {}", credential.windmill_variable);
186277
}
187278

279+
fn format_target_list(targets: &[Target]) {
280+
println!("Targets:");
281+
for target in targets {
282+
println!(" ID: {}", target.id);
283+
println!(" Name: {}", target.name);
284+
println!(" Type: {}", target.target_type);
285+
println!(" Address: {}", target.address);
286+
println!(" Description: {}", target.description.as_deref().unwrap_or(""));
287+
println!(" Created: {}", target.created_at.as_deref().unwrap_or(""));
288+
println!(" ---");
289+
}
290+
}
291+
292+
fn format_target(target: &Target) {
293+
println!("Target Details:");
294+
println!(" ID: {}", target.id);
295+
println!(" Name: {}", target.name);
296+
println!(" Type: {}", target.target_type);
297+
println!(" Address: {}", target.address);
298+
println!(" Username: {}", target.username.as_deref().unwrap_or(""));
299+
println!(" Credential ID: {}", target.credential_id.as_deref().unwrap_or(""));
300+
println!(" Credential Name: {}", target.credential_name.as_deref().unwrap_or(""));
301+
println!(" Description: {}", target.description.as_deref().unwrap_or(""));
302+
println!(" Allows Downtime: {}", target.allows_downtime.map_or("N/A".to_string(), |v| v.to_string()));
303+
println!(" Created: {}", target.created_at.as_deref().unwrap_or(""));
304+
println!(" Last Used: {}", target.last_used_at.as_deref().unwrap_or(""));
305+
}
306+
307+
fn format_target_created(target: &Target) {
308+
println!("Target created successfully:");
309+
println!(" ID: {}", target.id);
310+
println!(" Name: {}", target.name);
311+
println!(" Type: {}", target.target_type);
312+
println!(" Address: {}", target.address);
313+
}
314+
188315
#[tokio::main]
189316
async fn main() {
190317
let cli = Cli::parse();
@@ -203,6 +330,7 @@ async fn main() {
203330
match cli.command {
204331
Commands::Breeder { subcommand } => handle_breeder_command(&client, subcommand, &cli.output).await,
205332
Commands::Credential { subcommand } => handle_credential_command(&client, subcommand, &cli.output).await,
333+
Commands::Target { subcommand } => handle_target_command(&client, subcommand, &cli.output).await,
206334
}
207335
}
208336

0 commit comments

Comments
 (0)