From 1bd28b96a6f9510f06b5cfa21ba1371c0b6aa19d Mon Sep 17 00:00:00 2001 From: Thorsten Hans Date: Tue, 5 Nov 2024 15:16:19 +0100 Subject: [PATCH 1/5] chore: Add `--format` flag to `cloud apps list` and `cloud apps info` Signed-off-by: Thorsten Hans --- src/commands/apps.rs | 63 ++++++++++++------------ src/commands/apps_output.rs | 95 +++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + 3 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 src/commands/apps_output.rs diff --git a/src/commands/apps.rs b/src/commands/apps.rs index 622aa7e..d0459d7 100644 --- a/src/commands/apps.rs +++ b/src/commands/apps.rs @@ -1,8 +1,10 @@ -use crate::commands::{client_and_app_id, create_cloud_client, CommonArgs}; +use crate::commands::{apps_output::AppInfo, client_and_app_id, create_cloud_client, CommonArgs}; use anyhow::{Context, Result}; use clap::Parser; use cloud::{CloudClientInterface, DEFAULT_APPLIST_PAGE_SIZE}; -use cloud_openapi::models::{AppItem, AppItemPage, ValidationStatus}; +use cloud_openapi::models::{AppItem, ValidationStatus}; + +use super::apps_output::{print_app_info, print_app_list, OutputFormat}; #[derive(Parser, Debug)] #[clap(about = "Manage applications deployed to Fermyon Cloud")] @@ -19,6 +21,9 @@ pub enum AppsCommand { pub struct ListCommand { #[clap(flatten)] common: CommonArgs, + /// Desired output format + #[clap(value_enum, long = "format")] + format: Option, } #[derive(Parser, Debug)] @@ -35,6 +40,9 @@ pub struct InfoCommand { pub app: String, #[clap(flatten)] common: CommonArgs, + /// Desired output format + #[clap(value_enum, long = "format")] + format: Option, } impl AppsCommand { @@ -51,19 +59,21 @@ impl ListCommand { pub async fn run(self) -> Result<()> { let client = create_cloud_client(self.common.deployment_env_id.as_deref()).await?; let mut app_list_page = client.list_apps(DEFAULT_APPLIST_PAGE_SIZE, None).await?; - if app_list_page.total_items <= 0 { - eprintln!("No applications found"); - } else { - print_app_list(&app_list_page); - let mut page_index = 1; - while !app_list_page.is_last_page { - app_list_page = client - .list_apps(DEFAULT_APPLIST_PAGE_SIZE, Some(page_index)) - .await?; - print_app_list(&app_list_page); - page_index += 1; + let mut apps: Vec = vec![]; + let mut page_index = 1; + for app in app_list_page.items { + apps.push(app.name.clone()); + } + while !app_list_page.is_last_page { + app_list_page = client + .list_apps(DEFAULT_APPLIST_PAGE_SIZE, Some(page_index)) + .await?; + for app in app_list_page.items { + apps.push(app.name.clone()); } + page_index += 1; } + print_app_list(apps, self.format); Ok(()) } } @@ -92,13 +102,14 @@ impl InfoCommand { let (current_domain, in_progress_domain) = domains_current_and_in_progress(&app); - println!("Name: {}", &app.name); - print_if_present("Description: ", app.description.as_ref()); - print_if_present("URL: https://", current_domain); - if let Some(domain) = in_progress_domain { - println!("Validation for {} is in progress", domain); - }; + let info = AppInfo::new( + app.name.clone(), + app.description.clone(), + current_domain.cloned(), + in_progress_domain.is_none(), + ); + print_app_info(info, self.format); Ok(()) } } @@ -116,17 +127,3 @@ fn domains_current_and_in_progress(app: &AppItem) -> (Option<&String>, Option<&S None => (Some(auto_domain), None), } } - -fn print_if_present(prefix: &str, value: Option<&String>) { - if let Some(val) = value { - if !val.is_empty() { - println!("{prefix}{val}"); - } - } -} - -fn print_app_list(page: &AppItemPage) { - for app in &page.items { - println!("{}", app.name); - } -} diff --git a/src/commands/apps_output.rs b/src/commands/apps_output.rs new file mode 100644 index 0000000..55c0d21 --- /dev/null +++ b/src/commands/apps_output.rs @@ -0,0 +1,95 @@ +use std::fmt::Display; + +use clap::ValueEnum; +use serde::Serialize; + +#[derive(Debug, ValueEnum, PartialEq, Clone)] +pub(crate) enum OutputFormat { + Plain, + Json, +} + +#[derive(Serialize)] +pub(crate) struct AppInfo { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + url: Option, + #[serde(rename = "domainInfo")] + domain_info: DomainInfo, +} + +#[derive(Serialize)] +pub(crate) struct DomainInfo { + domain: Option, + #[serde(rename = "validationFinished")] + validation_finished: bool, +} + +impl AppInfo { + pub(crate) fn new( + name: String, + description: Option, + domain: Option, + domain_validation_finished: bool, + ) -> Self { + let url = match &domain { + Some(d) => Some(format!("https://{}", d)), + None => None, + }; + let desc: Option = match description { + Some(d) => match d.is_empty() { + true => None, + false => Some(d), + }, + None => None, + }; + Self { + name, + description: desc, + url, + domain_info: DomainInfo { + domain, + validation_finished: domain_validation_finished, + }, + } + } +} + +impl Display for AppInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Name: {}", self.name)?; + if self + .description + .as_ref() + .is_some_and(|desc| !desc.is_empty()) + { + writeln!(f, "Description: {}", self.description.clone().unwrap())?; + } + if self.domain_info.domain.is_some() { + let domain = self.domain_info.domain.clone().unwrap(); + writeln!(f, "URL: https://{}", domain)?; + if !self.domain_info.validation_finished { + writeln!(f, "Validation for {} is in progress", domain)?; + }; + } + Ok(()) + } +} + +pub(crate) fn print_app_list(apps: Vec, format: Option) { + let info = match format { + Some(OutputFormat::Json) => serde_json::to_string_pretty(&apps).unwrap(), + _ => apps.join("\n"), + }; + println!("{}", info); +} + +pub(crate) fn print_app_info(app: AppInfo, format: Option) { + match format { + Some(OutputFormat::Json) => { + print!("{}\n", serde_json::to_string_pretty(&app).unwrap()) + } + _ => print!("{}", app), + }; +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 29be8f3..b8f7590 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod apps; +pub mod apps_output; pub mod deploy; pub mod key_value; pub mod link; From 0d9a4a8c146a1f6730c241ff4479631743dfbb95 Mon Sep 17 00:00:00 2001 From: Thorsten Hans Date: Tue, 5 Nov 2024 15:28:01 +0100 Subject: [PATCH 2/5] chore: make clippy happy Signed-off-by: Thorsten Hans --- src/commands/apps_output.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/commands/apps_output.rs b/src/commands/apps_output.rs index 55c0d21..de9d700 100644 --- a/src/commands/apps_output.rs +++ b/src/commands/apps_output.rs @@ -33,10 +33,7 @@ impl AppInfo { domain: Option, domain_validation_finished: bool, ) -> Self { - let url = match &domain { - Some(d) => Some(format!("https://{}", d)), - None => None, - }; + let url = domain.as_ref().map(|d| format!("https://{}", d)); let desc: Option = match description { Some(d) => match d.is_empty() { true => None, @@ -88,7 +85,7 @@ pub(crate) fn print_app_list(apps: Vec, format: Option) { pub(crate) fn print_app_info(app: AppInfo, format: Option) { match format { Some(OutputFormat::Json) => { - print!("{}\n", serde_json::to_string_pretty(&app).unwrap()) + println!("{}", serde_json::to_string_pretty(&app).unwrap()) } _ => print!("{}", app), }; From cd1397037515b722576bd60a205eb24a2d2c0e12 Mon Sep 17 00:00:00 2001 From: Thorsten Hans Date: Wed, 6 Nov 2024 09:28:26 +0100 Subject: [PATCH 3/5] chore: refactor to use explict matches and unify how both info and app lists are printed Signed-off-by: Thorsten Hans --- src/commands/apps.rs | 8 ++++---- src/commands/apps_output.rs | 21 +++++++++------------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/commands/apps.rs b/src/commands/apps.rs index d0459d7..fe9ab01 100644 --- a/src/commands/apps.rs +++ b/src/commands/apps.rs @@ -22,8 +22,8 @@ pub struct ListCommand { #[clap(flatten)] common: CommonArgs, /// Desired output format - #[clap(value_enum, long = "format")] - format: Option, + #[clap(value_enum, long = "format", default_value = "plain")] + format: OutputFormat, } #[derive(Parser, Debug)] @@ -41,8 +41,8 @@ pub struct InfoCommand { #[clap(flatten)] common: CommonArgs, /// Desired output format - #[clap(value_enum, long = "format")] - format: Option, + #[clap(value_enum, long = "format", default_value = "plain")] + format: OutputFormat, } impl AppsCommand { diff --git a/src/commands/apps_output.rs b/src/commands/apps_output.rs index de9d700..9332257 100644 --- a/src/commands/apps_output.rs +++ b/src/commands/apps_output.rs @@ -74,19 +74,16 @@ impl Display for AppInfo { } } -pub(crate) fn print_app_list(apps: Vec, format: Option) { - let info = match format { - Some(OutputFormat::Json) => serde_json::to_string_pretty(&apps).unwrap(), - _ => apps.join("\n"), - }; - println!("{}", info); +pub(crate) fn print_app_list(apps: Vec, format: OutputFormat) { + match format { + OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&apps).unwrap()), + OutputFormat::Plain => println!("{}", apps.join("\n")), + } } -pub(crate) fn print_app_info(app: AppInfo, format: Option) { +pub(crate) fn print_app_info(app: AppInfo, format: OutputFormat) { match format { - Some(OutputFormat::Json) => { - println!("{}", serde_json::to_string_pretty(&app).unwrap()) - } - _ => print!("{}", app), - }; + OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&app).unwrap()), + OutputFormat::Plain => print!("{}", app), + } } From 6082a0f93f6bac25956becc76d2af220d312883b Mon Sep 17 00:00:00 2001 From: Thorsten Hans Date: Wed, 6 Nov 2024 09:34:25 +0100 Subject: [PATCH 4/5] chore: the return of the "No Applications found" message Signed-off-by: Thorsten Hans --- src/commands/apps_output.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/apps_output.rs b/src/commands/apps_output.rs index 9332257..69f92c8 100644 --- a/src/commands/apps_output.rs +++ b/src/commands/apps_output.rs @@ -77,7 +77,13 @@ impl Display for AppInfo { pub(crate) fn print_app_list(apps: Vec, format: OutputFormat) { match format { OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&apps).unwrap()), - OutputFormat::Plain => println!("{}", apps.join("\n")), + OutputFormat::Plain => { + if apps.is_empty() { + eprintln!("No applications found"); + return; + } + println!("{}", apps.join("\n")) + } } } From 3a3b972cac7caa9d76eec1bd03829b1e326ee3f5 Mon Sep 17 00:00:00 2001 From: Thorsten Hans Date: Wed, 6 Nov 2024 09:48:08 +0100 Subject: [PATCH 5/5] chore: streamlined AppInfo struct and applied review feedback Signed-off-by: Thorsten Hans --- src/commands/apps_output.rs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/commands/apps_output.rs b/src/commands/apps_output.rs index 69f92c8..a61eaf6 100644 --- a/src/commands/apps_output.rs +++ b/src/commands/apps_output.rs @@ -12,8 +12,7 @@ pub(crate) enum OutputFormat { #[derive(Serialize)] pub(crate) struct AppInfo { name: String, - #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: String, url: Option, #[serde(rename = "domainInfo")] domain_info: DomainInfo, @@ -34,16 +33,9 @@ impl AppInfo { domain_validation_finished: bool, ) -> Self { let url = domain.as_ref().map(|d| format!("https://{}", d)); - let desc: Option = match description { - Some(d) => match d.is_empty() { - true => None, - false => Some(d), - }, - None => None, - }; Self { name, - description: desc, + description: description.unwrap_or_default(), url, domain_info: DomainInfo { domain, @@ -56,15 +48,10 @@ impl AppInfo { impl Display for AppInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "Name: {}", self.name)?; - if self - .description - .as_ref() - .is_some_and(|desc| !desc.is_empty()) - { - writeln!(f, "Description: {}", self.description.clone().unwrap())?; + if !self.description.is_empty() { + writeln!(f, "Description: {}", self.description)?; } - if self.domain_info.domain.is_some() { - let domain = self.domain_info.domain.clone().unwrap(); + if let Some(domain) = self.domain_info.domain.as_ref() { writeln!(f, "URL: https://{}", domain)?; if !self.domain_info.validation_finished { writeln!(f, "Validation for {} is in progress", domain)?;