From 066550035e48ae462881c016b1cc2052ab909dc1 Mon Sep 17 00:00:00 2001 From: Nicolas Mohr Date: Tue, 30 Jan 2024 01:49:25 +0100 Subject: [PATCH 1/3] Bubble up result to main instead of process::exit in functions --- src/commands.rs | 23 ++++------- src/main.rs | 27 ++++++++---- src/modes.rs | 106 ++++++++++++++++++++++++------------------------ 3 files changed, 80 insertions(+), 76 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index ad3c482..a2670a8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -2,7 +2,6 @@ use crate::model::KubeConfig; use crate::config; use core::fmt; -use std::process; use std::{ io::Cursor, process::{Command, Stdio}, @@ -66,25 +65,21 @@ pub fn get_current_context() -> String { String::from_utf8(output.stdout).unwrap().trim().to_owned() } -pub fn selectable_list(input: Vec) -> String { +/// Prompts the user to select an item from a list. +/// Returns the selected item or `None` if no item was selected +pub fn selectable_list(input: Vec) -> Option { let input: Vec = input.into_iter().rev().collect(); let options = SkimOptionsBuilder::default().multi(false).build().unwrap(); let item_reader = SkimItemReader::default(); let items = item_reader.of_bufread(Cursor::new(input.join("\n"))); - let selected_items = Skim::run_with(&options, Some(items)) - .map(|out| match out.final_key { - Key::Enter => out.selected_items, - _ => Vec::new(), + Skim::run_with(&options, Some(items)) + .and_then(|out| match out.final_key { + Key::Enter => Some(out.selected_items), + _ => None, }) - .unwrap_or_default(); - - if selected_items.is_empty() { - eprintln!("No item selected"); - process::exit(1); - } - - selected_items[0].output().to_string() + .filter(|selected_items| !selected_items.is_empty()) + .map(|selected_items| selected_items[0].output().to_string()) } pub fn set_namespace(ctx: &str, selection: &str, temp_dir: &str, config: &KubeConfig) { diff --git a/src/main.rs b/src/main.rs index 4e0d795..dbbfe2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod modes; use clap::Parser; use std::env; use std::io; +use std::process; #[macro_use] extern crate lazy_static; @@ -68,14 +69,21 @@ enum Mode { } impl Mode { - fn invoke(&self) { + fn invoke(&self) -> Result <(), String> { + let args = Cli::parse(); match self { - Mode::Namespace => modes::namespace(Cli::parse()), - Mode::Context => modes::context(Cli::parse()), - Mode::DefaultContext => modes::default_context(Cli::parse()), - Mode::DefaultNamespace => modes::default_namespace(Cli::parse()), - Mode::CompletionContext => modes::completion_context(Cli::parse()), - Mode::CompletionNamespace => modes::completion_namespace(Cli::parse()), + Mode::Namespace => modes::namespace(args), + Mode::Context => modes::context(args), + Mode::DefaultContext => modes::default_context(args), + Mode::DefaultNamespace => modes::default_namespace(args), + Mode::CompletionContext => { + modes::completion_context(args); + Ok(()) + }, + Mode::CompletionNamespace => { + modes::completion_namespace(args); + Ok(()) + } } } } @@ -83,7 +91,10 @@ impl Mode { fn main() -> Result<(), io::Error> { let args = Cli::parse(); - Mode::invoke(&args.mode); + if let Err(err) = Mode::invoke(&args.mode) { + eprintln!("error: {}", err); + process::exit(1); + } Ok(()) } diff --git a/src/modes.rs b/src/modes.rs index c26e871..286c8ff 100644 --- a/src/modes.rs +++ b/src/modes.rs @@ -1,81 +1,71 @@ -use std::process; - use crate::{commands::{self}, config, Cli, DEST, KUBECONFIG}; -fn selection(value: Option, callback: fn() -> String) -> String { - match value { - None => callback(), - Some(x) => x.trim().to_string(), - } -} - -pub fn default_context(args: Cli) { +pub fn default_context(args: Cli) -> Result<(), String> { let config = config::get(); if args.current { println!("{}", config.current_context); - return; + return Ok(()); } let ctx = match args.value { None => { - let mut options = Vec::new(); - for context in &config.contexts { - options.push(context.name.to_string()); - } + let options : Vec = config.contexts.iter() + .map(|context| context.name.to_string()) + .collect(); - commands::selectable_list(options) + commands::selectable_list(options).expect("No item selected") } Some(x) => x.trim().to_string(), }; commands::set_default_context(&ctx); - match commands::set_context(&ctx, &DEST, &config) { - Ok(()) => println!("{}", KUBECONFIG.as_str()), - Err(e) => { - eprintln!("error: {}", e); - process::exit(1); - } + + let set_context_result = commands::set_context(&ctx, &DEST, &config) + .map_err(|err| err.to_string()); + + if set_context_result.is_ok() { + println!("{}", KUBECONFIG.as_str()); } + + set_context_result } -pub fn context(args: Cli) { +pub fn context(args: Cli) -> Result<(), String> { if args.current { let config = config::get_current_session(); println!("{}", config.current_context); - return; + return Ok(()); } let config = config::get(); let ctx = match args.value { None => { - let mut options = Vec::new(); - for context in &config.contexts { - options.push(context.name.to_string()); - } + let options : Vec = config.contexts.iter() + .map(|context| context.name.to_string()) + .collect(); - commands::selectable_list(options) + commands::selectable_list(options).ok_or("No item selected")? } Some(x) => x.trim().to_string(), }; - match commands::set_context(&ctx, &DEST, &config) { - Ok(()) => { - println!( - "{}/{}:{}", - &DEST.as_str(), - str::replace(&ctx, ":", "_"), - KUBECONFIG.to_string() - ); - }, - Err(e) => { - eprintln!("error: {}", e); - process::exit(1); - } + let set_context_result = commands::set_context(&ctx, &DEST, &config) + .map_err(|err| err.to_string()); + + if set_context_result.is_ok() { + println!( + "{}/{}:{}", + &DEST.as_str(), + str::replace(&ctx, ":", "_"), + KUBECONFIG.to_string() + ); } + + set_context_result } -pub fn namespace(args: Cli) { +pub fn namespace(args: Cli) -> Result<(), String> { let config = config::get_current_session(); if args.current { let ctx = config @@ -93,13 +83,16 @@ pub fn namespace(args: Cli) { } None => println!("default"), } - return; + return Ok(()); } - let ns = selection(args.value, || -> String { - let namespaces = commands::get_namespaces(); - commands::selectable_list(namespaces) - }); + let ns = match args.value { + None => { + let namespaces : Vec = commands::get_namespaces(); + commands::selectable_list(namespaces).ok_or("No item selected")? + } + Some(x) => x.trim().to_string(), + }; commands::set_namespace(&config.current_context, &ns, &DEST, &config); @@ -109,9 +102,10 @@ pub fn namespace(args: Cli) { str::replace(&config.current_context, ":", "_"), KUBECONFIG.to_string() ); + Ok(()) } -pub fn default_namespace(args: Cli) { +pub fn default_namespace(args: Cli) -> Result<(), String> { let config = config::get(); let ctx = commands::get_current_context(); @@ -132,16 +126,20 @@ pub fn default_namespace(args: Cli) { None => println!("default"), } - return; + return Ok(()); } - let ns = selection(args.value, || -> String { - let namespaces = commands::get_namespaces(); - commands::selectable_list(namespaces) - }); + let ns = match args.value { + None => { + let namespaces : Vec = commands::get_namespaces(); + commands::selectable_list(namespaces).ok_or("No item selected")? + } + Some(x) => x.trim().to_string(), + }; commands::set_default_namespace(&ns, &ctx); commands::set_namespace(&ctx, &ns, &DEST, &config); + Ok(()) } pub fn completion_context(args: Cli) { From 53cb819c4fd2eef520e19115b6f5f112193d3e04 Mon Sep 17 00:00:00 2001 From: Nicolas Mohr Date: Tue, 30 Jan 2024 16:29:40 +0100 Subject: [PATCH 2/3] Add thiserror error handling --- Cargo.lock | 54 ++++++++++++++++++++++++++++++------------------- Cargo.toml | 1 + src/commands.rs | 20 ++---------------- src/error.rs | 19 +++++++++++++++++ src/main.rs | 4 +++- src/modes.rs | 26 ++++++++++++++---------- 6 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 41bf0f8..d7f944a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,7 +176,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -311,7 +311,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.9.3", - "syn", + "syn 1.0.98", ] [[package]] @@ -322,7 +322,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -345,7 +345,7 @@ dependencies = [ "derive_builder_core", "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -357,7 +357,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -555,6 +555,7 @@ dependencies = [ "serde", "serde_yaml", "skim", + "thiserror", ] [[package]] @@ -700,7 +701,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.98", "version_check", ] @@ -723,18 +724,18 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.42" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -865,7 +866,7 @@ checksum = "75743a150d003dd863b51dc809bcad0d73f2102c53632f1e954e738192a3413f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -975,7 +976,7 @@ dependencies = [ "quote", "serde", "serde_derive", - "syn", + "syn 1.0.98", ] [[package]] @@ -991,7 +992,7 @@ dependencies = [ "serde_derive", "serde_json", "sha1", - "syn", + "syn 1.0.98", ] [[package]] @@ -1029,6 +1030,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "term" version = "0.7.0" @@ -1072,22 +1084,22 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -1145,7 +1157,7 @@ dependencies = [ "proc-macro2", "quote", "standback", - "syn", + "syn 1.0.98", ] [[package]] @@ -1270,7 +1282,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.98", "wasm-bindgen-shared", ] @@ -1292,7 +1304,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index f001cc8..c7ab169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ serde_yaml = "0.9.3" serde = { version = "1.0.141", features = ["derive"] } lazy_static = "1.4.0" skim = "0.9.4" +thiserror = "1.0.56" [dev-dependencies] assert_cmd = "2.0" diff --git a/src/commands.rs b/src/commands.rs index 0c309c8..8d961f2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,7 +1,6 @@ -use crate::model::KubeConfig; +use crate::{error::SetContextError, model::KubeConfig}; use crate::config; -use core::fmt; use std::{ io::Cursor, process::{Command, Stdio}, @@ -92,21 +91,6 @@ pub fn set_context(ctx: &str, temp_dir: &str, config: &KubeConfig) -> Result<(), config::write(choice, None, temp_dir); Ok(()) } else { - Err(SetContextError::ContextNotFound{ctx: ctx.to_owned()}) - } -} - -#[derive(Debug, Clone)] -pub enum SetContextError { - ContextNotFound { - ctx : String - }, -} - -impl fmt::Display for SetContextError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SetContextError::ContextNotFound{ctx} => write!(f, "no context exists with the name: \"{}\"", ctx), - } + Err(SetContextError::KubeContextNotFound{ctx: ctx.to_owned()}) } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..1df5b1f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to set context: {0}")] + SetContext(#[source] SetContextError), + #[error("no item selected when prompted to select {prompt}")] + NoItemSelected { + prompt: &'static str + }, +} + +#[derive(Error, Debug)] +pub enum SetContextError { + #[error("no context exists with the name {ctx}")] + KubeContextNotFound { + ctx: String + }, +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 4bff870..a516cc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,9 @@ mod commands; mod config; mod model; mod modes; +mod error; +use crate::error::Error; use clap::Parser; use std::env; use std::io; @@ -69,7 +71,7 @@ enum Mode { } impl Mode { - fn invoke(&self) -> Result <(), String> { + fn invoke(&self) -> Result <(), Error> { let args = Cli::parse(); match self { Mode::Namespace => modes::namespace(args), diff --git a/src/modes.rs b/src/modes.rs index 6884225..eca8aaf 100644 --- a/src/modes.rs +++ b/src/modes.rs @@ -1,6 +1,6 @@ -use crate::{commands::{self}, config, Cli, DEST, KUBECONFIG}; +use crate::{commands::{self}, config, error::Error, Cli, DEST, KUBECONFIG}; -pub fn default_context(args: Cli) -> Result<(), String> { +pub fn default_context(args: Cli) -> Result<(), Error> { let config = config::get(); if args.current { @@ -14,7 +14,8 @@ pub fn default_context(args: Cli) -> Result<(), String> { .map(|context| context.name.to_string()) .collect(); - commands::selectable_list(options).expect("No item selected") + commands::selectable_list(options) + .ok_or(Error::NoItemSelected{prompt: "context" })? } Some(x) => x.trim().to_string(), }; @@ -22,7 +23,7 @@ pub fn default_context(args: Cli) -> Result<(), String> { commands::set_default_context(&ctx); let set_context_result = commands::set_context(&ctx, &DEST, &config) - .map_err(|err| err.to_string()); + .map_err(Error::SetContext); if set_context_result.is_ok() { println!("{}", KUBECONFIG.as_str()); @@ -31,7 +32,7 @@ pub fn default_context(args: Cli) -> Result<(), String> { set_context_result } -pub fn context(args: Cli) -> Result<(), String> { +pub fn context(args: Cli) -> Result<(), Error> { if args.current { let config = config::get_current_session(); println!("{}", config.current_context); @@ -45,13 +46,14 @@ pub fn context(args: Cli) -> Result<(), String> { .map(|context| context.name.to_string()) .collect(); - commands::selectable_list(options).ok_or("No item selected")? + commands::selectable_list(options) + .ok_or(Error::NoItemSelected{prompt: "context"})? } Some(x) => x.trim().to_string(), }; let set_context_result = commands::set_context(&ctx, &DEST, &config) - .map_err(|err| err.to_string()); + .map_err(Error::SetContext); if set_context_result.is_ok() { println!( @@ -65,7 +67,7 @@ pub fn context(args: Cli) -> Result<(), String> { set_context_result } -pub fn namespace(args: Cli) -> Result<(), String> { +pub fn namespace(args: Cli) -> Result<(), Error> { let config = config::get_current_session(); if args.current { let ctx = config @@ -89,7 +91,8 @@ pub fn namespace(args: Cli) -> Result<(), String> { let ns = match args.value { None => { let namespaces : Vec = commands::get_namespaces(); - commands::selectable_list(namespaces).ok_or("No item selected")? + commands::selectable_list(namespaces) + .ok_or(Error::NoItemSelected{prompt: "namespace"})? } Some(x) => x.trim().to_string(), }; @@ -105,7 +108,7 @@ pub fn namespace(args: Cli) -> Result<(), String> { Ok(()) } -pub fn default_namespace(args: Cli) -> Result<(), String> { +pub fn default_namespace(args: Cli) -> Result<(), Error> { let config = config::get(); let ctx = commands::get_current_context(); @@ -132,7 +135,8 @@ pub fn default_namespace(args: Cli) -> Result<(), String> { let ns = match args.value { None => { let namespaces : Vec = commands::get_namespaces(); - commands::selectable_list(namespaces).ok_or("No item selected")? + commands::selectable_list(namespaces) + .ok_or(Error::NoItemSelected{prompt: "namespace"})? } Some(x) => x.trim().to_string(), }; From 6e0af3e77df32b7b9a143162b8ff54d49a4f137d Mon Sep 17 00:00:00 2001 From: Nicolas Mohr Date: Tue, 30 Jan 2024 16:31:04 +0100 Subject: [PATCH 3/3] Add trailing newline --- src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 1df5b1f..53c430d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,4 +16,4 @@ pub enum SetContextError { KubeContextNotFound { ctx: String }, -} \ No newline at end of file +}