diff --git a/src/cli/cmd/job/start.rs b/src/cli/cmd/job/start.rs index d17f758..3e97214 100644 --- a/src/cli/cmd/job/start.rs +++ b/src/cli/cmd/job/start.rs @@ -1,6 +1,10 @@ use crate::{ cli::{cmd::job::logs, complete::complete_job_launcher_id}, - data::{simple_message::SimpleMessage, submission_id::SubmissionId}, + data::{ + launcher_id::{LauncherIdError, LauncherIdOrName}, + simple_message::SimpleMessage, + submission_id::SubmissionId, + }, httpclient::{self, data::SessionStartRequest}, }; @@ -9,7 +13,6 @@ use crate::cli::sink::Error as SinkError; use clap::{Parser, ValueHint}; use clap_complete::ArgValueCompleter; -use ulid::Ulid; use snafu::{ResultExt, Snafu}; @@ -20,7 +23,7 @@ use snafu::{ResultExt, Snafu}; pub struct Input { /// The launcher to use for launching the job. #[arg(long, value_hint=ValueHint::Other, add = ArgValueCompleter::new(complete_job_launcher_id))] - pub launcher: Ulid, + pub launcher: LauncherIdOrName, /// A submission id allows to deduplicate same job submissions. If /// missing, a random one is generated. It must be at least 4 @@ -49,6 +52,9 @@ pub enum Error { #[snafu(display("Http error: {}", source))] HttpClient { source: httpclient::Error }, + + #[snafu(display("Launcher id error: {}", source))] + LauncherId { source: LauncherIdError }, } impl Input { @@ -67,8 +73,13 @@ impl Input { } else { Some(self.passthrough.clone()) }; + let launcher_id = self + .launcher + .resolve(&ctx.client) + .await + .context(LauncherIdSnafu)?; let req = SessionStartRequest { - launcher_id: self.launcher.to_string(), + launcher_id: launcher_id.to_string(), session_type: "non-interactive".into(), submission_id: Some(submission_id), job_args_override: args, diff --git a/src/cli/cmd/job/stop.rs b/src/cli/cmd/job/stop.rs index 89a5214..8402297 100644 --- a/src/cli/cmd/job/stop.rs +++ b/src/cli/cmd/job/stop.rs @@ -13,7 +13,7 @@ use snafu::{ResultExt, Snafu}; /// Stop a running non-interactive session. #[derive(Parser, Debug)] pub struct Input { - /// The launcher to use for launching the job. + /// The id of the job to stop #[arg(value_hint=ValueHint::Other, add = ArgValueCompleter::new(complete_job_name))] pub job_id: String, } diff --git a/src/cli/complete.rs b/src/cli/complete.rs index 08e32a6..780d0ab 100644 --- a/src/cli/complete.rs +++ b/src/cli/complete.rs @@ -62,20 +62,22 @@ fn parse_common_opts() -> Result { async fn make_launcher_completion_candidate( client: &Client, launcher: &SessionLauncher, -) -> CompletionCandidate { +) -> Vec { let mut help = StyledStr::new(); help.push_str(&launcher.name); - let cc = CompletionCandidate::new(launcher.id.clone()); - - let Ok(Some(project)) = client.get_project_by_id(&launcher.project_id).await else { + let cc = vec![ + CompletionCandidate::new(launcher.id.clone()), + CompletionCandidate::new(launcher.name.clone()), + ]; + + if let Ok(Some(project)) = client.get_project_by_id(&launcher.project_id).await { + help.push_str(" - "); + help.push_str(&project.name); + } else { eprintln!("Cannot get project details for: {}", launcher.project_id); - return cc.help(Some(help)); - }; - - help.push_str(" - "); - help.push_str(&project.name); + } - cc.help(Some(help)) + cc.into_iter().map(|c| c.help(Some(help.clone()))).collect() } async fn make_job_name_completion_candidate( @@ -149,7 +151,7 @@ pub fn complete_job_launcher_id(current: &ffi::OsStr) -> Vec Result { + s.parse::() + } + pub async fn resolve(&self, client: &Client) -> Result { + match self { + LauncherIdOrName::Id(ulid) => Ok(*ulid), + LauncherIdOrName::Name(name) => { + let launchers = client + .list_launchers() + .await + .map_err(LauncherIdError::RequestError)?; + match launchers.iter().find(|l| l.name == *name) { + Some(l) => { + l.id.parse::() + .map_err(|_| LauncherIdError::InvalidInput(l.id.clone())) + } + None => Err(LauncherIdError::NotFound(name.clone())), + } + } + } + } +} + +impl FromStr for LauncherIdOrName { + type Err = LauncherIdError; + + fn from_str(s: &str) -> Result { + Ok(s.parse::() + .map(LauncherIdOrName::Id) + .unwrap_or_else(|_| LauncherIdOrName::Name(s.to_string()))) + } +} + +impl fmt::Display for LauncherIdOrName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + LauncherIdOrName::Name(name) => { + name.to_string() + } + LauncherIdOrName::Id(id) => id.to_string(), + } + ) + } +} +impl Display for LauncherIdError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LauncherIdError::InvalidInput(msg) => write!(f, "Invalid launcher id: {}", msg), + LauncherIdError::NotFound(name) => write!(f, "Launcher not found: {}", name), + LauncherIdError::RequestError(error) => { + write!(f, "Couldn't resolve launcher name: {}", error) + } + } + } +} +impl std::error::Error for LauncherIdError {} + +#[test] +fn read_to_string() { + let id1 = LauncherIdOrName::Name("my-launcher".to_string()); + let id2 = LauncherIdOrName::Id(Ulid::generate()); + + for id in [id1, id2] { + let id_str = format!("{}", id); + let id_parsed = LauncherIdOrName::parse(&id_str).unwrap(); + assert_eq!(id, id_parsed); + } +}