parse-author 0.1.1

Parse an npm-style author string "Name <email> (url)" into its name, email, and url. A faithful port of the parse-author npm package. Zero dependencies, no_std.
Documentation
//! Behavioral spec for `parse-author`, cross-checked against the npm package.

use parse_author::{parse_author, Author};

fn a(name: Option<&str>, email: Option<&str>, url: Option<&str>) -> Author {
    Author {
        name: name.map(Into::into),
        email: email.map(Into::into),
        url: url.map(Into::into),
    }
}

#[test]
fn full() {
    assert_eq!(
        parse_author("Jon Schlinkert <jon@example.com> (https://github.com/jonschlinkert)"),
        a(
            Some("Jon Schlinkert"),
            Some("jon@example.com"),
            Some("https://github.com/jonschlinkert")
        )
    );
}

#[test]
fn parts() {
    assert_eq!(
        parse_author("Jon Schlinkert"),
        a(Some("Jon Schlinkert"), None, None)
    );
    assert_eq!(
        parse_author("Jon Schlinkert <jon@example.com>"),
        a(Some("Jon Schlinkert"), Some("jon@example.com"), None)
    );
    assert_eq!(
        parse_author("Jon Schlinkert (https://x.com)"),
        a(Some("Jon Schlinkert"), None, Some("https://x.com"))
    );
    assert_eq!(
        parse_author("<jon@example.com>"),
        a(None, Some("jon@example.com"), None)
    );
    assert_eq!(
        parse_author("(https://x.com)"),
        a(None, None, Some("https://x.com"))
    );
    assert_eq!(
        parse_author("<jon@example.com> (https://x.com)"),
        a(None, Some("jon@example.com"), Some("https://x.com"))
    );
}

#[test]
fn empty_and_no_word() {
    assert_eq!(parse_author(""), Author::default());
    assert_eq!(parse_author("<> ()"), Author::default());
    assert_eq!(parse_author("日本"), Author::default()); // no ASCII word char
    assert_eq!(parse_author("   "), Author::default());
}

#[test]
fn whitespace() {
    assert_eq!(
        parse_author("  Spaced  Name  "),
        a(Some("Spaced  Name"), None, None)
    );
    assert_eq!(
        parse_author("Name<no@space.com>"),
        a(Some("Name"), Some("no@space.com"), None)
    );
    // content keeps its inner spaces
    assert_eq!(
        parse_author("Name < a@b.com >"),
        a(Some("Name"), Some(" a@b.com "), None)
    );
}

#[test]
fn cross_brackets_and_last_wins() {
    // the opening bracket decides the type; the closing may differ
    assert_eq!(parse_author("Name <a)"), a(Some("Name"), Some("a"), None));
    assert_eq!(parse_author("Name (b>"), a(Some("Name"), None, Some("b")));
    // later delimiters of the same kind overwrite earlier ones
    assert_eq!(
        parse_author("Name <a><b>"),
        a(Some("Name"), Some("b"), None)
    );
    assert_eq!(
        parse_author("Foo (bar)(baz)"),
        a(Some("Foo"), None, Some("baz"))
    );
}

#[test]
fn no_match_returns_empty() {
    // unterminated or trailing junk does not match the author shape
    assert_eq!(parse_author("Name <foo"), Author::default());
    assert_eq!(parse_author("Name <a> junk"), Author::default());
    assert_eq!(parse_author("<a> trailing"), Author::default());
    // three whitespace-separated delimiters do not match (the repeated group is gap-free)
    assert_eq!(parse_author("a (b) <c> (d)"), Author::default());
}

#[test]
fn unicode_name() {
    assert_eq!(
        parse_author("José <j@x.com>"),
        a(Some("José"), Some("j@x.com"), None)
    );
}