OXYGENHUB

Coverage / crates/hub/src/social.rs 98.3%

59 of 60 lines run, 23 of 25 functions · run #482 on proposal/333 · run missed

1
//! Accounts elsewhere on a profile (hub spec §6, oxygenhub#105): a
2
//! Bluesky handle, a Mastodon address and a GitHub username, checked and
3
//! normalised on the way in, and turned into links on the way out.
4
5
/// A Bluesky handle: a domain such as `andy.bsky.social`, with or
6
/// without a leading `@`. `None` when it is not one.
76×
pub fn bluesky(input: &str) -> Option<String> {
86×
    let h = input.trim().trim_start_matches('@').trim_end_matches('/');
96×
    let h = h.strip_prefix("https://bsky.app/profile/").unwrap_or(h).to_ascii_lowercase();
106×
    (!h.is_empty() && h.len() <= 253 && h.contains('.') && h.split('.').all(label_ok)).then_some(h)
116×
}
12
13
/// A Mastodon address as `user@instance`, from `@user@instance`,
14
/// `user@instance` or `https://instance/@user`.
155×
pub fn mastodon(input: &str) -> Option<String> {
165×
    let s = input.trim().trim_end_matches('/');
175×
    let (user, instance) = if let Some(rest) = s.strip_prefix("https://") {
182×
        let (instance, user) = rest.split_once("/@")?;
192×
        (user, instance)
20
    } else {
213×
        s.trim_start_matches('@').split_once('@')?
22
    };
2316×
    let user_ok = !user.is_empty() && user.len() <= 64 && user.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.');
244×
    let instance = instance.to_ascii_lowercase();
254×
    let instance_ok = instance.contains('.') && instance.len() <= 253 && instance.split('.').all(label_ok);
264×
    (user_ok && instance_ok).then(|| format!("{user}@{instance}"))
275×
}
28
29
/// A GitHub username: 1 to 39 letters, digits and dashes, from the name
30
/// or a profile URL.
315×
pub fn github(input: &str) -> Option<String> {
325×
    let u = input.trim().trim_start_matches('@').trim_end_matches('/');
335×
    let u = u.strip_prefix("https://github.com/").unwrap_or(u);
345×
    let ok = !u.is_empty()
355×
        && u.len() <= 39
3641×
        && u.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
374×
        && !u.starts_with('-')
383×
        && !u.ends_with('-');
395×
    ok.then(|| u.to_string())
405×
}
41
4216×
fn label_ok(l: &str) -> bool {
4383×
    !l.is_empty() && l.len() <= 63 && l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') && !l.starts_with('-') && !l.ends_with('-')
4416×
}
45
462×
pub fn bluesky_url(handle: &str) -> String {
472×
    format!("https://bsky.app/profile/{handle}")
482×
}
49
50
/// `user@instance` → `https://instance/@user`.
513×
pub fn mastodon_url(address: &str) -> String {
523×
    match address.split_once('@') {
533×
        Some((user, instance)) => format!("https://{instance}/@{user}"),
540×
        None => String::new(),
55
    }
563×
}
57
582×
pub fn github_url(user: &str) -> String {
592×
    format!("https://github.com/{user}")
602×
}
61
62
#[cfg(test)]
63
mod tests {
64
    use super::*;
65
66
    #[test]
671×
    fn handles_are_checked_and_normalised() {
681×
        assert_eq!(bluesky("@Andy.bsky.social").as_deref(), Some("andy.bsky.social"));
691×
        assert_eq!(bluesky("https://bsky.app/profile/andy.example.org/").as_deref(), Some("andy.example.org"));
701×
        assert_eq!(bluesky("andy"), None, "a handle is a domain");
711×
        assert_eq!(bluesky("an dy.bsky.social"), None);
721×
        assert_eq!(mastodon("@[email protected]").as_deref(), Some("[email protected]"));
731×
        assert_eq!(mastodon("https://Fosstodon.org/@andy").as_deref(), Some("[email protected]"));
741×
        assert_eq!(mastodon("andy"), None);
751×
        assert_eq!(mastodon("andy@nodot"), None);
761×
        assert_eq!(github("https://github.com/andymccall/").as_deref(), Some("andymccall"));
771×
        assert_eq!(github("@andy-mccall").as_deref(), Some("andy-mccall"));
781×
        assert_eq!(github("-andy"), None);
791×
        assert_eq!(github("andy mccall"), None);
801×
        assert_eq!(mastodon_url("[email protected]"), "https://fosstodon.org/@andy");
811×
    }
82
}