vouched 0.2.0

A derive macro for validated tuple-struct wrappers.
Documentation
use vouched::{Error, Vouched};

#[derive(Debug, PartialEq, Eq, Vouched)]
#[vouched(len(1..), chars('a'..='z', '0'..='9', '_'))]
struct Username(String);

impl Username {
    fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Debug, PartialEq, Eq, Vouched)]
#[vouched(len(1..=32))]
struct DisplayName(String);

impl DisplayName {
    fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Debug, PartialEq, Eq)]
struct AccountProfile {
    username: Username,
    display_name: DisplayName,
}

impl AccountProfile {
    fn try_new(username: &str, display_name: &str) -> Result<Self, Error> {
        Ok(Self {
            username: Username::try_from(username.to_owned())?,
            display_name: DisplayName::try_from(display_name.to_owned())?,
        })
    }
    fn username(&self) -> &Username {
        &self.username
    }
    fn display_name(&self) -> &DisplayName {
        &self.display_name
    }
}

fn too_short_parts(err: Option<&Error>) -> Option<(usize, usize)> {
    err.and_then(|err| err.as_too_short())
        .map(|err| (err.min(), err.actual()))
}

fn too_long_parts(err: Option<&Error>) -> Option<(usize, usize)> {
    err.and_then(|err| err.as_too_long())
        .map(|err| (err.max(), err.actual()))
}

fn invalid_char_parts(err: Option<&Error>) -> Option<(usize, char)> {
    err.and_then(|err| err.as_invalid_char())
        .map(|err| (err.index(), err.ch()))
}

fn accepted_profile() -> Result<(), Error> {
    let account_profile = AccountProfile::try_new("alice_123", "Alice")?;
    assert_eq!(account_profile.username().as_str(), "alice_123");
    assert_eq!(account_profile.display_name().as_str(), "Alice");
    Ok(())
}

fn rejected_profile() {
    let empty_username = AccountProfile::try_new("", "Alice").err();
    assert_eq!(too_short_parts(empty_username.as_ref()), Some((1, 0)));

    let bad_username = AccountProfile::try_new("alice-123", "Alice").err();
    assert_eq!(invalid_char_parts(bad_username.as_ref()), Some((5, '-')));

    let long_display_name = AccountProfile::try_new("alice_123", "A".repeat(33).as_str()).err();
    assert_eq!(too_long_parts(long_display_name.as_ref()), Some((32, 33)));
}

fn main() -> Result<(), Error> {
    accepted_profile()?;
    rejected_profile();

    Ok(())
}