action_core/
input.rs

1use crate::{env, utils::not_empty};
2use std::ffi::{OsStr, OsString};
3
4#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
5pub struct Input<'a> {
6    pub description: Option<&'a str>,
7    pub deprecation_message: Option<&'a str>,
8    pub default: Option<&'a str>,
9    pub required: Option<bool>,
10}
11
12#[cfg(not(target_family = "unix"))]
13pub fn env_var_name(name: impl AsRef<OsStr>) -> OsString {
14    let name = name.as_ref().to_string_lossy();
15    let out: OsString = if name.starts_with("INPUT_") {
16        name.to_string().into()
17    } else {
18        format!("INPUT_{name}").into()
19    };
20    out.to_ascii_uppercase()
21}
22
23#[cfg(target_family = "unix")]
24pub fn env_var_name(name: impl AsRef<OsStr>) -> OsString {
25    use std::os::unix::ffi::OsStrExt;
26    let name = name.as_ref();
27    let prefix: &OsStr = OsStr::new("INPUT_");
28    let mut out = OsString::from(prefix);
29    if name
30        .as_encoded_bytes()
31        .starts_with(prefix.as_encoded_bytes())
32    {
33        out.push(OsStr::from_bytes(&name.as_encoded_bytes()[prefix.len()..]));
34    } else {
35        out.push(name);
36    }
37    out.to_ascii_uppercase()
38}
39
40pub trait Parse: Sized {
41    type Error: std::error::Error;
42
43    /// Parse input string to type T.
44    ///
45    /// # Errors
46    /// When the string value cannot be parsed as `Self`.
47    fn parse(value: OsString) -> Result<Self, Self::Error>;
48}
49
50#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
51pub enum ParseError {
52    #[error("invalid boolean value \"{0:?}\"")]
53    Bool(OsString),
54    #[error("invalid integer value \"{value:?}\"")]
55    Int {
56        value: OsString,
57        #[source]
58        source: std::num::ParseIntError,
59    },
60}
61
62impl Parse for String {
63    type Error = std::convert::Infallible;
64
65    fn parse(value: OsString) -> Result<Self, Self::Error> {
66        Ok(value.to_string_lossy().to_string())
67    }
68}
69
70impl Parse for OsString {
71    type Error = std::convert::Infallible;
72
73    fn parse(value: OsString) -> Result<Self, Self::Error> {
74        Ok(value)
75    }
76}
77
78impl Parse for bool {
79    type Error = ParseError;
80    fn parse(value: OsString) -> Result<Self, Self::Error> {
81        match value.to_ascii_lowercase().as_os_str().as_encoded_bytes() {
82            b"yes" | b"true" | b"t" => Ok(true),
83            b"no" | b"false" | b"f" => Ok(false),
84            _ => Err(ParseError::Bool(value)),
85        }
86    }
87}
88
89impl Parse for usize {
90    type Error = ParseError;
91    fn parse(value: OsString) -> Result<Self, Self::Error> {
92        value
93            .to_string_lossy()
94            .to_string()
95            .parse()
96            .map_err(|source| ParseError::Int { value, source })
97    }
98}
99
100pub trait SetInput {
101    /// Sets an input.
102    fn set_input(&self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>);
103}
104
105impl<E> SetInput for E
106where
107    E: env::Write,
108{
109    fn set_input(&self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
110        self.set(env_var_name(name.as_ref()), value);
111    }
112}
113
114pub trait GetInput {
115    /// Gets the raw value of an input.
116    fn get_input(&self, name: impl AsRef<OsStr>) -> Option<OsString>;
117}
118
119impl<E> GetInput for E
120where
121    E: env::Read,
122{
123    fn get_input(&self, name: impl AsRef<OsStr>) -> Option<OsString> {
124        self.get(env_var_name(name.as_ref())).and_then(not_empty)
125    }
126}
127
128pub trait ParseInput {
129    /// Parse the value of an input.
130    ///
131    /// Attempts to parse as T if a value is present, other returns `Ok(None)`.
132    ///
133    /// # Errors
134    /// If the variable cannot be parsed.
135    fn parse_input<T>(&self, name: impl AsRef<OsStr>) -> Result<Option<T>, <T as Parse>::Error>
136    where
137        T: Parse;
138}
139
140impl<E> ParseInput for E
141where
142    E: env::Read,
143{
144    fn parse_input<T>(&self, name: impl AsRef<OsStr>) -> Result<Option<T>, <T as Parse>::Error>
145    where
146        T: Parse,
147    {
148        match self.get_input(name) {
149            Some(input) => Some(T::parse(input)).transpose(),
150            None => Ok(None),
151        }
152    }
153}
154
155/// Gets the values of an multiline input.
156///
157/// # Errors
158/// If the environment variable is not present.
159pub fn get_multiline(env: &impl env::Read, name: impl AsRef<OsStr>) -> Option<Vec<String>> {
160    let value = env.get_input(name)?;
161    let lines = value
162        .to_string_lossy()
163        .lines()
164        .map(ToOwned::to_owned)
165        .collect();
166    Some(lines)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::{GetInput, ParseInput, SetInput};
172    use crate::env::{EnvMap, Read};
173    use similar_asserts::assert_eq as sim_assert_eq;
174
175    #[test]
176    fn test_env_name() {
177        sim_assert_eq!(super::env_var_name("some-input"), "INPUT_SOME-INPUT");
178        sim_assert_eq!(super::env_var_name("INPUT_some-input"), "INPUT_SOME-INPUT");
179        sim_assert_eq!(super::env_var_name("INPUT_SOME-INPUT"), "INPUT_SOME-INPUT");
180        sim_assert_eq!(
181            super::env_var_name("test-INPUT_SOME-INPUT"),
182            "INPUT_TEST-INPUT_SOME-INPUT"
183        );
184    }
185
186    #[test]
187    fn test_get_non_empty_input() {
188        let env = EnvMap::default();
189        env.set_input("some-input", "SET");
190        sim_assert_eq!(env.get("INPUT_SOME-INPUT"), Some("SET".into()));
191        sim_assert_eq!(env.get_input("some-input"), Some("SET".into()));
192    }
193
194    #[test]
195    fn test_get_empty_input() {
196        let env = EnvMap::default();
197        let input_name = "some-input";
198        sim_assert_eq!(env.parse_input::<String>(input_name), Ok(None));
199        env.set_input(input_name, "");
200        sim_assert_eq!(env.parse_input::<String>(input_name), Ok(None));
201        env.set_input(input_name, " ");
202        sim_assert_eq!(env.parse_input::<String>(input_name), Ok(Some(" ".into())));
203    }
204}