Add more tests, better error handling

This commit is contained in:
Vlasislav Kashin
2025-07-13 14:49:56 +03:00
parent ebe7d236e9
commit beae04dee8
4 changed files with 1075 additions and 82 deletions

View File

@@ -19,8 +19,8 @@ pub enum ParseError {
// NoParsingError,
#[error("Invalid country code")]
InvalidCountryCodeError, // INVALID_COUNTRY_CODE in the java version.
#[error("Not a number")]
NotANumber,
#[error("Not a number: {0}")]
NotANumber(#[from] NotANumberError),
#[error("Too short after idd")]
TooShortAfterIdd,
#[error("Too short Nsn")]
@@ -29,6 +29,14 @@ pub enum ParseError {
TooLongNsn, // TOO_LONG in the java version.
#[error("{0}")]
InvalidRegexError(#[from] ErrorInvalidRegex),
}
#[derive(Debug, PartialEq, Error)]
pub enum NotANumberError {
#[error("Number not matched a valid number pattern")]
NotMatchedValidNumberPattern,
#[error("Invalid phone context")]
InvalidPhoneContext,
#[error("{0}")]
ParseNumberAsIntError(#[from] ParseIntError),
#[error("{0}")]
@@ -43,6 +51,12 @@ pub enum ExtractNumberError {
NotANumber,
}
impl From<ExtractNumberError> for ParseError {
fn from(value: ExtractNumberError) -> Self {
NotANumberError::ExtractNumberError(value).into()
}
}
#[derive(Debug, PartialEq, Error)]
pub enum GetExampleNumberError {
#[error("Parse error: {0}")]

View File

@@ -307,7 +307,7 @@ impl PhoneNumberRegExpsAndMappings {
separator_pattern: Regex::new(&format!("[{}]+", VALID_PUNCTUATION)).unwrap(),
extn_patterns_for_matching: create_extn_pattern(false),
extn_pattern: Regex::new(&format!("(?i)(?:{})$", &extn_patterns_for_parsing)).unwrap(),
valid_phone_number_pattern: Regex::new(&format!("(?i)(?:{})(?:{})?",
valid_phone_number_pattern: Regex::new(&format!("(?i)^(?:{})(?:{})?$",
&valid_phone_number,
&extn_patterns_for_parsing
)).unwrap(),

View File

@@ -1,3 +1,18 @@
// Copyright (C) 2009 The Libphonenumber Authors
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
borrow::Cow,
cmp::max,
@@ -7,38 +22,25 @@ use std::{
use super::phone_number_regexps_and_mappings::PhoneNumberRegExpsAndMappings;
use crate::{
i18n,
interfaces::MatcherApi,
macros::owned_from_cow_or,
phonemetadata::PhoneMetadataCollection,
phonenumberutil::{
MatchType, PhoneNumberFormat, PhoneNumberType, ValidNumberLenType,
errors::NotANumberError, i18n, interfaces::MatcherApi, macros::owned_from_cow_or, phonemetadata::PhoneMetadataCollection, phonenumberutil::{
errors::{
ExtractNumberError, GetExampleNumberError, InternalLogicError,
InvalidMetadataForValidRegionError, InvalidNumberError, ParseError,
ValidationResultErr,
},
helper_constants::{
}, helper_constants::{
DEFAULT_EXTN_PREFIX, MAX_LENGTH_COUNTRY_CODE, MAX_LENGTH_FOR_NSN, MIN_LENGTH_FOR_NSN,
NANPA_COUNTRY_CODE, PLUS_SIGN, REGION_CODE_FOR_NON_GEO_ENTITY, RFC3966_EXTN_PREFIX,
RFC3966_ISDN_SUBADDRESS, RFC3966_PHONE_CONTEXT, RFC3966_PREFIX,
},
helper_functions::{
}, helper_functions::{
self, copy_core_fields_only, get_number_desc_by_type, get_supported_types_for_metadata,
is_national_number_suffix_of_the_other, load_compiled_metadata, normalize_helper,
prefix_number_with_country_calling_code, test_number_length,
test_number_length_with_unknown_type,
},
helper_types::{PhoneNumberAndCarrierCode, PhoneNumberWithCountryCodeSource},
},
proto_gen::{
}, helper_types::{PhoneNumberAndCarrierCode, PhoneNumberWithCountryCodeSource}, MatchType, PhoneNumberFormat, PhoneNumberType, ValidNumberLenType
}, proto_gen::{
phonemetadata::{NumberFormat, PhoneMetadata, PhoneNumberDesc},
phonenumber::{PhoneNumber, phone_number::CountryCodeSource},
},
regex_based_matcher::RegexBasedMatcher,
regex_util::{RegexConsume, RegexFullMatch},
regexp_cache::ErrorInvalidRegex,
string_util::strip_cow_prefix,
phonenumber::{phone_number::CountryCodeSource, PhoneNumber},
}, regex_based_matcher::RegexBasedMatcher, regex_util::{RegexConsume, RegexFullMatch}, regexp_cache::ErrorInvalidRegex, string_util::strip_cow_prefix
};
use dec_from_char::DecimalExtended;
@@ -1516,7 +1518,7 @@ impl PhoneNumberUtil {
Self::extract_phone_context(number_to_parse, index_of_phone_context);
if !self.is_phone_context_valid(phone_context) {
trace!("The phone-context value for phone number {number_to_parse} is invalid.");
return Err(ParseError::NotANumber);
return Err(NotANumberError::InvalidPhoneContext.into());
}
// If the phone context contains a phone number prefix, we need to capture
// it, whereas domains will be ignored.
@@ -1628,7 +1630,8 @@ impl PhoneNumberUtil {
return Ok(self
.reg_exps
.capture_up_to_second_number_start_pattern
.find(&extracted_number)
.captures(&extracted_number)
.and_then(| c | c.get(1))
.map(move |m| m.as_str())
.unwrap_or(extracted_number));
}
@@ -1736,7 +1739,7 @@ impl PhoneNumberUtil {
let national_number = self.build_national_number_for_parsing(number_to_parse)?;
if !self.is_viable_phone_number(&national_number) {
trace!("The string supplied did not seem to be a phone number '{national_number}'.");
return Err(ParseError::NotANumber);
return Err(ParseError::NotANumber(NotANumberError::NotMatchedValidNumberPattern));
}
if check_region && !self.check_region_for_parsing(&national_number, default_region) {
@@ -1808,22 +1811,19 @@ impl PhoneNumberUtil {
return Err(ParseError::TooShortNsn.into());
}
if let Some(country_metadata) = country_metadata {
let potential_national_number = normalized_national_number.clone();
let mut potential_national_number = normalized_national_number.clone();
let phone_number_and_carrier_code = self.maybe_strip_national_prefix_and_carrier_code(
let (phone_number, carrier_code) = self.maybe_strip_national_prefix_and_carrier_code(
country_metadata,
&potential_national_number,
)?;
let carrier_code = phone_number_and_carrier_code
.as_ref()
.and_then(|p| p.carrier_code.map(|c| c.to_string()));
let potential_national_number =
if let Some(phone_number_and_carrier_code) = phone_number_and_carrier_code {
Cow::Owned(phone_number_and_carrier_code.phone_number.to_string())
} else {
potential_national_number
};
let carrier_code = carrier_code
.map(|c| c.to_string());
if potential_national_number != phone_number {
potential_national_number = Cow::Owned(phone_number.into_owned());
}
// We require that the NSN remaining after stripping the national prefix
// and carrier code be long enough to be a possible length for the region.
@@ -1864,15 +1864,16 @@ impl PhoneNumberUtil {
temp_number.set_country_code(country_code);
if let Some(zeroes_count) =
Self::get_italian_leading_zeros_for_phone_number(&normalized_national_number)
{
Self::get_italian_leading_zeros_for_phone_number(&normalized_national_number) {
temp_number.set_italian_leading_zero(true);
temp_number.set_number_of_leading_zeros(zeroes_count as i32);
if zeroes_count > 1 {
temp_number.set_number_of_leading_zeros(zeroes_count as i32);
}
}
let number_as_int = u64::from_str_radix(&normalized_national_number, 10);
match number_as_int {
Ok(number_as_int) => temp_number.set_national_number(number_as_int),
Err(err) => return Err(ParseError::ParseNumberAsIntError(err).into()),
Err(err) => return Err(NotANumberError::ParseNumberAsIntError(err).into()),
}
return Ok(temp_number);
}
@@ -2488,12 +2489,12 @@ impl PhoneNumberUtil {
&self,
metadata: &PhoneMetadata,
phone_number: &'a str,
) -> RegexResult<Option<PhoneNumberAndCarrierCode<'a>>> {
) -> RegexResult<(Cow<'a, str>, Option<&'a str>)> {
let possible_national_prefix = metadata.national_prefix_for_parsing();
if phone_number.is_empty() || possible_national_prefix.is_empty() {
// Early return for numbers of zero length or with no national prefix
// possible.
return Ok(None);
return Ok((phone_number.into(), None));
}
let general_desc = &metadata.general_desc;
// Check if the original number is viable.
@@ -2530,17 +2531,17 @@ impl PhoneNumberUtil {
// have been some part of the prefix that we captured.
// We make the transformation and check that the resultant number is still
// viable. If so, replace the number and return.
// Rust note: There is no known transform rules containing $\d\d
// But if any appears this should be handled with {} braces: {$\d}\d
let replaced_number =
possible_national_prefix_pattern.replace(&phone_number, transform_rule);
if is_viable_original_number
&& !helper_functions::is_match(&self.matcher_api, &replaced_number, general_desc)
{
return Ok(None);
return Ok((phone_number.into(), None));
}
return Ok(Some(PhoneNumberAndCarrierCode::new(
carrier_code_temp,
replaced_number,
)));
return Ok((replaced_number, carrier_code_temp));
} else if let Some(matched) = captures.and_then(|c| c.get(0)) {
trace!(
"Parsed the first digits as a national prefix for number '{}'.",
@@ -2551,17 +2552,22 @@ impl PhoneNumberUtil {
// transformation is necessary, and we just remove the national prefix.
let stripped_number = &phone_number[matched.end()..];
if is_viable_original_number
&& !helper_functions::is_match(&self.matcher_api, stripped_number, general_desc)
{
return Ok(None);
&& !helper_functions::is_match(&self.matcher_api, stripped_number, general_desc) {
return Ok((phone_number.into(), None));
}
return Ok(Some(PhoneNumberAndCarrierCode::new_phone(stripped_number)));
let carrier_code_temp = if let Some(capture) = first_capture {
Some(capture.as_str())
} else {
None
};
return Ok((stripped_number.into(), carrier_code_temp));
}
trace!(
"The first digits did not match the national prefix for number '{}'.",
phone_number
);
Ok(None)
Ok((phone_number.into(), None))
}
// A helper function to set the values related to leading zeros in a

File diff suppressed because it is too large Load Diff