diff --git a/src/errors/types.rs b/src/errors/types.rs index e31307e2b..5c3fc1a7c 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -445,8 +445,8 @@ macro_rules! to_string_render { }; } -fn plural_s(value: usize) -> &'static str { - if value == 1 { +fn plural_s + PartialEq>(value: T) -> &'static str { + if value == 1.into() { "" } else { "s" @@ -494,8 +494,8 @@ impl ErrorType { Self::StringType {..} => "Input should be a valid string", Self::StringSubType {..} => "Input should be a string, not an instance of a subclass of str", Self::StringUnicode {..} => "Input should be a valid string, unable to parse raw data as a unicode string", - Self::StringTooShort {..} => "String should have at least {min_length} characters", - Self::StringTooLong {..} => "String should have at most {max_length} characters", + Self::StringTooShort {..} => "String should have at least {min_length} character{expected_plural}", + Self::StringTooLong {..} => "String should have at most {max_length} character{expected_plural}", Self::StringPatternMismatch {..} => "String should match pattern '{pattern}'", Self::Enum {..} => "Input should be {expected}", Self::DictType {..} => "Input should be a valid dictionary", @@ -512,8 +512,8 @@ impl ErrorType { Self::FloatType {..} => "Input should be a valid number", Self::FloatParsing {..} => "Input should be a valid number, unable to parse string as a number", Self::BytesType {..} => "Input should be a valid bytes", - Self::BytesTooShort {..} => "Data should have at least {min_length} bytes", - Self::BytesTooLong {..} => "Data should have at most {max_length} bytes", + Self::BytesTooShort {..} => "Data should have at least {min_length} byte{expected_plural}", + Self::BytesTooLong {..} => "Data should have at most {max_length} byte{expected_plural}", Self::ValueError {..} => "Value error, {error}", Self::AssertionError {..} => "Assertion failed, {error}", Self::CustomError {..} => "", // custom errors are handled separately @@ -552,16 +552,16 @@ impl ErrorType { Self::UrlType {..} => "URL input should be a string or URL", Self::UrlParsing {..} => "Input should be a valid URL, {error}", Self::UrlSyntaxViolation {..} => "Input violated strict URL syntax rules, {error}", - Self::UrlTooLong {..} => "URL should have at most {max_length} characters", + Self::UrlTooLong {..} => "URL should have at most {max_length} character{expected_plural}", Self::UrlScheme {..} => "URL scheme should be {expected_schemes}", Self::UuidType {..} => "UUID input should be a string, bytes or UUID object", Self::UuidParsing {..} => "Input should be a valid UUID, {error}", Self::UuidVersion {..} => "UUID version {expected_version} expected", Self::DecimalType {..} => "Decimal input should be an integer, float, string or Decimal object", Self::DecimalParsing {..} => "Input should be a valid decimal", - Self::DecimalMaxDigits {..} => "Decimal input should have no more than {max_digits} digits in total", - Self::DecimalMaxPlaces {..} => "Decimal input should have no more than {decimal_places} decimal places", - Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digits before the decimal point", + Self::DecimalMaxDigits {..} => "Decimal input should have no more than {max_digits} digit{expected_plural} in total", + Self::DecimalMaxPlaces {..} => "Decimal input should have no more than {decimal_places} decimal place{expected_plural}", + Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point", } } @@ -643,13 +643,25 @@ impl ErrorType { to_string_render!(tmpl, field_type, max_length, actual_length, expected_plural,) } Self::IterationError { error, .. } => render!(tmpl, error), - Self::StringTooShort { min_length, .. } => to_string_render!(tmpl, min_length), - Self::StringTooLong { max_length, .. } => to_string_render!(tmpl, max_length), + Self::StringTooShort { min_length, .. } => { + let expected_plural = plural_s(*min_length); + to_string_render!(tmpl, min_length, expected_plural) + } + Self::StringTooLong { max_length, .. } => { + let expected_plural = plural_s(*max_length); + to_string_render!(tmpl, max_length, expected_plural) + } Self::StringPatternMismatch { pattern, .. } => render!(tmpl, pattern), Self::Enum { expected, .. } => to_string_render!(tmpl, expected), Self::MappingType { error, .. } => render!(tmpl, error), - Self::BytesTooShort { min_length, .. } => to_string_render!(tmpl, min_length), - Self::BytesTooLong { max_length, .. } => to_string_render!(tmpl, max_length), + Self::BytesTooShort { min_length, .. } => { + let expected_plural = plural_s(*min_length); + to_string_render!(tmpl, min_length, expected_plural) + } + Self::BytesTooLong { max_length, .. } => { + let expected_plural = plural_s(*max_length); + to_string_render!(tmpl, max_length, expected_plural) + } Self::ValueError { error, .. } => { let error = &error .as_ref() @@ -688,13 +700,25 @@ impl ErrorType { Self::UnionTagNotFound { discriminator, .. } => render!(tmpl, discriminator), Self::UrlParsing { error, .. } => render!(tmpl, error), Self::UrlSyntaxViolation { error, .. } => render!(tmpl, error), - Self::UrlTooLong { max_length, .. } => to_string_render!(tmpl, max_length), + Self::UrlTooLong { max_length, .. } => { + let expected_plural = plural_s(*max_length); + to_string_render!(tmpl, max_length, expected_plural) + } Self::UrlScheme { expected_schemes, .. } => render!(tmpl, expected_schemes), Self::UuidParsing { error, .. } => render!(tmpl, error), Self::UuidVersion { expected_version, .. } => to_string_render!(tmpl, expected_version), - Self::DecimalMaxDigits { max_digits, .. } => to_string_render!(tmpl, max_digits), - Self::DecimalMaxPlaces { decimal_places, .. } => to_string_render!(tmpl, decimal_places), - Self::DecimalWholeDigits { whole_digits, .. } => to_string_render!(tmpl, whole_digits), + Self::DecimalMaxDigits { max_digits, .. } => { + let expected_plural = plural_s(*max_digits); + to_string_render!(tmpl, max_digits, expected_plural) + } + Self::DecimalMaxPlaces { decimal_places, .. } => { + let expected_plural = plural_s(*decimal_places); + to_string_render!(tmpl, decimal_places, expected_plural) + } + Self::DecimalWholeDigits { whole_digits, .. } => { + let expected_plural = plural_s(*whole_digits); + to_string_render!(tmpl, whole_digits, expected_plural) + } _ => Ok(tmpl.to_string()), } } diff --git a/tests/test_errors.py b/tests/test_errors.py index 293880977..3683150ec 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -289,7 +289,9 @@ def f(input_value, info): ('string_unicode', 'Input should be a valid string, unable to parse raw data as a unicode string', None), ('string_pattern_mismatch', "String should match pattern 'foo'", {'pattern': 'foo'}), ('string_too_short', 'String should have at least 42 characters', {'min_length': 42}), + ('string_too_short', 'String should have at least 1 character', {'min_length': 1}), ('string_too_long', 'String should have at most 42 characters', {'max_length': 42}), + ('string_too_long', 'String should have at most 1 character', {'max_length': 1}), ('dict_type', 'Input should be a valid dictionary', None), ('mapping_type', 'Input should be a valid mapping, error: foobar', {'error': 'foobar'}), ('iterable_type', 'Input should be iterable', None), @@ -312,7 +314,9 @@ def f(input_value, info): ('float_parsing', 'Input should be a valid number, unable to parse string as a number', None), ('bytes_type', 'Input should be a valid bytes', None), ('bytes_too_short', 'Data should have at least 42 bytes', {'min_length': 42}), + ('bytes_too_short', 'Data should have at least 1 byte', {'min_length': 1}), ('bytes_too_long', 'Data should have at most 42 bytes', {'max_length': 42}), + ('bytes_too_long', 'Data should have at most 1 byte', {'max_length': 1}), ('value_error', 'Value error, foobar', {'error': ValueError('foobar')}), ('assertion_error', 'Assertion failed, foobar', {'error': AssertionError('foobar')}), ('literal_error', 'Input should be foo', {'expected': 'foo'}), @@ -356,6 +360,7 @@ def f(input_value, info): ('url_parsing', 'Input should be a valid URL, Foobar', {'error': 'Foobar'}), ('url_syntax_violation', 'Input violated strict URL syntax rules, Foobar', {'error': 'Foobar'}), ('url_too_long', 'URL should have at most 42 characters', {'max_length': 42}), + ('url_too_long', 'URL should have at most 1 character', {'max_length': 1}), ('url_scheme', 'URL scheme should be "foo", "bar" or "spam"', {'expected_schemes': '"foo", "bar" or "spam"'}), ('uuid_type', 'UUID input should be a string, bytes or UUID object', None), ('uuid_parsing', 'Input should be a valid UUID, Foobar', {'error': 'Foobar'}), @@ -363,12 +368,19 @@ def f(input_value, info): ('decimal_type', 'Decimal input should be an integer, float, string or Decimal object', None), ('decimal_parsing', 'Input should be a valid decimal', None), ('decimal_max_digits', 'Decimal input should have no more than 42 digits in total', {'max_digits': 42}), + ('decimal_max_digits', 'Decimal input should have no more than 1 digit in total', {'max_digits': 1}), ('decimal_max_places', 'Decimal input should have no more than 42 decimal places', {'decimal_places': 42}), + ('decimal_max_places', 'Decimal input should have no more than 1 decimal place', {'decimal_places': 1}), ( 'decimal_whole_digits', 'Decimal input should have no more than 42 digits before the decimal point', {'whole_digits': 42}, ), + ( + 'decimal_whole_digits', + 'Decimal input should have no more than 1 digit before the decimal point', + {'whole_digits': 1}, + ), ]