action_core/
input.rs

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