pub mod parameter_sweep;
pub mod parsers;
pub mod timezone;
use anyhow::{anyhow, Result};
use clap::builder::{
styling::{AnsiColor, Effects},
Styles,
};
use compact_str::CompactString;
use regex::Regex;
use std::collections::HashMap;
use std::time::{Duration, SystemTime};
pub use parameter_sweep::{generate_param_combinations, parse_param_spec};
pub use parsers::{
parse_gpu_indices, parse_job_ids, parse_memory_limit, parse_since_time, parse_time_limit,
};
pub trait ParameterLookup {
fn get_param(&self, key: &str) -> Option<&CompactString>;
}
impl ParameterLookup for HashMap<CompactString, CompactString> {
fn get_param(&self, key: &str) -> Option<&CompactString> {
self.get(key)
}
}
pub fn substitute_parameters<P: ParameterLookup + ?Sized>(
command: &str,
parameters: &P,
) -> Result<String> {
let re = Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}").unwrap();
let mut result = command.to_string();
let mut missing_params = Vec::new();
for cap in re.captures_iter(command) {
let param_name = &cap[1];
if let Some(value) = parameters.get_param(param_name) {
let pattern = format!("{{{}}}", param_name);
result = result.replace(&pattern, value.as_str());
} else {
missing_params.push(param_name.to_string());
}
}
if !missing_params.is_empty() {
return Err(anyhow!(
"Missing parameter values: {}",
missing_params.join(", ")
));
}
Ok(result)
}
pub fn format_duration(duration: Duration) -> String {
let total_secs = duration.as_secs();
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
pub fn format_elapsed_time(
started_at: Option<SystemTime>,
finished_at: Option<SystemTime>,
) -> String {
match started_at {
Some(start_time) => {
let end_time = finished_at.unwrap_or_else(SystemTime::now);
if let Ok(elapsed) = end_time.duration_since(start_time) {
format_duration(elapsed)
} else {
"-".to_string()
}
}
None => "-".to_string(),
}
}
pub fn format_memory(memory_mb: u64) -> String {
if memory_mb >= 1024 {
let gb = memory_mb as f64 / 1024.0;
if gb.fract() < 0.01 {
format!("{:.0}G", gb)
} else {
format!("{:.1}G", gb)
}
} else {
format!("{}M", memory_mb)
}
}
pub fn normalize_project(project: Option<&str>) -> Option<String> {
project
.map(str::trim)
.filter(|p| !p.is_empty())
.map(ToOwned::to_owned)
}
pub fn validate_project_policy(
project: Option<&str>,
projects: &crate::config::ProjectsConfig,
) -> Result<Option<String>> {
let normalized = normalize_project(project);
if projects.require_project && normalized.is_none() {
return Err(anyhow!(
"Project is required by configuration. Provide --project (CLI) or a non-empty `project` field."
));
}
if let Some(ref proj) = normalized {
if proj.len() > 64 {
return Err(anyhow!(
"Project name too long (max 64 characters): '{}'",
proj
));
}
if !projects.known_projects.is_empty()
&& !projects.known_projects.iter().any(|known| known == proj)
{
return Err(anyhow!(
"Unknown project '{}'. Known projects: {}",
proj,
projects.known_projects.join(", ")
));
}
}
Ok(normalized)
}
pub const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default());
pub fn format_system_time(time: SystemTime) -> String {
use chrono::{DateTime, Utc};
let duration = time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let datetime =
DateTime::<Utc>::from_timestamp(duration.as_secs() as i64, 0).unwrap_or_default();
datetime.format("%Y-%m-%d %H:%M:%S UTC").to_string()
}
pub fn format_duration_compact(duration: Duration) -> String {
let total_secs = duration.as_secs();
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
if hours > 0 {
if minutes > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}h", hours)
}
} else if minutes > 0 {
if seconds > 0 {
format!("{}m {}s", minutes, seconds)
} else {
format!("{}m", minutes)
}
} else {
format!("{}s", seconds)
}
}
pub fn validate_job_state(
job: &crate::core::job::Job,
expected_state: crate::core::job::JobState,
operation: &str,
) -> Result<()> {
if job.state != expected_state {
Err(anyhow!(
"Job {} is in state '{}' and cannot be {}. Only {} jobs can be {}.",
job.id,
job.state,
operation,
expected_state,
operation
))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {}