diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2d62720 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "better-comments.tags": [ + { + "tag": "//", + "strikethrough": false, + }, + ] +} \ No newline at end of file diff --git a/rust-lib/build.rs b/rust-lib/build.rs index 6f5726e..e315341 100644 --- a/rust-lib/build.rs +++ b/rust-lib/build.rs @@ -47,6 +47,14 @@ fn main() { &invalid_domains, ); + create_is_valid_tests( + &mut content, + &valid_local_parts, + &valid_domains, + &invalid_local_parts, + &invalid_domains, + ); + write!(test_file, "{}", content.trim()).unwrap(); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=resources/.test_data/valid_local_parts.txt"); @@ -81,11 +89,11 @@ fn create_valid_parsing_tests( ) { content.push_str( " -macro_rules! generate_test_positive_parsing_test { +macro_rules! generate_positive_parsing_test { ($($case:ident: ($local_part:literal, $domain:literal),)+) => { #[cfg(test)] mod parses_valid_email_address { - use email_address_parser::EmailAddress; + use email_address_parser::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); $( @@ -93,7 +101,7 @@ macro_rules! generate_test_positive_parsing_test { #[wasm_bindgen_test] fn $case() { let address_str = concat!($local_part, \"@\", $domain); - let address = EmailAddress::parse(&address_str, Some(true)); + let address = EmailAddress::parse(&address_str, None); assert_eq!(address.is_some(), true, \"expected {} to be parsed\", address_str); let address = address.unwrap(); assert_eq!(address.get_local_part(), $local_part, \"local_part of {}\", address_str); @@ -105,7 +113,7 @@ macro_rules! generate_test_positive_parsing_test { }; } -generate_test_positive_parsing_test!{ +generate_positive_parsing_test!{ ", ); create_case(content, &mut 0, local_parts, domains); @@ -122,11 +130,11 @@ fn create_invalid_parsing_tests( ) { content.push_str( " -macro_rules! generate_test_negative_parsing_test { +macro_rules! generate_negative_parsing_test { ($($case:ident: ($local_part:literal, $domain:literal),)+) => { #[cfg(test)] mod does_not_parse_invalid_email_address { - use email_address_parser::EmailAddress; + use email_address_parser::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); $( @@ -134,14 +142,14 @@ macro_rules! generate_test_negative_parsing_test { #[wasm_bindgen_test] fn $case() { let address_str = concat!($local_part, \"@\", $domain); - assert_eq!(EmailAddress::parse(&address_str, Some(true)).is_none(), true, \"expected {} not to be parsed\", address_str); + assert_eq!(EmailAddress::parse(&address_str, None).is_none(), true, \"expected {} not to be parsed\", address_str); } )* } }; } -generate_test_negative_parsing_test!{ +generate_negative_parsing_test!{ ", ); let mut i = 0; @@ -161,14 +169,14 @@ macro_rules! generate_is_email_test { ($($case:ident: ($email:literal, $is_email:literal),)+) => { #[cfg(test)] mod is_email_parsing_tests { - use email_address_parser::EmailAddress; + use email_address_parser::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); $( #[test] #[wasm_bindgen_test] fn $case() { - let email = EmailAddress::parse(&$email, None); + let email = EmailAddress::parse(&$email, Some(ParsingOptions{is_lax: true, supports_unicode: false})); assert_eq!(email.is_some(), $is_email, \"expected {} to be valid: {}\", $email, $is_email); if $is_email { assert_eq!( @@ -263,18 +271,18 @@ fn create_valid_instantiation_tests( ) { content.push_str( " -macro_rules! generate_test_positive_instantiation_test { +macro_rules! generate_positive_instantiation_test { ($($case:ident: ($local_part:literal, $domain:literal),)+) => { #[cfg(test)] mod instantiates_valid_email_address { - use email_address_parser::EmailAddress; + use email_address_parser::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); $( #[test] #[wasm_bindgen_test] fn $case() { - let address = EmailAddress::new(&$local_part, &$domain, None).unwrap(); + let address = EmailAddress::new(&$local_part, &$domain, Some(ParsingOptions{is_lax: true, supports_unicode: false})).unwrap(); assert_eq!(address.get_local_part(), $local_part); assert_eq!(address.get_domain(), $domain); assert_eq!(format!(\"{}\", address), concat!($local_part, \"@\", $domain), \"incorrect display\"); @@ -284,7 +292,7 @@ macro_rules! generate_test_positive_instantiation_test { }; } -generate_test_positive_instantiation_test!{ +generate_positive_instantiation_test!{ ", ); create_case(content, &mut 0, local_parts, domains); @@ -301,25 +309,25 @@ fn create_invalid_instantiation_tests( ) { content.push_str( " -macro_rules! generate_test_negative_instantiation_test { +macro_rules! generate_negative_instantiation_test { ($($case:ident: ($local_part:literal, $domain:literal),)+) => { #[cfg(test)] mod panics_instantiating_invalid_email_address { - use email_address_parser::EmailAddress; + use email_address_parser::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); $( #[test] #[wasm_bindgen_test] fn $case() { - assert_eq!(EmailAddress::new(&$local_part, &$domain, None).is_none(), true); + assert_eq!(EmailAddress::new(&$local_part, &$domain, Some(ParsingOptions{is_lax: true, supports_unicode: false})).is_none(), true); } )* } }; } -generate_test_negative_instantiation_test!{ +generate_negative_instantiation_test!{ ", ); let mut i = 0; @@ -329,3 +337,61 @@ generate_test_negative_instantiation_test!{ content.push_str("}\n"); } + +fn create_is_valid_tests( + content: &mut String, + valid_local_parts: &Vec, + valid_domains: &Vec, + invalid_local_parts: &Vec, + invalid_domains: &Vec, +) { + fn create_is_valid_case( + content: &mut String, + case_index: &mut i32, + local_parts: &Vec, + domains: &Vec, + is_valid: bool, + ) { + for local_part in local_parts { + for domain in domains { + *case_index += 1; + content.push_str(&format!( + " {}: (\"{}\", {}),\n", + &format!("case{}", case_index), + format!("{}@{}", local_part, domain), + is_valid + )); + } + } + } + content.push_str( + " +macro_rules! generate_is_valid_test { + ($($case:ident: ($address:literal, $is_valid:literal),)+) => { + #[cfg(test)] + mod is_valid_email_address { + use email_address_parser::*; + use wasm_bindgen_test::*; + wasm_bindgen_test_configure!(run_in_browser); + $( + #[test] + #[wasm_bindgen_test] + fn $case() { + assert_eq!(EmailAddress::is_valid(&$address, None), $is_valid, \"expected {} not to be valid: {}\", $address, $is_valid); + } + )* + } + }; +} + +generate_is_valid_test!{ +", + ); + let mut i = 0; + create_is_valid_case(content, &mut i, valid_local_parts, valid_domains, true); + create_is_valid_case(content, &mut i, invalid_local_parts, valid_domains, false); + create_is_valid_case(content, &mut i, valid_local_parts, invalid_domains, false); + create_is_valid_case(content, &mut i, invalid_local_parts, invalid_domains, false); + + content.push_str("}\n"); +} diff --git a/rust-lib/src/email_address.rs b/rust-lib/src/email_address.rs index 382935e..a209b5c 100644 --- a/rust-lib/src/email_address.rs +++ b/rust-lib/src/email_address.rs @@ -1,9 +1,25 @@ extern crate pest; extern crate pest_derive; -use pest::Parser; +use pest::{iterators::Pairs, Parser}; use std::fmt; use wasm_bindgen::prelude::*; +#[wasm_bindgen] +#[derive(Debug)] +pub struct ParsingOptions { + pub is_lax: bool, + pub supports_unicode: bool, +} + +impl Default for ParsingOptions { + fn default() -> Self { + ParsingOptions { + is_lax: false, + supports_unicode: false, + } + } +} + #[derive(Parser)] #[grammar = "rfc5322.pest"] struct RFC5322; @@ -14,8 +30,8 @@ struct RFC5322; /// ``` /// use email_address_parser::EmailAddress; /// -/// assert!(EmailAddress::parse("foo@-bar.com", Some(true)).is_none()); -/// let email = EmailAddress::parse("foo@bar.com", Some(true)); +/// assert!(EmailAddress::parse("foo@-bar.com", None).is_none()); +/// let email = EmailAddress::parse("foo@bar.com", None); /// assert!(email.is_some()); /// let email = email.unwrap(); /// assert_eq!(email.get_local_part(), "foo"); @@ -45,12 +61,17 @@ impl EmailAddress { /// ``` /// use email_address_parser::EmailAddress; /// - /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::new("foo", "bar.com", None).unwrap(); /// - /// assert_eq!(EmailAddress::new("foo", "-bar.com", Some(true)).is_none(), true); + /// assert_eq!(EmailAddress::new("foo", "-bar.com", None).is_none(), true); /// ``` - pub fn new(local_part: &str, domain: &str, is_strict: Option) -> Option { - let is_strict = is_strict.unwrap_or_default(); + pub fn new( + local_part: &str, + domain: &str, + options: Option, + ) -> Option { + let options = options.unwrap_or_default(); + let is_strict = !options.is_lax; if (is_strict && !RFC5322::parse(Rule::local_part_complete, local_part).is_ok()) || (!is_strict && !RFC5322::parse(Rule::local_part_complete, local_part).is_ok()) @@ -78,30 +99,30 @@ impl EmailAddress { /// Returns `Some(EmailAddress)` if the parsing is successful, else `None`. /// # Examples /// ``` - /// use email_address_parser::EmailAddress; + /// use email_address_parser::*; /// /// // strict parsing - /// let email = EmailAddress::parse("foo@bar.com", Some(true)); + /// let email = EmailAddress::parse("foo@bar.com", None); /// assert!(email.is_some()); /// let email = email.unwrap(); /// assert_eq!(email.get_local_part(), "foo"); /// assert_eq!(email.get_domain(), "bar.com"); /// /// // non-strict parsing - /// let email = EmailAddress::parse("\u{0d}\u{0a} \u{0d}\u{0a} test@iana.org", None); + /// let email = EmailAddress::parse("\u{0d}\u{0a} \u{0d}\u{0a} test@iana.org", Some(ParsingOptions{is_lax: true, supports_unicode: false})); /// assert!(email.is_some()); /// /// // parsing invalid address - /// let email = EmailAddress::parse("test@-iana.org", Some(true)); + /// let email = EmailAddress::parse("test@-iana.org", Some(ParsingOptions{is_lax: true, supports_unicode: false})); /// assert!(email.is_none()); - /// let email = EmailAddress::parse("test@-iana.org", None); + /// let email = EmailAddress::parse("test@-iana.org", Some(ParsingOptions{is_lax: true, supports_unicode: false})); /// assert!(email.is_none()); - /// let email = EmailAddress::parse("test", Some(true)); + /// let email = EmailAddress::parse("test", Some(ParsingOptions{is_lax: true, supports_unicode: false})); /// assert!(email.is_none()); - /// let email = EmailAddress::parse("test", None); + /// let email = EmailAddress::parse("test", Some(ParsingOptions{is_lax: true, supports_unicode: false})); /// assert!(email.is_none()); /// ``` - pub fn parse(input: &str, is_strict: Option) -> Option { + pub fn parse(input: &str, options: Option) -> Option { let instantiate = |mut parsed: pest::iterators::Pairs| { let mut parsed = parsed .next() @@ -115,21 +136,14 @@ impl EmailAddress { domain: String::from(parsed.next().unwrap().as_str()), }) }; - let is_strict = is_strict.unwrap_or_default(); - match RFC5322::parse(Rule::address_single, input) { - Ok(parsed) => instantiate(parsed), - Err(_) => { - if is_strict { - None - } else { - match RFC5322::parse(Rule::address_single_obs, input) { - Ok(parsed) => instantiate(parsed), - Err(_) => None, - } - } - } + match EmailAddress::parse_core(input, options) { + Some(parsed) => instantiate(parsed), + None => None, } } + pub fn is_valid(input: &str, options: Option) -> bool { + EmailAddress::parse_core(input, options).is_some() + } /// Returns the local part of the email address. /// /// Note that if you are using this library from rust, then consider using the `get_local_part` method instead. @@ -139,10 +153,10 @@ impl EmailAddress { /// ``` /// use email_address_parser::EmailAddress; /// - /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::new("foo", "bar.com", None).unwrap(); /// assert_eq!(email.local_part(), "foo"); /// - /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::parse("foo@bar.com", None).unwrap(); /// assert_eq!(email.local_part(), "foo"); /// ``` #[doc(hidden)] @@ -158,16 +172,34 @@ impl EmailAddress { /// ``` /// use email_address_parser::EmailAddress; /// - /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::new("foo", "bar.com", None).unwrap(); /// assert_eq!(email.domain(), "bar.com"); /// - /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::parse("foo@bar.com", None).unwrap(); /// assert_eq!(email.domain(), "bar.com"); /// ``` #[doc(hidden)] pub fn domain(&self) -> String { self.domain.clone() } + + fn parse_core<'i>(input: &'i str, options: Option) -> Option> { + let options = options.unwrap_or_default(); + let is_strict = !options.is_lax; + match RFC5322::parse(Rule::address_single, input) { + Ok(parsed) => Some(parsed), + Err(_) => { + if is_strict { + None + } else { + match RFC5322::parse(Rule::address_single_obs, input) { + Ok(parsed) => Some(parsed), + Err(_) => None, + } + } + } + } + } } impl EmailAddress { @@ -181,10 +213,10 @@ impl EmailAddress { /// ``` /// use email_address_parser::EmailAddress; /// - /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::new("foo", "bar.com", None).unwrap(); /// assert_eq!(email.get_local_part(), "foo"); /// - /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::parse("foo@bar.com", None).unwrap(); /// assert_eq!(email.get_local_part(), "foo"); /// ``` pub fn get_local_part(&self) -> &str { @@ -198,10 +230,10 @@ impl EmailAddress { /// ``` /// use email_address_parser::EmailAddress; /// - /// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::new("foo", "bar.com", None).unwrap(); /// assert_eq!(email.get_domain(), "bar.com"); /// - /// let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); + /// let email = EmailAddress::parse("foo@bar.com", None).unwrap(); /// assert_eq!(email.get_domain(), "bar.com"); /// ``` pub fn get_domain(&self) -> &str { @@ -221,7 +253,7 @@ mod tests { #[test] fn email_address_instantiation_works() { - let address = EmailAddress::new("foo", "bar.com", Some(true)).unwrap(); + let address = EmailAddress::new("foo", "bar.com", None).unwrap(); assert_eq!(address.get_local_part(), "foo"); assert_eq!(address.get_domain(), "bar.com"); assert_eq!(format!("{}", address), "foo@bar.com"); @@ -279,7 +311,13 @@ mod tests { #[test] fn can_parse_domain_with_space() { println!("{:#?}", RFC5322::parse(Rule::domain_obs, " iana .com")); - let actual = EmailAddress::parse("test@ iana .com", None); + let actual = EmailAddress::parse( + "test@ iana .com", + Some(ParsingOptions { + is_lax: true, + supports_unicode: false, + }), + ); println!("{:#?}", actual); assert_eq!(actual.is_some(), true, "test@ iana .com"); } @@ -295,7 +333,13 @@ mod tests { #[test] fn can_parse_email_with_crlf() { let email = "\u{0d}\u{0a} test@iana.org"; - let actual = EmailAddress::parse(&email, None); + let actual = EmailAddress::parse( + &email, + Some(ParsingOptions { + is_lax: true, + supports_unicode: false, + }), + ); println!("{:#?}", actual); assert_eq!(format!("{}", actual.unwrap()), email); } diff --git a/rust-lib/src/lib.rs b/rust-lib/src/lib.rs index 92ae69f..db30834 100644 --- a/rust-lib/src/lib.rs +++ b/rust-lib/src/lib.rs @@ -1,27 +1,27 @@ //! An RFC 5322 compliant email address parser. -//! +//! //! # Examples //! You can parse string for email address like this. //! ``` //! use email_address_parser::EmailAddress; -//! -//! let email = EmailAddress::parse("foo@bar.com", Some(true)).unwrap(); +//! +//! let email = EmailAddress::parse("foo@bar.com", None).unwrap(); //! assert_eq!(email.get_local_part(), "foo"); //! assert_eq!(email.get_domain(), "bar.com"); //! ``` -//! +//! //! For an input string that is an invalid email address, it returns `None`. //! ``` //! use email_address_parser::EmailAddress; -//! -//! assert!(EmailAddress::parse("test@-iana.org", Some(true)).is_none()); +//! +//! assert!(EmailAddress::parse("test@-iana.org", None).is_none()); //! ``` -//! +//! //! To parse an email address with obsolete parts (as per RFC 5322) in it, pass `None` as the second argument to have non-strict parsing. //! ``` -//! use email_address_parser::EmailAddress; -//! -//! let email = EmailAddress::parse("\u{0d}\u{0a} \u{0d}\u{0a} test@iana.org", None); +//! use email_address_parser::*; +//! +//! let email = EmailAddress::parse("\u{0d}\u{0a} \u{0d}\u{0a} test@iana.org", Some(ParsingOptions{is_lax: true, supports_unicode: false})); //! assert!(email.is_some()); //! ``` @@ -30,4 +30,5 @@ extern crate pest_derive; mod email_address; #[doc(inline)] -pub use self::email_address::EmailAddress; \ No newline at end of file +pub use self::email_address::EmailAddress; +pub use self::email_address::ParsingOptions;