diff --git a/Cargo.toml b/Cargo.toml index 520ade3..574eb1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,10 @@ include = ["src/**/*.rs", "Cargo.toml", "LICENSE", "README.md"] [dependencies] -urlencoding = { version = "2.1.3", optional = true } regex = "1.10.2" +urlencoding = { version = "2.1.3", optional = true } serde = { version = "1.0.193", features = ["derive"], optional = true } +thiserror = { version = "1.0.48", optional = true } # Edit `Makefile` and `src/lib.src` after making changes in this section: @@ -43,13 +44,17 @@ commas = [] digits = [] find-capital-by-province = ["to-persian-chars"] is-persian = [] -national-id = [] +national-id = ["dep:thiserror"] remove-ordinal-suffix = [] to-persian-chars = [] url-fix = ["dep:urlencoding"] verity-card-number = [] -phone-number = [] +phone-number = ["dep:thiserror"] serde = ["dep:serde"] [package.metadata.docs.rs] all-features = true + +[dev-dependencies] +# To test `serde` feature expectaions: +serde_json = "1.0.107" diff --git a/Makefile b/Makefile index 4cc6773..7a1fcef 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ build: full default add-ordinal-suffix commas digits find-capital-by-province is check: clippy lint test: - cargo test --all-features -- --nocapture + RUST_BACKTRACE=1 cargo test --all-features -- --nocapture docs: cargo doc --all-features @@ -61,6 +61,9 @@ national-id: @ echo "" cargo build --no-default-features --features=national-id @ ls -sh target/debug/*.rlib + cargo build --no-default-features --features="national-id serde" + @ ls -sh target/debug/*.rlib + remove-ordinal-suffix: @ echo "" diff --git a/src/national_id/mod.rs b/src/national_id/mod.rs index a2b7fd7..ff90fac 100644 --- a/src/national_id/mod.rs +++ b/src/national_id/mod.rs @@ -1,66 +1,145 @@ -/// Validation of Iranian National Number(code-e Melli) -pub fn verify_iranian_national_id(code: S) -> bool -where - S: Into, -{ - let code: String = code.into(); +//! Iranian National Number utils (`national-id` Cargo feature). +//! +//! #### Example +//! ```rust +//! use rust_persian_tools::national_id::{NationalIdError, verify_iranian_national_id}; +//! +//! assert!(verify_iranian_national_id("11537027").is_ok()); +//! assert!(verify_iranian_national_id("0076229645").is_ok()); +//! assert!(verify_iranian_national_id("1583250689").is_ok()); +//! +//! assert_eq!( +//! verify_iranian_national_id("12345"), +//! Err(NationalIdError::Length(5)), +//! ); +//! assert_eq!( +//! verify_iranian_national_id("9999999999"), +//! Err(NationalIdError::Invalid) +//! ); +//! ``` +//! +//! #### [serde] Integration +//! ##### National Number +//! ```rust +//! use rust_persian_tools::national_id::serde::national_id_de; +//! +//! #[derive(Debug, PartialEq, serde::Deserialize)] +//! struct MyStruct { +//! #[serde(deserialize_with = "national_id_de")] +//! id: String, +//! } +//! +//! let json_str = "{\"id\": \"0076229645\"}"; +//! let my_struct: MyStruct = serde_json::from_str(json_str).unwrap(); +//! assert_eq!(my_struct, MyStruct{id: "0076229645".to_string()}); +//! +//! let json_str_invalid = "{\"id\": \"ZeroOneTwo\"}"; +//! assert!(serde_json::from_str::(json_str_invalid).is_err()); +//! assert_eq!( +//! serde_json::from_str::(json_str_invalid).err().unwrap().to_string(), +//! "Could not convert National Number to numeric at line 1 column 19".to_string(), +//! ); +//! ``` +//! ##### Option\ +//! ```rust +//! use rust_persian_tools::national_id::serde::national_id_option_de; +//! +//! #[derive(Debug, PartialEq, serde::Deserialize)] +//! struct MyStruct { +//! #[serde(default, deserialize_with = "national_id_option_de")] +//! id: Option, +//! } +//! +//! let json_str = "{}"; +//! let my_struct: MyStruct = serde_json::from_str(json_str).unwrap(); +//! assert_eq!(my_struct, MyStruct{id: None}); +//! let json_str = "{\"id\": \"0076229645\"}"; +//! let my_struct: MyStruct = serde_json::from_str(json_str).unwrap(); +//! assert_eq!(my_struct, MyStruct{id: Some("0076229645".to_string())}); +//! ``` - let code_length = code.len(); - if !(8..=10).contains(&code_length) { - return false; - } +use std::num::ParseIntError; - if code.parse::().is_err() { - return false; - } +#[cfg(feature = "serde")] +pub mod serde; - if code == "0000000000" { - return false; - } - if code == "1111111111" { - return false; - } - if code == "2222222222" { - return false; - } - if code == "3333333333" { - return false; - } - if code == "4444444444" { - return false; - } - if code == "5555555555" { - return false; - } - if code == "6666666666" { - return false; - } - if code == "7777777777" { - return false; - } - if code == "8888888888" { - return false; - } - if code == "9999999999" { - return false; - } +/// Possible errors during validation of Iranian National Number. +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum NationalIdError { + /// If input length is invalid. + #[error("Invalid length {0} for National Number")] + Length(usize), + /// If input is not a [u64] number. + #[error("Could not convert National Number to numeric")] + NumericConvert { source: ParseIntError }, + /// Other checks. + #[error("National Number is invalid")] + Invalid, +} + +/// Validation of Iranian National Number (code-e Melli). +/// +/// ## Examples +/// ```rust +/// use rust_persian_tools::national_id::{NationalIdError, verify_iranian_national_id}; +/// +/// assert!(verify_iranian_national_id("68415941").is_ok()); +/// assert!(verify_iranian_national_id("0200203241").is_ok()); +/// assert!(verify_iranian_national_id("0067749828").is_ok()); +/// +/// assert_eq!( +/// verify_iranian_national_id("0"), +/// Err(NationalIdError::Length(1)), +/// ); +/// assert_eq!( +/// verify_iranian_national_id("1230000000"), +/// Err(NationalIdError::Invalid) +/// ); +/// ``` +pub fn verify_iranian_national_id(code: impl AsRef) -> Result<(), NationalIdError> { + let code_str = code.as_ref(); - let code = ("00".to_owned() + &code)[code_length + 2 - 10..].to_string(); - if str::parse::(&code[3..9]).unwrap() == 0 { - return false; + let length = code_str.len(); + if !((8..=10).contains(&length)) { + return Err(NationalIdError::Length(length)); } - let last_number = - str::parse::(code.chars().last().unwrap().to_string().as_str()).unwrap(); + let code_u64 = code_str + .parse::() + .map_err(|source| NationalIdError::NumericConvert { source })?; - let mut sum = 0; - for i in 0..9 { - sum += str::parse::(&code[i..i + 1]).unwrap() * (10 - i); + if length == 10 && (code_u64 == 0 || are_digits_the_same(code_u64)) { + return Err(NationalIdError::Invalid); } + let code_str = &("00".to_owned() + code_str)[length + 2 - 10..]; + if code_str[3..9].parse::().unwrap() == 0 { + return Err(NationalIdError::Invalid); + } + + let mut sum = (0usize..9).fold(0, |sum, i| { + sum + code_str[i..i + 1].parse::().unwrap() * (10 - i) + }); sum %= 11; + let last_number = (code_u64 % 10) as usize; + if (sum < 2 && last_number == sum) || (sum >= 2 && last_number == 11 - sum) { + Ok(()) + } else { + Err(NationalIdError::Invalid) + } +} - (sum < 2 && last_number == sum) || (sum >= 2 && last_number == 11 - sum) +#[inline] +fn are_digits_the_same(mut number: u64) -> bool { + let last = number % 10; + while number != 0 { + let current = number % 10; + number /= 10; + if current != last { + return false; + } + } + true } #[cfg(test)] @@ -69,39 +148,80 @@ mod verify_iranian_national_id_tests { #[test] fn check_falsy() { - assert_eq!(verify_iranian_national_id(""), false); - assert_eq!(verify_iranian_national_id("12345"), false); - assert_eq!(verify_iranian_national_id("0"), false); - assert_eq!(verify_iranian_national_id("000000"), false); - assert_eq!(verify_iranian_national_id("12300000"), false); - assert_eq!(verify_iranian_national_id("123000000"), false); - assert_eq!(verify_iranian_national_id("1230000000"), false); - assert_eq!(verify_iranian_national_id("0000000000"), false); - assert_eq!(verify_iranian_national_id("4444444444"), false); - assert_eq!(verify_iranian_national_id("9999999999"), false); - assert_eq!(verify_iranian_national_id("0684159415"), false); - assert_eq!(verify_iranian_national_id("1111111111"), false); - assert_eq!(verify_iranian_national_id("079041a904"), false); // this is not in typescript version + assert_eq!( + verify_iranian_national_id(""), + Err(NationalIdError::Length(0)) + ); + assert_eq!( + verify_iranian_national_id("12345"), + Err(NationalIdError::Length(5)) + ); + assert_eq!( + verify_iranian_national_id("0"), + Err(NationalIdError::Length(1)) + ); + assert_eq!( + verify_iranian_national_id("000000"), + Err(NationalIdError::Length(6)) + ); + assert_eq!( + verify_iranian_national_id("12300000"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("123000000"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("1230000000"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("0000000000"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("4444444444"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("9999999999"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("0684159415"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("1111111111"), + Err(NationalIdError::Invalid) + ); + assert_eq!( + verify_iranian_national_id("079041a904"), + Err(NationalIdError::NumericConvert { + source: "079041a904".parse::().err().unwrap() + }) + ); // this is not in typescript version } #[test] fn check_truly() { - assert_eq!(verify_iranian_national_id("11537027"), true); - assert_eq!(verify_iranian_national_id("787833770"), true); - assert_eq!(verify_iranian_national_id("1583250689"), true); - assert_eq!(verify_iranian_national_id("0499370899"), true); - assert_eq!(verify_iranian_national_id("0790419904"), true); - assert_eq!(verify_iranian_national_id("0084575948"), true); - assert_eq!(verify_iranian_national_id("0963695398"), true); - assert_eq!(verify_iranian_national_id("0684159414"), true); - assert_eq!(verify_iranian_national_id("0067749828"), true); - assert_eq!(verify_iranian_national_id("0650451252"), true); - assert_eq!(verify_iranian_national_id("4032152314"), true); - assert_eq!(verify_iranian_national_id("0076229645"), true); - assert_eq!(verify_iranian_national_id("4271467685"), true); - assert_eq!(verify_iranian_national_id("0200203241"), true); - assert_eq!(verify_iranian_national_id("068415941"), true); - assert_eq!(verify_iranian_national_id("68415941"), true); - assert_eq!(verify_iranian_national_id("787833770"), true); + assert_eq!(verify_iranian_national_id("11537027"), Ok(())); + assert_eq!(verify_iranian_national_id("787833770"), Ok(())); + assert_eq!(verify_iranian_national_id("1583250689"), Ok(())); + assert_eq!(verify_iranian_national_id("0499370899"), Ok(())); + assert_eq!(verify_iranian_national_id("0790419904"), Ok(())); + assert_eq!(verify_iranian_national_id("0084575948"), Ok(())); + assert_eq!(verify_iranian_national_id("0963695398"), Ok(())); + assert_eq!(verify_iranian_national_id("0684159414"), Ok(())); + assert_eq!(verify_iranian_national_id("0067749828"), Ok(())); + assert_eq!(verify_iranian_national_id("0650451252"), Ok(())); + assert_eq!(verify_iranian_national_id("4032152314"), Ok(())); + assert_eq!(verify_iranian_national_id("0076229645"), Ok(())); + assert_eq!(verify_iranian_national_id("4271467685"), Ok(())); + assert_eq!(verify_iranian_national_id("0200203241"), Ok(())); + assert_eq!(verify_iranian_national_id("068415941"), Ok(())); + assert_eq!(verify_iranian_national_id("68415941"), Ok(())); + assert_eq!(verify_iranian_national_id("787833770"), Ok(())); } } diff --git a/src/national_id/serde.rs b/src/national_id/serde.rs new file mode 100644 index 0000000..2be7235 --- /dev/null +++ b/src/national_id/serde.rs @@ -0,0 +1,153 @@ +//! [serde] helpers to deserialize Iranian National Number. Enabled if `serde` Cargo feature is enabled. + +use crate::national_id::verify_iranian_national_id; +use serde::Deserializer; + +struct NationalId; +struct NationalIdOption; + +impl<'de> serde::de::Visitor<'de> for NationalId { + type Value = String; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("expecting Iranian national-id, e.g. 0076229645") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + verify_iranian_national_id(s) + .map(|_| s.to_string()) + .map_err(serde::de::Error::custom) + } +} + +impl<'de> serde::de::Visitor<'de> for NationalIdOption { + type Value = Option; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("expecting Iranian national-id, e.g. 0076229645") + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + fn visit_some(self, d: D) -> Result + where + D: Deserializer<'de>, + { + use serde::Deserialize; + let s: Option = Option::deserialize(d)?; + if let Some(s) = s { + verify_iranian_national_id(&s) + .map(|_| Some(s)) + .map_err(serde::de::Error::custom) + } else { + Ok(None) + } + } +} + +/// Deserializes Iranian National Number in [serde]. +/// +/// For more info see [crate::national_id] module example. +pub fn national_id_de<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(NationalId) +} + +/// Deserializes Iranian National Number (if exists) in [serde]. +/// +/// For more info see [crate::national_id] module example. +pub fn national_id_option_de<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_option(NationalIdOption) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::national_id::NationalIdError; + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct FooStr { + id: String, + } + + #[derive(Debug, PartialEq, Deserialize)] + struct FooNationalId { + #[serde(deserialize_with = "national_id_de")] + id: String, + } + + #[derive(Debug, PartialEq, Deserialize)] + struct FooOptionStr { + #[serde(default)] + id: Option, + } + + #[derive(Debug, PartialEq, Deserialize)] + struct FooOptionNationalId { + #[serde(default, deserialize_with = "national_id_option_de")] + id: Option, + } + + #[test] + fn de() { + // Valid: + let json_str = "{\"id\": \"0076229645\"}"; + assert!(serde_json::from_str::(json_str).is_ok()); + assert!(serde_json::from_str::(json_str).is_ok()); + assert!(serde_json::from_str::(json_str).is_ok()); + assert_eq!( + serde_json::from_str::(json_str).unwrap(), + FooNationalId { + id: "0076229645".to_string() + } + ); + assert!(serde_json::from_str::(json_str).is_ok()); + assert_eq!( + serde_json::from_str::(json_str).unwrap(), + FooOptionNationalId { + id: Some("0076229645".to_string()) + } + ); + + // Invalid: + let json_str = "{\"id\": \"12345\"}"; + assert!(serde_json::from_str::(json_str).is_ok()); + assert!(serde_json::from_str::(json_str).is_err()); + assert!(serde_json::from_str::(json_str) + .err() + .unwrap() + .to_string() + .find(&NationalIdError::Length(5).to_string()) + .is_some()); + assert!(serde_json::from_str::(json_str).is_ok()); + assert!(serde_json::from_str::(json_str).is_err()); + assert!(serde_json::from_str::(json_str) + .err() + .unwrap() + .to_string() + .find(&NationalIdError::Length(5).to_string()) + .is_some()); + + // Test Option::None + let json_str = "{}"; + assert!(serde_json::from_str::(json_str).is_ok()); + assert!(serde_json::from_str::(json_str) + .unwrap() + .id + .is_none()); + } +} diff --git a/src/phone_number/mod.rs b/src/phone_number/mod.rs index 7f8c43c..fb751fb 100644 --- a/src/phone_number/mod.rs +++ b/src/phone_number/mod.rs @@ -1,10 +1,19 @@ pub mod operators; -use regex::Regex; +use thiserror::Error; -static MOBILE_REGEX: &str = r#"^(\+98|98|0098|0)?9(\d{2})\d{7}$"#; pub static PREFIXES: [&str; 4] = ["+98", "98", "0098", "0"]; +#[derive(Error, Debug)] +pub enum PhoneNumberError { + #[error("This prefix is not a valid phone number (prefix : `{0}`)")] + InvalidPrefix(String), + #[error("The phone number format is invalid")] + InvalidFormat, + #[error("Unexpected error happened !")] + Unknown, +} + /// This is a simple function that checks if a phone number valid or not /// /// # Examples @@ -12,13 +21,21 @@ pub static PREFIXES: [&str; 4] = ["+98", "98", "0098", "0"]; /// ``` /// use rust_persian_tools::phone_number::is_phone_valid; /// -/// assert_eq!(is_phone_valid("00989122221811"),true); -/// assert_eq!(is_phone_valid("09185371111"),true); -/// assert_eq!(is_phone_valid("20989122221811"),false); +/// assert!(is_phone_valid("+989122221811").is_ok()); +/// assert!(is_phone_valid("12903908").is_err()); /// ``` -pub fn is_phone_valid(phone_number: &str) -> bool { - let regex = Regex::new(MOBILE_REGEX).unwrap(); - regex.is_match(phone_number) +pub fn is_phone_valid(phone_number: impl AsRef) -> Result<(), PhoneNumberError> { + let phone_number = phone_number.as_ref(); + + let prefix = get_phone_prefix(phone_number).unwrap_or(""); + + let phone_number_without_prefix = &phone_number[prefix.len()..]; + + if phone_number_without_prefix.len() == 10 && phone_number_without_prefix.starts_with('9') { + return Ok(()); + } + + Err(PhoneNumberError::InvalidFormat) } /// returns phone prefix for example +98 98 based on given phone number @@ -34,14 +51,21 @@ pub fn is_phone_valid(phone_number: &str) -> bool { /// ``` /// use rust_persian_tools::phone_number::get_phone_prefix; /// -/// assert_eq!(get_phone_prefix("00989122221811"),Some("0098")); -/// assert_eq!(get_phone_prefix("09122221811"),Some("0")); -/// assert_eq!(get_phone_prefix("29122221811"),None); +/// assert_eq!(get_phone_prefix("00989122221811").unwrap(),"0098"); +/// assert_eq!(get_phone_prefix("09122221811").unwrap(),"0"); +/// assert!(get_phone_prefix("29122221811").is_err()); /// ``` -pub fn get_phone_prefix(phone_number: &str) -> Option<&str> { - PREFIXES +pub fn get_phone_prefix(phone_number: impl AsRef) -> Result<&'static str, PhoneNumberError> { + let phone_number = phone_number.as_ref(); + + let prefix = PREFIXES .into_iter() - .find(|&prefix| phone_number.starts_with(prefix)) + .find(|&prefix| phone_number.starts_with(prefix)); + + match prefix { + Some(pre) => Ok(pre), + None => Err(PhoneNumberError::InvalidFormat), + } } /// replaces current phone number prefix with your desired prefix @@ -54,20 +78,24 @@ pub fn get_phone_prefix(phone_number: &str) -> Option<&str> { /// ``` /// use rust_persian_tools::phone_number::phone_number_normalizer; /// -/// assert_eq!(phone_number_normalizer("00989022002580" , "+98") , Some("+989022002580".to_string())); -/// assert_eq!(phone_number_normalizer("9191282819921" , "0") , None); +/// assert_eq!(phone_number_normalizer("+989373708555", "0").unwrap(),"09373708555".to_string()); +/// assert!(phone_number_normalizer("09132222", "+98").is_err()); /// ``` -pub fn phone_number_normalizer(phone_number: &str, new_prefix: &str) -> Option { - if !is_phone_valid(phone_number) { - return None; - } +pub fn phone_number_normalizer( + phone_number: impl AsRef, + new_prefix: impl AsRef, +) -> Result { + let phone_number = phone_number.as_ref(); + let new_prefix = new_prefix.as_ref(); + + is_phone_valid(phone_number)?; - if let Some(prefix) = get_phone_prefix(phone_number) { + if let Ok(prefix) = get_phone_prefix(phone_number) { let (_, splited) = phone_number.split_at(prefix.len()); - return Some(format!("{new_prefix}{splited}")); + return Ok(format!("{new_prefix}{splited}")); } - Some(format!("{new_prefix}{phone_number}")) + Ok(format!("{new_prefix}{phone_number}")) } /// returns operator prefix of phone number (919,912,...) @@ -81,21 +109,19 @@ pub fn phone_number_normalizer(phone_number: &str, new_prefix: &str) -> Option Option<&str> { - if !is_phone_valid(phone_number) { - return None; - } +pub fn get_operator_prefix(phone_number: &str) -> Result<&str, PhoneNumberError> { + is_phone_valid(phone_number)?; for prefix in PREFIXES { if phone_number.starts_with(prefix) { - return Some(&phone_number[prefix.len()..prefix.len() + 3]); + return Ok(&phone_number[prefix.len()..prefix.len() + 3]); } } - None + Err(PhoneNumberError::InvalidFormat) } #[cfg(test)] @@ -104,70 +130,73 @@ mod test_phone_number { #[test] fn check_phone_number_valid() { - assert_eq!(is_phone_valid("9122221811"), true); - assert_eq!(is_phone_valid("09122221811"), true); - assert_eq!(is_phone_valid("+989122221811"), true); - assert_eq!(is_phone_valid("12903908"), false); - assert_eq!(is_phone_valid("901239812390812908"), false); + assert!(is_phone_valid("9122221811").is_ok()); + assert!(is_phone_valid("09122221811").is_ok()); + assert!(is_phone_valid("+989122221811").is_ok()); + assert!(is_phone_valid("12903908").is_err()); + assert!(is_phone_valid("901239812390812908").is_err()); } #[test] fn test_phone_number_normilizer() { // normalize to 0 + assert_eq!( - phone_number_normalizer("+989373708555", "0"), - Some("09373708555".to_string()) + phone_number_normalizer("+989373708555", "0").unwrap(), + "09373708555".to_string() ); + assert_eq!( - phone_number_normalizer("989373708555", "0"), - Some("09373708555".to_string()) + phone_number_normalizer("989373708555", "0").unwrap(), + "09373708555".to_string() ); + assert_eq!( - phone_number_normalizer("00989022002580", "0"), - Some("09022002580".to_string()) + phone_number_normalizer("00989022002580", "0").unwrap(), + "09022002580".to_string() ); assert_eq!( - phone_number_normalizer("09122002580", "0"), - Some("09122002580".to_string()) + phone_number_normalizer("09122002580", "0").unwrap(), + "09122002580".to_string() ); assert_eq!( - phone_number_normalizer("9322002580", "0"), - Some("09322002580".to_string()) + phone_number_normalizer("9322002580", "0").unwrap(), + "09322002580".to_string() ); // normalize to +98 assert_eq!( - phone_number_normalizer("09373708555", "+98"), - Some("+989373708555".to_string()) + phone_number_normalizer("09373708555", "+98").unwrap(), + "+989373708555".to_string() ); assert_eq!( - phone_number_normalizer("09022002580", "+98"), - Some("+989022002580".to_string()) + phone_number_normalizer("09022002580", "+98").unwrap(), + "+989022002580".to_string() ); assert_eq!( - phone_number_normalizer("09122002580", "+98"), - Some("+989122002580".to_string()) + phone_number_normalizer("09122002580", "+98").unwrap(), + "+989122002580".to_string() ); assert_eq!( - phone_number_normalizer("9322002580", "+98"), - Some("+989322002580".to_string()) + phone_number_normalizer("9322002580", "+98").unwrap(), + "+989322002580".to_string() ); assert_eq!( - phone_number_normalizer("00989022002580", "+98"), - Some("+989022002580".to_string()) + phone_number_normalizer("00989022002580", "+98").unwrap(), + "+989022002580".to_string() ); } #[test] fn test_phone_number_normilizer_invalid_phone() { - assert_eq!(phone_number_normalizer("09132222", "+98"), None); - assert_eq!(phone_number_normalizer("9191282819921", "0"), None); + assert!(phone_number_normalizer("09132222", "+98").is_err()); + assert!(phone_number_normalizer("9191282819921", "0").is_err()); } #[test] fn test_operator_prefix() { - assert_eq!(get_operator_prefix("+989373708555"), Some("937")); - assert_eq!(get_operator_prefix("00989013708555"), Some("901")); - assert_eq!(get_operator_prefix("00988013708555"), None); + assert_eq!(get_operator_prefix("+989373708555").unwrap(), "937"); + assert_eq!(get_operator_prefix("00989013708555").unwrap(), "901"); + assert!(get_operator_prefix("00988013708555").is_err()); } } diff --git a/src/phone_number/operators.rs b/src/phone_number/operators.rs index e7ce1a7..213ce09 100644 --- a/src/phone_number/operators.rs +++ b/src/phone_number/operators.rs @@ -1,4 +1,4 @@ -use crate::phone_number::{get_operator_prefix, is_phone_valid}; +use crate::phone_number::{get_operator_prefix, is_phone_valid, PhoneNumberError}; use std::borrow::Cow; pub mod constants { @@ -439,7 +439,7 @@ impl<'a> OperatorDetails<'a> { } pub fn base(&self) -> &'a str { - &self.base + self.base } pub fn model(&self) -> Option<&'a str> { @@ -514,12 +514,17 @@ pub fn prefixes() -> Vec<&'static str> { /// assert_eq!(details.sim_type_list(), &[SimType::Permanent, SimType::Credit]); /// assert_eq!(details.operator(), Operator::MCI); /// -/// assert_eq!(get_prefix_details("9100"), None); +/// assert!(get_prefix_details("9100").is_err()); /// ``` -pub fn get_prefix_details(prefix: &str) -> Option<&OperatorDetails<'static>> { - all_operators() +pub fn get_prefix_details(prefix: &str) -> Result<&OperatorDetails<'static>, PhoneNumberError> { + let result = all_operators() .find(|(key, _)| key == &prefix) - .map(|(_, details)| details) + .map(|(_, details)| details); + + match result { + Some(detail) => Ok(detail), + None => Err(PhoneNumberError::InvalidPrefix(prefix.to_string())), + } } /// returns operator details of givin phone number @@ -535,14 +540,14 @@ pub fn get_prefix_details(prefix: &str) -> Option<&OperatorDetails<'static>> { /// assert_eq!(details.sim_type_list(), &[SimType::Credit]); /// assert_eq!(details.operator(), Operator::MCI); /// -/// assert_eq!(get_phone_details("009195431812") , None); +/// assert!(get_phone_details("009195431812").is_err()); /// ``` -pub fn get_phone_details(phone_number: &str) -> Option<&OperatorDetails<'static>> { - if !is_phone_valid(phone_number) { - return None; - } +pub fn get_phone_details( + phone_number: &str, +) -> Result<&OperatorDetails<'static>, PhoneNumberError> { + is_phone_valid(phone_number)?; - get_operator_prefix(phone_number).and_then(|prefix| get_prefix_details(prefix)) + get_operator_prefix(phone_number).and_then(get_prefix_details) } #[cfg(test)] @@ -552,43 +557,43 @@ mod test_mobile_operators { #[test] fn test_get_phone_prefix_operator() { assert_eq!( - get_prefix_details("904"), - Some(&OperatorDetails { + get_prefix_details("904").unwrap(), + &OperatorDetails { base: "کشوری", province: Cow::Borrowed(&[]), sim_types: Cow::Borrowed(&[SimType::Credit]), operator: Operator::Irancell, model: Some("سیم‌کارت کودک"), - },) + }, ); assert_eq!( - get_prefix_details("910"), - Some(&OperatorDetails { + get_prefix_details("910").unwrap(), + &OperatorDetails { base: "کشوری", province: Cow::Borrowed(&[]), sim_types: Cow::Borrowed(&[SimType::Permanent, SimType::Credit]), operator: Operator::MCI, model: None, - },) + }, ); - assert_eq!(get_prefix_details("9100"), None); + assert!(get_prefix_details("9100").is_err()); } #[test] fn test_get_phone_details() { assert_eq!( - get_phone_details("09195431812"), - Some(&OperatorDetails { + get_phone_details("09195431812").unwrap(), + &OperatorDetails { base: "تهران", province: Cow::Borrowed(&["البرز", "سمنان", "قم", "قزوین", "زنجان"]), sim_types: Cow::Borrowed(&[SimType::Credit]), operator: Operator::MCI, model: None, - },) + }, ); - assert_eq!(get_phone_details("009195431812"), None); + assert!(get_phone_details("009195431812").is_err()); } }