@@ -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+
141176impl < ' 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'" ) && s. contains( "provided by BAZ" ) )
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( ) ) && s. contains( "provided by BAR" ) ) ,
752+ "expected error to contain 'invalid boolean value '{}', got: {:#?}" ,
753+ input,
754+ e
755+ )
756+ }
757+ }
631758}
0 commit comments