Skip to content

Commit

Permalink
feat: parse local_part, domain on instantiation
Browse files Browse the repository at this point in the history
The local part and the domain are validated on EmailAddress::new.
The return type is changed to Option<EmailAddress>. Ideally it
should have been `Result<EmailAddress, std::error::Error>`. However,
unfortunately it does not seems to be working with wasm_bindgen;
refer: rustwasm/wasm-bindgen#1017.
  • Loading branch information
Sayan751 committed Sep 1, 2020
1 parent 32c9f8a commit 92b2af9
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 43 deletions.
84 changes: 84 additions & 0 deletions rust-lib/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,18 @@ fn main() {
&invalid_local_parts,
&invalid_domains,
);

create_is_email_tests(&mut content, &test_data_root);

create_valid_instantiation_tests(&mut content, &valid_local_parts, &valid_domains);
create_invalid_instantiation_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");
Expand Down Expand Up @@ -245,3 +255,77 @@ generate_is_email_test!{

content.push_str("}");
}

fn create_valid_instantiation_tests(
content: &mut String,
local_parts: &Vec<String>,
domains: &Vec<String>,
) {
content.push_str(
"
macro_rules! generate_test_positive_instantiation_test {
($($case:ident: ($local_part:literal, $domain:literal),)+) => {
#[cfg(test)]
mod instantiates_valid_email_address {
use email_address_parser::EmailAddress;
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();
assert_eq!(address.get_local_part(), $local_part);
assert_eq!(address.get_domain(), $domain);
assert_eq!(format!(\"{}\", address), concat!($local_part, \"@\", $domain), \"incorrect display\");
}
)*
}
};
}
generate_test_positive_instantiation_test!{
",
);
create_case(content, &mut 0, local_parts, domains);

content.push_str("}\n");
}

fn create_invalid_instantiation_tests(
content: &mut String,
valid_local_parts: &Vec<String>,
valid_domains: &Vec<String>,
invalid_local_parts: &Vec<String>,
invalid_domains: &Vec<String>,
) {
content.push_str(
"
macro_rules! generate_test_negative_instantiation_test {
($($case:ident: ($local_part:literal, $domain:literal),)+) => {
#[cfg(test)]
mod panics_instantiating_invalid_email_address {
use email_address_parser::EmailAddress;
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);
}
)*
}
};
}
generate_test_negative_instantiation_test!{
",
);
let mut i = 0;
create_case(content, &mut i, invalid_local_parts, valid_domains);
create_case(content, &mut i, valid_local_parts, invalid_domains);
create_case(content, &mut i, invalid_local_parts, invalid_domains);

content.push_str("}\n");
}
110 changes: 67 additions & 43 deletions rust-lib/src/email_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,45 +33,64 @@ pub struct EmailAddress {
impl EmailAddress {
#![warn(missing_docs)]
#![warn(missing_doc_code_examples)]
// TODO: validate local part and domain

/// Instantiates a new `EmailAddress`.
///
/// Instantiates a new `Some(EmailAddress)` for a valid local part and domain.
/// Returns `None` otherwise.
/// Ideally a `Result<EmailAddress, std::error::Error>` should have been returned instead of `Option<EmailAddress>`.
/// However, unfortunately it does not seems to be working with wasm_bindgen (refer: https://github.com/rustwasm/wasm-bindgen/issues/1017).
///
/// Accessible from WASM.
///
/// #Examples
///
/// # Examples
/// ```
/// use email_address_parser::EmailAddress;
///
/// let email = EmailAddress::new("foo", "bar.com");
///
/// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap();
///
/// assert_eq!(EmailAddress::new("foo", "-bar.com", Some(true)).is_none(), true);
/// ```
pub fn new(local_part: &str, domain: &str) -> EmailAddress {
EmailAddress {
pub fn new(local_part: &str, domain: &str, is_strict: Option<bool>) -> Option<EmailAddress> {
let is_strict = is_strict.unwrap_or_default();

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())
{
// return Err(format!("Invalid local part '{}'.", local_part));
return None;
}
if (is_strict && !RFC5322::parse(Rule::domain_complete, domain).is_ok())
|| (!is_strict && !RFC5322::parse(Rule::domain_complete, domain).is_ok())
{
// return Err(format!("Invalid domain '{}'.", domain));
return None;
}

Some(EmailAddress {
local_part: String::from(local_part),
domain: String::from(domain),
}
})
}

/// Parses a given string as an email address.
///
///
/// Accessible from WASM.
///
///
/// Returns `Some(EmailAddress)` if the parsing is successful, else `None`.
/// #Examples
/// # Examples
/// ```
/// use email_address_parser::EmailAddress;
///
///
/// // strict parsing
/// let email = EmailAddress::parse("[email protected]", Some(true));
/// 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} [email protected]", None);
/// assert!(email.is_some());
///
///
/// // parsing invalid address
/// let email = EmailAddress::parse("[email protected]", Some(true));
/// assert!(email.is_none());
Expand Down Expand Up @@ -112,17 +131,17 @@ impl EmailAddress {
}
}
/// 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.
/// This returns a cloned copy of the local part string, instead of a borrowed `&str`, and exists purely for WASM interoperability.
///
/// #Examples
///
/// # Examples
/// ```
/// use email_address_parser::EmailAddress;
///
/// let email = EmailAddress::new("foo", "bar.com");
///
/// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap();
/// assert_eq!(email.local_part(), "foo");
///
///
/// let email = EmailAddress::parse("[email protected]", Some(true)).unwrap();
/// assert_eq!(email.local_part(), "foo");
/// ```
Expand All @@ -131,17 +150,17 @@ impl EmailAddress {
self.local_part.clone()
}
/// Returns the domain of the email address.
///
///
/// Note that if you are using this library from rust, then consider using the `get_domain` method instead.
/// This returns a cloned copy of the domain string, instead of a borrowed `&str`, and exists purely for WASM interoperability.
///
/// #Examples
///
/// # Examples
/// ```
/// use email_address_parser::EmailAddress;
///
/// let email = EmailAddress::new("foo", "bar.com");
///
/// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap();
/// assert_eq!(email.domain(), "bar.com");
///
///
/// let email = EmailAddress::parse("[email protected]", Some(true)).unwrap();
/// assert_eq!(email.domain(), "bar.com");
/// ```
Expand All @@ -154,36 +173,34 @@ impl EmailAddress {
impl EmailAddress {
#![warn(missing_docs)]
#![warn(missing_doc_code_examples)]

/// Returns the local part of the email address.
///
///
/// Not accessible from WASM.
///
/// #Examples
///
/// # Examples
/// ```
/// use email_address_parser::EmailAddress;
///
/// let email = EmailAddress::new("foo", "bar.com");
///
/// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap();
/// assert_eq!(email.get_local_part(), "foo");
///
///
/// let email = EmailAddress::parse("[email protected]", Some(true)).unwrap();
/// assert_eq!(email.get_local_part(), "foo");
/// ```
pub fn get_local_part(&self) -> &str {
self.local_part.as_str()
}

/// Returns the domain of the email address.
///
///
/// Not accessible from WASM.
///
/// #Examples
///
/// # Examples
/// ```
/// use email_address_parser::EmailAddress;
///
/// let email = EmailAddress::new("foo", "bar.com");
///
/// let email = EmailAddress::new("foo", "bar.com", Some(true)).unwrap();
/// assert_eq!(email.get_domain(), "bar.com");
///
///
/// let email = EmailAddress::parse("[email protected]", Some(true)).unwrap();
/// assert_eq!(email.get_domain(), "bar.com");
/// ```
Expand All @@ -204,7 +221,7 @@ mod tests {

#[test]
fn email_address_instantiation_works() {
let address = EmailAddress::new("foo", "bar.com");
let address = EmailAddress::new("foo", "bar.com", Some(true)).unwrap();
assert_eq!(address.get_local_part(), "foo");
assert_eq!(address.get_domain(), "bar.com");
assert_eq!(format!("{}", address), "[email protected]");
Expand Down Expand Up @@ -296,4 +313,11 @@ mod tests {
println!("{:#?}", actual);
assert_eq!(actual.is_err(), false);
}

#[test]
fn can_parse_local_part_with_space_and_quote() {
let actual = RFC5322::parse(Rule::local_part_complete, "\"test test\"");
println!("{:#?}", actual);
assert_eq!(actual.is_err(), false);
}
}
5 changes: 5 additions & 0 deletions rust-lib/src/rfc5322.pest
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ address_spec = { local_part ~ "@" ~ domain }

local_part = @{ dot_atom | quoted_string }
domain = @{ dot_atom | domain_literal }

local_part_complete = { SOI ~ local_part ~ EOI }
domain_complete = { SOI ~ domain ~ EOI }

/*------------ lower level rules -------------*/
Expand Down Expand Up @@ -56,6 +58,9 @@ address_spec_obs = { local_part_obs ~ "@" ~ domain_obs }
local_part_obs = @{ obs_local_part | dot_atom | quoted_string }
domain_obs = @{ obs_domain | dot_atom | domain_literal }

obs_local_part_complete = { SOI ~ obs_local_part ~ EOI }
obs_domain_complete = { SOI ~ obs_domain ~ EOI }

obs_local_part = { FWS* ~ word ~ (CFWS* ~ "." ~ CFWS* ~ word)* }
obs_domain = {
CFWS* ~
Expand Down

0 comments on commit 92b2af9

Please sign in to comment.