Skip to content

Commit 29801d8

Browse files
committed
feat: enhance boolean parsing for environment variables
Add support for common boolean representations in environment variables beyond the standard Rust bool parsing. Now accepts various forms like: - True values: "y", "yes", "t", "true", "on", "1" - False values: "n", "no", "f", "false", "off", "0", "" This makes the library more user-friendly and aligns with common expectations for environment variable configuration. Implementation draws inspiration from clap_builder and python-humanfriendly. Signed-off-by: Michel Heily <michelheily@gmail.com>
1 parent 8eb504f commit 29801d8

File tree

1 file changed

+132
-5
lines changed

1 file changed

+132
-5
lines changed

src/lib.rs

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,41 @@ macro_rules! forward_parsed_values {
138138
}
139139
}
140140

141+
/// Parses string input into a boolean value.
142+
///
143+
/// Accepts various common boolean representations:
144+
/// - True values: "y", "yes", "t", "true", "on", "1"
145+
/// - False values: "f", "no", "n", "false", "off", "0"
146+
///
147+
/// Returns an error for any other input.
148+
///
149+
/// Implementation is heavily inspired by:
150+
/// - [str_to_bool](https://github.com/clap-rs/clap/blob/c3a1ddc1182fa7cf2cfe6d6dba4f76db83d48178/clap_builder/src/util/str_to_bool.rs) module from clap_builder
151+
/// - [humanfriendly.coerce_boolean](https://github.com/xolox/python-humanfriendly/blob/6758ac61f906cd8528682003070a57febe4ad3cf/humanfriendly/__init__.py#L91) from python-humanfriendly
152+
fn parse_boolean_like_str(s: &str) -> Result<bool> {
153+
const TRUE_LITERALS: [&str; 6] = ["y", "yes", "t", "true", "on", "1"];
154+
const FALSE_LITERALS: [&str; 6] = ["n", "no", "f", "false", "off", "0"];
155+
156+
let lower_s = s.trim().to_lowercase();
157+
158+
if TRUE_LITERALS.contains(&lower_s.as_str()) {
159+
Ok(true)
160+
} else if FALSE_LITERALS.contains(&lower_s.as_str()) {
161+
Ok(false)
162+
} else {
163+
Err(de::Error::custom(format!(
164+
"invalid boolean value '{}' - valid values: [{}]",
165+
s,
166+
TRUE_LITERALS
167+
.into_iter()
168+
.zip(FALSE_LITERALS.into_iter())
169+
.flat_map(|(a, b)| [a, b])
170+
.collect::<Vec<_>>()
171+
.join(", ")
172+
)))
173+
}
174+
}
175+
141176
impl<'de> de::Deserializer<'de> for Val {
142177
type Error = Error;
143178
fn deserialize_any<V>(
@@ -182,8 +217,23 @@ impl<'de> de::Deserializer<'de> for Val {
182217
visitor.visit_some(self)
183218
}
184219

220+
fn deserialize_bool<V>(
221+
self,
222+
visitor: V,
223+
) -> Result<V::Value>
224+
where
225+
V: de::Visitor<'de>,
226+
{
227+
match parse_boolean_like_str(&self.1) {
228+
Ok(val) => val.into_deserializer().deserialize_bool(visitor),
229+
Err(e) => Err(de::Error::custom(format_args!(
230+
"{} while parsing value '{}' provided by {}",
231+
e, self.1, self.0
232+
))),
233+
}
234+
}
235+
185236
forward_parsed_values! {
186-
bool => deserialize_bool,
187237
u8 => deserialize_u8,
188238
u16 => deserialize_u16,
189239
u32 => deserialize_u32,
@@ -548,10 +598,11 @@ mod tests {
548598
];
549599
match from_iter::<_, Foo>(data) {
550600
Ok(_) => panic!("expected failure"),
551-
Err(e) => assert_eq!(
552-
e,
553-
Error::Custom(String::from("provided string was not `true` or `false` while parsing value \'notabool\' provided by BAZ"))
554-
),
601+
Err(e) => {
602+
assert!(
603+
matches!(e, Error::Custom(s) if s.contains("invalid boolean value 'notabool'"))
604+
)
605+
}
555606
}
556607
}
557608

@@ -628,4 +679,80 @@ mod tests {
628679
Err(e) => panic!("{:#?}", e),
629680
}
630681
}
682+
683+
const VALID_BOOLEAN_INPUTS: [(&str, bool); 25] = [
684+
("true", true),
685+
("TRUE", true),
686+
("True", true),
687+
("false", false),
688+
("FALSE", false),
689+
("False", false),
690+
("yes", true),
691+
("YES", true),
692+
("Yes", true),
693+
("no", false),
694+
("NO", false),
695+
("No", false),
696+
("on", true),
697+
("ON", true),
698+
("On", true),
699+
("off", false),
700+
("OFF", false),
701+
("Off", false),
702+
("1", true),
703+
("1 ", true),
704+
("0", false),
705+
("y", true),
706+
("Y", true),
707+
("n", false),
708+
("N", false),
709+
];
710+
711+
const INVALID_BOOLEAN_INPUTS: [&str; 6] = ["notabool", "asd", "TRU", "Noo", "dont", ""];
712+
713+
#[test]
714+
fn parse_boolean_like_str_works() {
715+
for (input, expected) in VALID_BOOLEAN_INPUTS {
716+
let parsed_bool = parse_boolean_like_str(input).expect("expected success, got error");
717+
assert_eq!(parsed_bool, expected);
718+
}
719+
}
720+
721+
#[test]
722+
fn parse_boolean_like_str_fails_with_invalid_input() {
723+
for input in INVALID_BOOLEAN_INPUTS {
724+
let err = parse_boolean_like_str(input).unwrap_err();
725+
assert!(
726+
matches!(err, Error::Custom(s) if s.contains(format!("invalid boolean value '{}'", input).as_str()))
727+
);
728+
}
729+
}
730+
#[derive(Deserialize, Debug, PartialEq)]
731+
struct BoolTest {
732+
bar: bool,
733+
}
734+
735+
#[test]
736+
fn deserialize_bool_works() {
737+
for (input, expected) in VALID_BOOLEAN_INPUTS {
738+
let data = vec![(String::from("BAR"), String::from(input))];
739+
let parsed = from_iter::<_, BoolTest>(data).expect("expected success, got error");
740+
assert_eq!(parsed.bar, expected);
741+
}
742+
}
743+
744+
#[test]
745+
fn deserialize_bool_fails_with_invalid_input() {
746+
for input in INVALID_BOOLEAN_INPUTS {
747+
let data = vec![(String::from("BAR"), String::from(input))];
748+
let e = from_iter::<_, BoolTest>(data)
749+
.expect_err(format!("expected Err for input: '{}' but got Ok", input).as_str());
750+
assert!(
751+
matches!(&e, Error::Custom(s) if s.contains(format!("invalid boolean value '{}'", input).as_str())),
752+
"expected error to contain 'invalid boolean value '{}', got: {:#?}",
753+
input,
754+
e
755+
)
756+
}
757+
}
631758
}

0 commit comments

Comments
 (0)