apx_sdk 0.20.0

Minimalistic ActivityPub toolkit
Documentation
//! Parsing object identifiers.

use std::str::FromStr;

use regex::{Captures, Regex};
use thiserror::Error;

use apx_core::url::http_uri::HttpUri;

#[derive(Debug, Error)]
#[error("{0}")]
pub struct PathError(&'static str);

pub trait FromCaptures {
    fn from_captures(caps: Captures) -> Result<Self, PathError>
        where Self: Sized;
}

fn extract(caps: Captures<'_>) -> Result<Vec<&str>, PathError> {
    let substrings = caps.iter().skip(1)
        .map(|maybe_match| {
            maybe_match
                .map(|match_| match_.as_str())
                .ok_or(PathError("invalid path"))
        })
        .collect::<Result<_, _>>()?;
    Ok(substrings)
}

impl<A: FromStr> FromCaptures for (A,) {
    fn from_captures(caps: Captures) -> Result<Self, PathError> {
        if let [a] = extract(caps)?[..] {
            let a = a.parse()
                .map_err(|_| PathError("invalid path segment"))?;
            Ok((a,))
        } else {
            Err(PathError("unexpected number of groups"))
        }
    }
}

impl<A: FromStr, B: FromStr> FromCaptures for (A, B) {
    fn from_captures(caps: Captures) -> Result<Self, PathError> {
        if let [a, b] = extract(caps)?[..] {
            let a = a.parse()
                .map_err(|_| PathError("invalid path segment"))?;
            let b = b.parse()
                .map_err(|_| PathError("invalid path segment"))?;
            Ok((a, b))
        } else {
            Err(PathError("unexpected number of groups"))
        }
    }
}

pub fn parse_object_id<T: FromCaptures>(
    object_id: &str,
    path_re: Regex,
) -> Result<(String, T), PathError> {
    let uri = HttpUri::parse(object_id)
        .map_err(|_| PathError("invalid URL"))?;
    let base_uri = uri.base();
    let path = uri.to_relative();
    let path_caps = path_re.captures(&path)
        .ok_or(PathError("invalid path"))?;
    let value = T::from_captures(path_caps)?;
    Ok((base_uri, value))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_object_id() {
        let actor_id = "https://social.example/users/test";
        let path_re = Regex::new(r"^/users/(?P<username>[0-9A-Za-z_\-]+)$").unwrap();
        let (base_uri, (username,)) = parse_object_id::<(String,)>(
            actor_id,
            path_re,
        ).unwrap();
        assert_eq!(base_uri, "https://social.example");
        assert_eq!(username, "test");
    }
}