use crate::{env, utils::not_empty};
use std::ffi::{OsStr, OsString};
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct Input<'a> {
pub description: Option<&'a str>,
pub deprecation_message: Option<&'a str>,
pub default: Option<&'a str>,
pub required: Option<bool>,
}
#[cfg(not(target_family = "unix"))]
pub fn env_var_name(name: impl AsRef<OsStr>) -> OsString {
let name = name.as_ref().to_string_lossy();
let out: OsString = if name.starts_with("INPUT_") {
name.to_string().into()
} else {
format!("INPUT_{name}").into()
};
out.to_ascii_uppercase()
}
#[cfg(target_family = "unix")]
pub fn env_var_name(name: impl AsRef<OsStr>) -> OsString {
use std::os::unix::ffi::OsStrExt;
let name = name.as_ref();
let prefix: &OsStr = OsStr::new("INPUT_");
let mut out = OsString::from(prefix);
if name
.as_encoded_bytes()
.starts_with(prefix.as_encoded_bytes())
{
out.push(OsStr::from_bytes(&name.as_encoded_bytes()[prefix.len()..]));
} else {
out.push(name);
}
out.to_ascii_uppercase()
}
pub trait Parse: Sized {
type Error: std::error::Error;
fn parse(value: OsString) -> Result<Self, Self::Error>;
}
#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
pub enum ParseError {
#[error("invalid boolean value \"{0:?}\"")]
Bool(OsString),
#[error("invalid integer value \"{value:?}\"")]
Int {
value: OsString,
#[source]
source: std::num::ParseIntError,
},
}
impl Parse for String {
type Error = std::convert::Infallible;
fn parse(value: OsString) -> Result<Self, Self::Error> {
Ok(value.to_string_lossy().to_string())
}
}
impl Parse for OsString {
type Error = std::convert::Infallible;
fn parse(value: OsString) -> Result<Self, Self::Error> {
Ok(value)
}
}
impl Parse for bool {
type Error = ParseError;
fn parse(value: OsString) -> Result<Self, Self::Error> {
match value.to_ascii_lowercase().as_os_str().as_encoded_bytes() {
b"yes" | b"true" | b"t" => Ok(true),
b"no" | b"false" | b"f" => Ok(false),
_ => Err(ParseError::Bool(value)),
}
}
}
impl Parse for usize {
type Error = ParseError;
fn parse(value: OsString) -> Result<Self, Self::Error> {
value
.to_string_lossy()
.to_string()
.parse()
.map_err(|source| ParseError::Int { value, source })
}
}
pub trait SetInput {
fn set_input(&self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>);
}
impl<E> SetInput for E
where
E: env::Write,
{
fn set_input(&self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
self.set(env_var_name(name.as_ref()), value);
}
}
pub trait GetInput {
fn get_input(&self, name: impl AsRef<OsStr>) -> Option<OsString>;
}
impl<E> GetInput for E
where
E: env::Read,
{
fn get_input(&self, name: impl AsRef<OsStr>) -> Option<OsString> {
self.get(env_var_name(name.as_ref())).and_then(not_empty)
}
}
pub trait ParseInput {
fn parse_input<T>(&self, name: impl AsRef<OsStr>) -> Result<Option<T>, <T as Parse>::Error>
where
T: Parse;
}
impl<E> ParseInput for E
where
E: env::Read,
{
fn parse_input<T>(&self, name: impl AsRef<OsStr>) -> Result<Option<T>, <T as Parse>::Error>
where
T: Parse,
{
match self.get_input(name) {
Some(input) => Some(T::parse(input)).transpose(),
None => Ok(None),
}
}
}
pub fn get_multiline(env: &impl env::Read, name: impl AsRef<OsStr>) -> Option<Vec<String>> {
let value = env.get_input(name)?;
let lines = value
.to_string_lossy()
.lines()
.map(ToOwned::to_owned)
.collect();
Some(lines)
}
#[cfg(test)]
mod tests {
use super::{GetInput, ParseInput, SetInput};
use crate::env::{EnvMap, Read};
use similar_asserts::assert_eq as sim_assert_eq;
#[test]
fn test_env_name() {
sim_assert_eq!(super::env_var_name("some-input"), "INPUT_SOME-INPUT");
sim_assert_eq!(super::env_var_name("INPUT_some-input"), "INPUT_SOME-INPUT");
sim_assert_eq!(super::env_var_name("INPUT_SOME-INPUT"), "INPUT_SOME-INPUT");
sim_assert_eq!(
super::env_var_name("test-INPUT_SOME-INPUT"),
"INPUT_TEST-INPUT_SOME-INPUT"
);
}
#[test]
fn test_get_non_empty_input() {
let env = EnvMap::default();
env.set_input("some-input", "SET");
sim_assert_eq!(env.get("INPUT_SOME-INPUT"), Some("SET".into()));
sim_assert_eq!(env.get_input("some-input"), Some("SET".into()));
}
#[test]
fn test_get_empty_input() {
let env = EnvMap::default();
let input_name = "some-input";
sim_assert_eq!(env.parse_input::<String>(input_name), Ok(None));
env.set_input(input_name, "");
sim_assert_eq!(env.parse_input::<String>(input_name), Ok(None));
env.set_input(input_name, " ");
sim_assert_eq!(env.parse_input::<String>(input_name), Ok(Some(" ".into())));
}
}