use std::fmt::Display;
use std::str::FromStr;
use crate::error::{Error, Result};
use crate::log;
#[derive(Debug, Clone, Copy)]
pub struct InputOptions {
pub required: bool,
pub trim: bool,
}
impl Default for InputOptions {
fn default() -> Self {
Self {
required: false,
trim: true,
}
}
}
#[must_use]
pub fn input_env_key(name: &str) -> String {
format!("INPUT_{}", name.replace(' ', "_").to_uppercase())
}
fn raw(name: &str) -> Option<String> {
std::env::var(input_env_key(name)).ok()
}
pub fn input_with(name: &str, options: InputOptions) -> Result<String> {
let value = raw(name).unwrap_or_default();
if options.required && value.is_empty() {
return Err(Error::MissingRequiredInput(name.to_owned()));
}
let value = if options.trim {
value.trim().to_owned()
} else {
value
};
Ok(value)
}
#[must_use]
pub fn input(name: &str) -> String {
input_with(name, InputOptions::default()).unwrap_or_default()
}
pub fn input_required(name: &str) -> Result<String> {
input_with(
name,
InputOptions {
required: true,
trim: true,
},
)
}
fn parse_bool(name: &str, value: &str) -> Result<bool> {
match value {
"true" | "True" | "TRUE" => Ok(true),
"false" | "False" | "FALSE" => Ok(false),
_ => Err(Error::InvalidBool {
name: name.to_owned(),
value: value.to_owned(),
}),
}
}
pub fn bool_input(name: &str) -> Result<bool> {
let value = input_with(
name,
InputOptions {
required: false,
trim: true,
},
)?;
parse_bool(name, &value)
}
#[must_use]
pub fn multiline_input(name: &str) -> Vec<String> {
multiline_input_with(name, InputOptions::default()).unwrap_or_default()
}
pub fn multiline_input_with(name: &str, options: InputOptions) -> Result<Vec<String>> {
let value = input_with(
name,
InputOptions {
required: options.required,
trim: false,
},
)?;
Ok(split_multiline(&value, options.trim))
}
fn split_multiline(value: &str, trim: bool) -> Vec<String> {
let items = value
.split('\n')
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned);
if trim {
items.map(|line| line.trim().to_owned()).collect()
} else {
items.collect()
}
}
pub fn input_as<T>(name: &str) -> Result<T>
where
T: FromStr,
T::Err: Display,
{
let value = input_required(name)?;
value.parse::<T>().map_err(|e| Error::ParseInput {
name: name.to_owned(),
reason: e.to_string(),
})
}
pub fn mask_input(name: &str) {
if let Some(value) = raw(name).filter(|v| !v.is_empty()) {
log::mask(value);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_transform() {
assert_eq!(input_env_key("my input"), "INPUT_MY_INPUT");
assert_eq!(input_env_key("my-input"), "INPUT_MY-INPUT");
assert_eq!(input_env_key("myInput"), "INPUT_MYINPUT");
assert_eq!(input_env_key("a b-c d"), "INPUT_A_B-C_D");
}
#[test]
fn strict_bool_accepts_canonical() {
for v in ["true", "True", "TRUE"] {
assert!(parse_bool("x", v).unwrap());
}
for v in ["false", "False", "FALSE"] {
assert!(!parse_bool("x", v).unwrap());
}
}
#[test]
fn strict_bool_rejects_others() {
for v in ["yes", "1", "TrUe", "", " true", "0"] {
let e = parse_bool("flag", v).unwrap_err();
assert!(
matches!(e, Error::InvalidBool { .. }),
"{v:?} should be invalid"
);
}
}
#[test]
fn multiline_splits_and_trims_and_drops_empty() {
assert_eq!(
split_multiline("a\n b \n\n c\n", true),
vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]
);
assert!(split_multiline("", true).is_empty());
}
#[test]
fn multiline_keeps_whitespace_only_entries_until_after_filter() {
assert_eq!(
split_multiline("a\n \n\n b\n", true),
vec!["a".to_owned(), "".to_owned(), "b".to_owned()]
);
}
}