use secrecy::SecretString;
use std::env;
use std::io::{self, IsTerminal, Write};
pub fn is_interactive() -> bool {
io::stdin().is_terminal()
}
pub fn prompt_for_value(prompt: &str) -> Result<String, String> {
if !is_interactive() {
return Err(format!(
"Cannot prompt for '{}': not running in interactive terminal (no TTY). \
Please provide the value via CLI flag or environment variable.",
prompt
));
}
print!("{}: ", prompt);
io::stdout()
.flush()
.map_err(|e| format!("Failed to flush stdout: {}", e))?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| format!("Failed to read input: {}", e))?;
Ok(input.trim().to_string())
}
pub fn prompt_for_value_with_default(prompt: &str, default: &str) -> Result<String, String> {
if !is_interactive() {
return Err(format!(
"Cannot prompt for '{}': not running in interactive terminal (no TTY). \
Please provide the value via CLI flag or environment variable.",
prompt
));
}
print!("{} (default: {}): ", prompt, default);
io::stdout()
.flush()
.map_err(|e| format!("Failed to flush stdout: {}", e))?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| format!("Failed to read input: {}", e))?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
pub fn prompt_for_bool(prompt: &str, default: bool) -> Result<bool, String> {
if !is_interactive() {
return Err(format!(
"Cannot prompt for '{}': not running in interactive terminal (no TTY). \
Please provide the value via CLI flag or environment variable.",
prompt
));
}
let default_str = if default { "Y/n" } else { "y/N" };
print!("{} ({}): ", prompt, default_str);
io::stdout()
.flush()
.map_err(|e| format!("Failed to flush stdout: {}", e))?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| format!("Failed to read input: {}", e))?;
let trimmed = input.trim().to_lowercase();
if trimmed.is_empty() {
Ok(default)
} else {
Ok(trimmed == "y" || trimmed == "yes")
}
}
pub fn resolve_param(value: Option<String>, env_var: &str, prompt: &str) -> Result<String, String> {
if let Some(v) = value {
return Ok(v);
}
if let Ok(v) = env::var(env_var) {
if !v.is_empty() {
return Ok(v);
}
}
prompt_for_value(prompt)
}
pub fn resolve_port(
value: Option<u16>,
env_var: &str,
prompt: &str,
default: u16,
) -> Result<u16, String> {
if let Some(v) = value {
return Ok(v);
}
if let Ok(v) = env::var(env_var) {
if !v.is_empty() {
return v.parse::<u16>().map_err(|_| {
format!(
"Invalid port value '{}' in environment variable {}",
v, env_var
)
});
}
}
let input = prompt_for_value_with_default(prompt, &default.to_string())?;
input
.parse()
.map_err(|_| format!("Invalid port value: '{}'", input))
}
pub fn resolve_bool(
value: Option<bool>,
env_var: &str,
prompt: &str,
default: bool,
) -> Result<bool, String> {
if let Some(v) = value {
return Ok(v);
}
if let Ok(v) = env::var(env_var) {
return Ok(v.to_lowercase() == "true" || v == "1");
}
prompt_for_bool(prompt, default)
}
pub fn resolve_password(env_var: &str, prompt: &str) -> Result<SecretString, String> {
if let Ok(v) = env::var(env_var) {
if !v.is_empty() {
return Ok(SecretString::from(v));
}
}
if !is_interactive() {
return Err(format!(
"Cannot prompt for '{}': not running in interactive terminal (no TTY). \
Please provide the value via environment variable {}.",
prompt, env_var
));
}
print!("{} (input hidden): ", prompt);
io::stdout()
.flush()
.map_err(|e| format!("Failed to flush stdout: {}", e))?;
let password =
rpassword::read_password().map_err(|e| format!("Failed to read password: {}", e))?;
Ok(SecretString::from(password))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_interactive_returns_bool() {
let result = is_interactive();
assert!(result == true || result == false);
}
#[test]
fn test_resolve_param_with_value() {
let result = resolve_param(Some("provided".to_string()), "UNUSED_VAR", "unused");
assert_eq!(result.unwrap(), "provided");
}
#[test]
fn test_resolve_param_with_empty_value_uses_it() {
let result = resolve_param(Some("".to_string()), "UNUSED_VAR", "unused");
assert_eq!(result.unwrap(), "");
}
#[test]
fn test_resolve_port_with_value() {
let result = resolve_port(Some(8080), "UNUSED_VAR", "unused", 465);
assert_eq!(result.unwrap(), 8080);
}
#[test]
fn test_resolve_port_with_zero_value() {
let result = resolve_port(Some(0), "UNUSED_VAR", "unused", 465);
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_resolve_bool_with_value_true() {
let result = resolve_bool(Some(true), "UNUSED_VAR", "unused", false);
assert!(result.unwrap());
}
#[test]
fn test_resolve_bool_with_value_false() {
let result = resolve_bool(Some(false), "UNUSED_VAR", "unused", true);
assert!(!result.unwrap());
}
#[test]
fn test_resolve_bool_value_overrides_default() {
let result = resolve_bool(Some(false), "UNUSED_VAR", "unused", true);
assert!(!result.unwrap());
let result = resolve_bool(Some(true), "UNUSED_VAR", "unused", false);
assert!(result.unwrap());
}
#[test]
fn test_resolve_param_no_tty_no_value_fails() {
let result = resolve_param(Some("value".to_string()), "NONEXISTENT_VAR_12345", "test");
assert!(result.is_ok());
}
}