Skip to content

Commit 3835356

Browse files
authored
Fix proper pluralization in validation error messages (#1050)
1 parent ef3e813 commit 3835356

File tree

2 files changed

+54
-18
lines changed

2 files changed

+54
-18
lines changed

src/errors/types.rs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,8 @@ macro_rules! to_string_render {
445445
};
446446
}
447447

448-
fn plural_s(value: usize) -> &'static str {
449-
if value == 1 {
448+
fn plural_s<T: From<u8> + PartialEq>(value: T) -> &'static str {
449+
if value == 1.into() {
450450
""
451451
} else {
452452
"s"
@@ -494,8 +494,8 @@ impl ErrorType {
494494
Self::StringType {..} => "Input should be a valid string",
495495
Self::StringSubType {..} => "Input should be a string, not an instance of a subclass of str",
496496
Self::StringUnicode {..} => "Input should be a valid string, unable to parse raw data as a unicode string",
497-
Self::StringTooShort {..} => "String should have at least {min_length} characters",
498-
Self::StringTooLong {..} => "String should have at most {max_length} characters",
497+
Self::StringTooShort {..} => "String should have at least {min_length} character{expected_plural}",
498+
Self::StringTooLong {..} => "String should have at most {max_length} character{expected_plural}",
499499
Self::StringPatternMismatch {..} => "String should match pattern '{pattern}'",
500500
Self::Enum {..} => "Input should be {expected}",
501501
Self::DictType {..} => "Input should be a valid dictionary",
@@ -512,8 +512,8 @@ impl ErrorType {
512512
Self::FloatType {..} => "Input should be a valid number",
513513
Self::FloatParsing {..} => "Input should be a valid number, unable to parse string as a number",
514514
Self::BytesType {..} => "Input should be a valid bytes",
515-
Self::BytesTooShort {..} => "Data should have at least {min_length} bytes",
516-
Self::BytesTooLong {..} => "Data should have at most {max_length} bytes",
515+
Self::BytesTooShort {..} => "Data should have at least {min_length} byte{expected_plural}",
516+
Self::BytesTooLong {..} => "Data should have at most {max_length} byte{expected_plural}",
517517
Self::ValueError {..} => "Value error, {error}",
518518
Self::AssertionError {..} => "Assertion failed, {error}",
519519
Self::CustomError {..} => "", // custom errors are handled separately
@@ -552,16 +552,16 @@ impl ErrorType {
552552
Self::UrlType {..} => "URL input should be a string or URL",
553553
Self::UrlParsing {..} => "Input should be a valid URL, {error}",
554554
Self::UrlSyntaxViolation {..} => "Input violated strict URL syntax rules, {error}",
555-
Self::UrlTooLong {..} => "URL should have at most {max_length} characters",
555+
Self::UrlTooLong {..} => "URL should have at most {max_length} character{expected_plural}",
556556
Self::UrlScheme {..} => "URL scheme should be {expected_schemes}",
557557
Self::UuidType {..} => "UUID input should be a string, bytes or UUID object",
558558
Self::UuidParsing {..} => "Input should be a valid UUID, {error}",
559559
Self::UuidVersion {..} => "UUID version {expected_version} expected",
560560
Self::DecimalType {..} => "Decimal input should be an integer, float, string or Decimal object",
561561
Self::DecimalParsing {..} => "Input should be a valid decimal",
562-
Self::DecimalMaxDigits {..} => "Decimal input should have no more than {max_digits} digits in total",
563-
Self::DecimalMaxPlaces {..} => "Decimal input should have no more than {decimal_places} decimal places",
564-
Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digits before the decimal point",
562+
Self::DecimalMaxDigits {..} => "Decimal input should have no more than {max_digits} digit{expected_plural} in total",
563+
Self::DecimalMaxPlaces {..} => "Decimal input should have no more than {decimal_places} decimal place{expected_plural}",
564+
Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point",
565565
}
566566
}
567567

@@ -643,13 +643,25 @@ impl ErrorType {
643643
to_string_render!(tmpl, field_type, max_length, actual_length, expected_plural,)
644644
}
645645
Self::IterationError { error, .. } => render!(tmpl, error),
646-
Self::StringTooShort { min_length, .. } => to_string_render!(tmpl, min_length),
647-
Self::StringTooLong { max_length, .. } => to_string_render!(tmpl, max_length),
646+
Self::StringTooShort { min_length, .. } => {
647+
let expected_plural = plural_s(*min_length);
648+
to_string_render!(tmpl, min_length, expected_plural)
649+
}
650+
Self::StringTooLong { max_length, .. } => {
651+
let expected_plural = plural_s(*max_length);
652+
to_string_render!(tmpl, max_length, expected_plural)
653+
}
648654
Self::StringPatternMismatch { pattern, .. } => render!(tmpl, pattern),
649655
Self::Enum { expected, .. } => to_string_render!(tmpl, expected),
650656
Self::MappingType { error, .. } => render!(tmpl, error),
651-
Self::BytesTooShort { min_length, .. } => to_string_render!(tmpl, min_length),
652-
Self::BytesTooLong { max_length, .. } => to_string_render!(tmpl, max_length),
657+
Self::BytesTooShort { min_length, .. } => {
658+
let expected_plural = plural_s(*min_length);
659+
to_string_render!(tmpl, min_length, expected_plural)
660+
}
661+
Self::BytesTooLong { max_length, .. } => {
662+
let expected_plural = plural_s(*max_length);
663+
to_string_render!(tmpl, max_length, expected_plural)
664+
}
653665
Self::ValueError { error, .. } => {
654666
let error = &error
655667
.as_ref()
@@ -688,13 +700,25 @@ impl ErrorType {
688700
Self::UnionTagNotFound { discriminator, .. } => render!(tmpl, discriminator),
689701
Self::UrlParsing { error, .. } => render!(tmpl, error),
690702
Self::UrlSyntaxViolation { error, .. } => render!(tmpl, error),
691-
Self::UrlTooLong { max_length, .. } => to_string_render!(tmpl, max_length),
703+
Self::UrlTooLong { max_length, .. } => {
704+
let expected_plural = plural_s(*max_length);
705+
to_string_render!(tmpl, max_length, expected_plural)
706+
}
692707
Self::UrlScheme { expected_schemes, .. } => render!(tmpl, expected_schemes),
693708
Self::UuidParsing { error, .. } => render!(tmpl, error),
694709
Self::UuidVersion { expected_version, .. } => to_string_render!(tmpl, expected_version),
695-
Self::DecimalMaxDigits { max_digits, .. } => to_string_render!(tmpl, max_digits),
696-
Self::DecimalMaxPlaces { decimal_places, .. } => to_string_render!(tmpl, decimal_places),
697-
Self::DecimalWholeDigits { whole_digits, .. } => to_string_render!(tmpl, whole_digits),
710+
Self::DecimalMaxDigits { max_digits, .. } => {
711+
let expected_plural = plural_s(*max_digits);
712+
to_string_render!(tmpl, max_digits, expected_plural)
713+
}
714+
Self::DecimalMaxPlaces { decimal_places, .. } => {
715+
let expected_plural = plural_s(*decimal_places);
716+
to_string_render!(tmpl, decimal_places, expected_plural)
717+
}
718+
Self::DecimalWholeDigits { whole_digits, .. } => {
719+
let expected_plural = plural_s(*whole_digits);
720+
to_string_render!(tmpl, whole_digits, expected_plural)
721+
}
698722
_ => Ok(tmpl.to_string()),
699723
}
700724
}

tests/test_errors.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,9 @@ def f(input_value, info):
289289
('string_unicode', 'Input should be a valid string, unable to parse raw data as a unicode string', None),
290290
('string_pattern_mismatch', "String should match pattern 'foo'", {'pattern': 'foo'}),
291291
('string_too_short', 'String should have at least 42 characters', {'min_length': 42}),
292+
('string_too_short', 'String should have at least 1 character', {'min_length': 1}),
292293
('string_too_long', 'String should have at most 42 characters', {'max_length': 42}),
294+
('string_too_long', 'String should have at most 1 character', {'max_length': 1}),
293295
('dict_type', 'Input should be a valid dictionary', None),
294296
('mapping_type', 'Input should be a valid mapping, error: foobar', {'error': 'foobar'}),
295297
('iterable_type', 'Input should be iterable', None),
@@ -312,7 +314,9 @@ def f(input_value, info):
312314
('float_parsing', 'Input should be a valid number, unable to parse string as a number', None),
313315
('bytes_type', 'Input should be a valid bytes', None),
314316
('bytes_too_short', 'Data should have at least 42 bytes', {'min_length': 42}),
317+
('bytes_too_short', 'Data should have at least 1 byte', {'min_length': 1}),
315318
('bytes_too_long', 'Data should have at most 42 bytes', {'max_length': 42}),
319+
('bytes_too_long', 'Data should have at most 1 byte', {'max_length': 1}),
316320
('value_error', 'Value error, foobar', {'error': ValueError('foobar')}),
317321
('assertion_error', 'Assertion failed, foobar', {'error': AssertionError('foobar')}),
318322
('literal_error', 'Input should be foo', {'expected': 'foo'}),
@@ -356,19 +360,27 @@ def f(input_value, info):
356360
('url_parsing', 'Input should be a valid URL, Foobar', {'error': 'Foobar'}),
357361
('url_syntax_violation', 'Input violated strict URL syntax rules, Foobar', {'error': 'Foobar'}),
358362
('url_too_long', 'URL should have at most 42 characters', {'max_length': 42}),
363+
('url_too_long', 'URL should have at most 1 character', {'max_length': 1}),
359364
('url_scheme', 'URL scheme should be "foo", "bar" or "spam"', {'expected_schemes': '"foo", "bar" or "spam"'}),
360365
('uuid_type', 'UUID input should be a string, bytes or UUID object', None),
361366
('uuid_parsing', 'Input should be a valid UUID, Foobar', {'error': 'Foobar'}),
362367
('uuid_version', 'UUID version 42 expected', {'expected_version': 42}),
363368
('decimal_type', 'Decimal input should be an integer, float, string or Decimal object', None),
364369
('decimal_parsing', 'Input should be a valid decimal', None),
365370
('decimal_max_digits', 'Decimal input should have no more than 42 digits in total', {'max_digits': 42}),
371+
('decimal_max_digits', 'Decimal input should have no more than 1 digit in total', {'max_digits': 1}),
366372
('decimal_max_places', 'Decimal input should have no more than 42 decimal places', {'decimal_places': 42}),
373+
('decimal_max_places', 'Decimal input should have no more than 1 decimal place', {'decimal_places': 1}),
367374
(
368375
'decimal_whole_digits',
369376
'Decimal input should have no more than 42 digits before the decimal point',
370377
{'whole_digits': 42},
371378
),
379+
(
380+
'decimal_whole_digits',
381+
'Decimal input should have no more than 1 digit before the decimal point',
382+
{'whole_digits': 1},
383+
),
372384
]
373385

374386

0 commit comments

Comments
 (0)