@@ -138,6 +138,41 @@ macro_rules! forward_parsed_values {
138
138
}
139
139
}
140
140
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
+
141
176
impl < ' de > de:: Deserializer < ' de > for Val {
142
177
type Error = Error ;
143
178
fn deserialize_any < V > (
@@ -182,8 +217,23 @@ impl<'de> de::Deserializer<'de> for Val {
182
217
visitor. visit_some ( self )
183
218
}
184
219
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
+
185
236
forward_parsed_values ! {
186
- bool => deserialize_bool,
187
237
u8 => deserialize_u8,
188
238
u16 => deserialize_u16,
189
239
u32 => deserialize_u32,
@@ -548,10 +598,11 @@ mod tests {
548
598
] ;
549
599
match from_iter :: < _ , Foo > ( data) {
550
600
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
+ }
555
606
}
556
607
}
557
608
@@ -628,4 +679,80 @@ mod tests {
628
679
Err ( e) => panic ! ( "{:#?}" , e) ,
629
680
}
630
681
}
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
+ }
631
758
}
0 commit comments