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

View File

@@ -307,7 +307,7 @@ impl PhoneNumberRegExpsAndMappings {
separator_pattern: Regex::new(&format!("[{}]+", VALID_PUNCTUATION)).unwrap(), separator_pattern: Regex::new(&format!("[{}]+", VALID_PUNCTUATION)).unwrap(),
extn_patterns_for_matching: create_extn_pattern(false), extn_patterns_for_matching: create_extn_pattern(false),
extn_pattern: Regex::new(&format!("(?i)(?:{})$", &extn_patterns_for_parsing)).unwrap(), 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, &valid_phone_number,
&extn_patterns_for_parsing &extn_patterns_for_parsing
)).unwrap(), )).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::{ use std::{
borrow::Cow, borrow::Cow,
cmp::max, cmp::max,
@@ -7,38 +22,25 @@ use std::{
use super::phone_number_regexps_and_mappings::PhoneNumberRegExpsAndMappings; use super::phone_number_regexps_and_mappings::PhoneNumberRegExpsAndMappings;
use crate::{ use crate::{
i18n, errors::NotANumberError, i18n, interfaces::MatcherApi, macros::owned_from_cow_or, phonemetadata::PhoneMetadataCollection, phonenumberutil::{
interfaces::MatcherApi,
macros::owned_from_cow_or,
phonemetadata::PhoneMetadataCollection,
phonenumberutil::{
MatchType, PhoneNumberFormat, PhoneNumberType, ValidNumberLenType,
errors::{ errors::{
ExtractNumberError, GetExampleNumberError, InternalLogicError, ExtractNumberError, GetExampleNumberError, InternalLogicError,
InvalidMetadataForValidRegionError, InvalidNumberError, ParseError, InvalidMetadataForValidRegionError, InvalidNumberError, ParseError,
ValidationResultErr, ValidationResultErr,
}, }, helper_constants::{
helper_constants::{
DEFAULT_EXTN_PREFIX, MAX_LENGTH_COUNTRY_CODE, MAX_LENGTH_FOR_NSN, MIN_LENGTH_FOR_NSN, 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, NANPA_COUNTRY_CODE, PLUS_SIGN, REGION_CODE_FOR_NON_GEO_ENTITY, RFC3966_EXTN_PREFIX,
RFC3966_ISDN_SUBADDRESS, RFC3966_PHONE_CONTEXT, RFC3966_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, 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, is_national_number_suffix_of_the_other, load_compiled_metadata, normalize_helper,
prefix_number_with_country_calling_code, test_number_length, prefix_number_with_country_calling_code, test_number_length,
test_number_length_with_unknown_type, test_number_length_with_unknown_type,
}, }, helper_types::{PhoneNumberAndCarrierCode, PhoneNumberWithCountryCodeSource}, MatchType, PhoneNumberFormat, PhoneNumberType, ValidNumberLenType
helper_types::{PhoneNumberAndCarrierCode, PhoneNumberWithCountryCodeSource}, }, proto_gen::{
},
proto_gen::{
phonemetadata::{NumberFormat, PhoneMetadata, PhoneNumberDesc}, phonemetadata::{NumberFormat, PhoneMetadata, PhoneNumberDesc},
phonenumber::{PhoneNumber, phone_number::CountryCodeSource}, phonenumber::{phone_number::CountryCodeSource, PhoneNumber},
}, }, regex_based_matcher::RegexBasedMatcher, regex_util::{RegexConsume, RegexFullMatch}, regexp_cache::ErrorInvalidRegex, string_util::strip_cow_prefix
regex_based_matcher::RegexBasedMatcher,
regex_util::{RegexConsume, RegexFullMatch},
regexp_cache::ErrorInvalidRegex,
string_util::strip_cow_prefix,
}; };
use dec_from_char::DecimalExtended; use dec_from_char::DecimalExtended;
@@ -1516,7 +1518,7 @@ impl PhoneNumberUtil {
Self::extract_phone_context(number_to_parse, index_of_phone_context); Self::extract_phone_context(number_to_parse, index_of_phone_context);
if !self.is_phone_context_valid(phone_context) { if !self.is_phone_context_valid(phone_context) {
trace!("The phone-context value for phone number {number_to_parse} is invalid."); 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 // If the phone context contains a phone number prefix, we need to capture
// it, whereas domains will be ignored. // it, whereas domains will be ignored.
@@ -1628,7 +1630,8 @@ impl PhoneNumberUtil {
return Ok(self return Ok(self
.reg_exps .reg_exps
.capture_up_to_second_number_start_pattern .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()) .map(move |m| m.as_str())
.unwrap_or(extracted_number)); .unwrap_or(extracted_number));
} }
@@ -1736,7 +1739,7 @@ impl PhoneNumberUtil {
let national_number = self.build_national_number_for_parsing(number_to_parse)?; let national_number = self.build_national_number_for_parsing(number_to_parse)?;
if !self.is_viable_phone_number(&national_number) { if !self.is_viable_phone_number(&national_number) {
trace!("The string supplied did not seem to be a 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) { if check_region && !self.check_region_for_parsing(&national_number, default_region) {
@@ -1808,22 +1811,19 @@ impl PhoneNumberUtil {
return Err(ParseError::TooShortNsn.into()); return Err(ParseError::TooShortNsn.into());
} }
if let Some(country_metadata) = country_metadata { 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, country_metadata,
&potential_national_number, &potential_national_number,
)?; )?;
let carrier_code = phone_number_and_carrier_code let carrier_code = carrier_code
.as_ref() .map(|c| c.to_string());
.and_then(|p| p.carrier_code.map(|c| c.to_string()));
let potential_national_number = if potential_national_number != phone_number {
if let Some(phone_number_and_carrier_code) = phone_number_and_carrier_code { potential_national_number = Cow::Owned(phone_number.into_owned());
Cow::Owned(phone_number_and_carrier_code.phone_number.to_string()) }
} else {
potential_national_number
};
// We require that the NSN remaining after stripping the national prefix // 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. // 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); temp_number.set_country_code(country_code);
if let Some(zeroes_count) = 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_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); let number_as_int = u64::from_str_radix(&normalized_national_number, 10);
match number_as_int { match number_as_int {
Ok(number_as_int) => temp_number.set_national_number(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); return Ok(temp_number);
} }
@@ -2488,12 +2489,12 @@ impl PhoneNumberUtil {
&self, &self,
metadata: &PhoneMetadata, metadata: &PhoneMetadata,
phone_number: &'a str, phone_number: &'a str,
) -> RegexResult<Option<PhoneNumberAndCarrierCode<'a>>> { ) -> RegexResult<(Cow<'a, str>, Option<&'a str>)> {
let possible_national_prefix = metadata.national_prefix_for_parsing(); let possible_national_prefix = metadata.national_prefix_for_parsing();
if phone_number.is_empty() || possible_national_prefix.is_empty() { if phone_number.is_empty() || possible_national_prefix.is_empty() {
// Early return for numbers of zero length or with no national prefix // Early return for numbers of zero length or with no national prefix
// possible. // possible.
return Ok(None); return Ok((phone_number.into(), None));
} }
let general_desc = &metadata.general_desc; let general_desc = &metadata.general_desc;
// Check if the original number is viable. // Check if the original number is viable.
@@ -2530,17 +2531,17 @@ impl PhoneNumberUtil {
// have been some part of the prefix that we captured. // have been some part of the prefix that we captured.
// We make the transformation and check that the resultant number is still // We make the transformation and check that the resultant number is still
// viable. If so, replace the number and return. // 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 = let replaced_number =
possible_national_prefix_pattern.replace(&phone_number, transform_rule); possible_national_prefix_pattern.replace(&phone_number, transform_rule);
if is_viable_original_number if is_viable_original_number
&& !helper_functions::is_match(&self.matcher_api, &replaced_number, general_desc) && !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( return Ok((replaced_number, carrier_code_temp));
carrier_code_temp,
replaced_number,
)));
} else if let Some(matched) = captures.and_then(|c| c.get(0)) { } else if let Some(matched) = captures.and_then(|c| c.get(0)) {
trace!( trace!(
"Parsed the first digits as a national prefix for number '{}'.", "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. // transformation is necessary, and we just remove the national prefix.
let stripped_number = &phone_number[matched.end()..]; let stripped_number = &phone_number[matched.end()..];
if is_viable_original_number if is_viable_original_number
&& !helper_functions::is_match(&self.matcher_api, stripped_number, general_desc) && !helper_functions::is_match(&self.matcher_api, stripped_number, general_desc) {
{ return Ok((phone_number.into(), None));
return Ok(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!( trace!(
"The first digits did not match the national prefix for number '{}'.", "The first digits did not match the national prefix for number '{}'.",
phone_number phone_number
); );
Ok(None) Ok((phone_number.into(), None))
} }
// A helper function to set the values related to leading zeros in a // A helper function to set the values related to leading zeros in a

File diff suppressed because it is too large Load Diff