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 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(OsStr::from_bytes(&name.as_bytes()[..prefix.len()]));
26 } else {
27 out.push(name);
28 }
29 out
30 }
37
38pub trait Parse: Sized {
39 type Error: std::error::Error;
40
41 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
98pub trait SetInput {
118 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 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 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
172pub 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}