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 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 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 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 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
155pub 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}