dialtone_common 0.1.0

Dialtone Common Code
Documentation
use std::fmt::Display;

use super::pun::{actor_type_from_pun, is_valid_pun};
use super::ActorType;
use crate::ap::ap_object::ApObjectType;
use crate::pages::create_base_url;

const AP_SEGMENT: &str = "pub";

// actors
const AP_APPLICATION_SEGMENT: &str = "a";
const AP_GROUP_SEGMENT: &str = "g";
const AP_ORGANIZATION_SEGMENT: &str = "o";
const AP_PERSON_SEGMENT: &str = "p";
const AP_SERVICE_SEGMENT: &str = "s";

// attributes
const AP_SHARED_INBOX_SEGMENT: &str = "sharedInbox";
const AP_INBOX_SEGMENT: &str = "inbox";
const AP_OUTBOX_SEGMENT: &str = "outbox";
const AP_FOLLOWING_SEGMENT: &str = "following";
const AP_FOLLOWERS_SEGMENT: &str = "followers";
const AP_LIKED_SEGMENT: &str = "liked";
const AP_LIKES_SEGMENT: &str = "likes";

// ap objects
const AP_NOTE_SEGMENT: &str = "note";
const AP_ARTICLE_SEGMENT: &str = "article";
const AP_DOCUMENT_SEGMENT: &str = "document";
const AP_IMAGE_SEGMENT: &str = "image";
const AP_VIDEO_SEGMENT: &str = "video";
const AP_AUDIO_SEGMENT: &str = "audio";
const AP_PAGE_SEGMENT: &str = "page";

/// The `pun` should already be suffixed.
/// See [super::pun::create_preferred_user_name].
pub fn create_actor_id(host_name: &str, pun: &str) -> String {
    let actor_type = actor_type_from_pun(pun);
    let (actor_segment, pun_segment) = match actor_type {
        ActorType::Application => (AP_APPLICATION_SEGMENT, &pun[0..pun.len() - 2]),
        ActorType::Group => (AP_GROUP_SEGMENT, &pun[0..pun.len() - 2]),
        ActorType::Organization => (AP_ORGANIZATION_SEGMENT, &pun[0..pun.len() - 2]),
        ActorType::Person => (AP_PERSON_SEGMENT, pun),
        ActorType::Service => (AP_SERVICE_SEGMENT, &pun[0..pun.len() - 2]),
    };
    format!(
        "{}/{}/{}/{}",
        create_base_url(host_name),
        AP_SEGMENT,
        actor_segment,
        pun_segment
    )
}

#[derive(Debug)]
pub enum IdError {
    BadHostPrefix,
    MissingPath,
    InvalidPun,
}

impl Display for IdError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            IdError::BadHostPrefix => write!(f, "Bad Host Prefix"),
            IdError::MissingPath => write!(f, "Missing Path"),
            IdError::InvalidPun => write!(f, "Invalid Preferred User Name"),
        }
    }
}

const HTTP_PREFIX: &str = "http://";
const HTTPS_PREFIX: &str = "https://";

pub fn parse_actors_host(actor_id: &str) -> Result<String, IdError> {
    if actor_id.starts_with(HTTPS_PREFIX) {
        actor_id_host(actor_id, HTTPS_PREFIX)
    } else if actor_id.starts_with(HTTP_PREFIX) {
        actor_id_host(actor_id, HTTP_PREFIX)
    } else {
        Err(IdError::BadHostPrefix)
    }
}

fn actor_id_host(actor_id: &str, prefix: &str) -> Result<String, IdError> {
    let no_prefix = actor_id
        .strip_prefix(prefix)
        .ok_or(IdError::BadHostPrefix)?;
    let parts = no_prefix.split_once('/').ok_or(IdError::MissingPath)?;
    if parts.0.len() < 1 {
        Err(IdError::MissingPath)
    } else {
        Ok(parts.0.to_string())
    }
}

pub fn parse_actors_pun(actor_id: &str) -> Result<String, IdError> {
    let mut parts = actor_id.split('/');
    let scheme = parts.next().ok_or(IdError::BadHostPrefix)?;
    if !(HTTPS_PREFIX.starts_with(scheme) || HTTP_PREFIX.starts_with(scheme)) {
        return Err(IdError::BadHostPrefix);
    }
    let empty = parts.next().ok_or(IdError::BadHostPrefix)?;
    if !empty.is_empty() {
        return Err(IdError::BadHostPrefix);
    }
    let host = parts.next().ok_or(IdError::BadHostPrefix)?;
    if host.is_empty() {
        return Err(IdError::BadHostPrefix);
    }
    let ap_segment = parts.next().ok_or(IdError::MissingPath)?;
    if ap_segment.is_empty() {
        return Err(IdError::MissingPath);
    }
    let actor_type_segment = parts.next().ok_or(IdError::MissingPath)?;
    if actor_type_segment.is_empty() {
        return Err(IdError::MissingPath);
    }
    let pun = parts.next().ok_or(IdError::InvalidPun)?;
    if !is_valid_pun(pun, true) {
        return Err(IdError::InvalidPun);
    }
    let after_pun = parts.next();
    if after_pun.is_some() {
        return Err(IdError::InvalidPun);
    }
    Ok(pun.to_string())
}

pub fn create_ap_object_id(
    host_name: &str,
    ap_object_type: &ApObjectType,
    local_id: &str,
) -> String {
    let ap_object_segment = match ap_object_type {
        ApObjectType::Article => AP_ARTICLE_SEGMENT,
        ApObjectType::Note => AP_NOTE_SEGMENT,
        ApObjectType::Document => AP_DOCUMENT_SEGMENT,
        ApObjectType::Image => AP_IMAGE_SEGMENT,
        ApObjectType::Video => AP_VIDEO_SEGMENT,
        ApObjectType::Audio => AP_AUDIO_SEGMENT,
        ApObjectType::Page => AP_PAGE_SEGMENT,
    };
    format!(
        "{}/{}/{}/{}",
        create_base_url(host_name),
        AP_SEGMENT,
        ap_object_segment,
        local_id
    )
}

pub fn parse_ap_object_id_host(ap_object_id: &str) -> Result<String, IdError> {
    if ap_object_id.starts_with(HTTPS_PREFIX) {
        ap_object_id_host(ap_object_id, HTTPS_PREFIX)
    } else if ap_object_id.starts_with(HTTP_PREFIX) {
        ap_object_id_host(ap_object_id, HTTP_PREFIX)
    } else {
        Err(IdError::BadHostPrefix)
    }
}

fn ap_object_id_host(ap_object_id: &str, prefix: &str) -> Result<String, IdError> {
    let no_prefix = ap_object_id
        .strip_prefix(prefix)
        .ok_or(IdError::BadHostPrefix)?;
    let parts = no_prefix.split_once('/').ok_or(IdError::MissingPath)?;
    if parts.0.len() < 1 {
        Err(IdError::MissingPath)
    } else {
        Ok(parts.0.to_string())
    }
}

pub fn create_inbox(id: &str) -> String {
    format!("{}/{}", id, AP_INBOX_SEGMENT)
}

pub fn create_outbox(id: &str) -> String {
    format!("{}/{}", id, AP_OUTBOX_SEGMENT)
}

pub fn create_following(id: &str) -> String {
    format!("{}/{}", id, AP_FOLLOWING_SEGMENT)
}

pub fn create_followers(id: &str) -> String {
    format!("{}/{}", id, AP_FOLLOWERS_SEGMENT)
}

pub fn create_liked(id: &str) -> String {
    format!("{}/{}", id, AP_LIKED_SEGMENT)
}

pub fn create_likes(id: &str) -> String {
    format!("{}/{}", id, AP_LIKES_SEGMENT)
}

pub fn create_shared_inbox(host_name: &str) -> String {
    format!(
        "{}/{}/{}",
        create_base_url(host_name),
        AP_SEGMENT,
        AP_SHARED_INBOX_SEGMENT
    )
}

#[cfg(test)]
#[allow(non_snake_case)]
mod id_tests {
    use dialtone_test_util::test_constants::TEST_HOSTNAME;
    use dialtone_test_util::test_constants::TEST_NOROLEUSER_PUN;

    use super::create_actor_id;
    use super::parse_actors_host;
    use super::parse_actors_pun;
    use super::parse_ap_object_id_host;
    use crate::ap::ap_object::ApObjectType;
    use crate::ap::id::create_ap_object_id;
    use crate::ap::pun::create_preferred_user_name;

    #[test]
    fn GIVEN_person_pun_WHEN_create_actor_id_THEN_segment_is_p() {
        // GIVEN
        let host_name = "example.com";
        let name = "testy_mctestalot";
        let pun = create_preferred_user_name(name, &crate::ap::ActorType::Person);

        // WHEN
        let actor_id = create_actor_id(host_name, &pun);

        // THEN
        assert_eq!("https://example.com/pub/p/testy_mctestalot", actor_id);
    }

    #[test]
    fn GIVEN_group_pun_WHEN_create_actor_id_THEN_segment_is_g() {
        // GIVEN
        let host_name = "example.com";
        let name = "testy_mctestalot";
        let pun = create_preferred_user_name(name, &crate::ap::ActorType::Group);

        // WHEN
        let actor_id = create_actor_id(host_name, &pun);

        // THEN
        assert_eq!("https://example.com/pub/g/testy_mctestalot", actor_id);
    }

    #[test]
    fn GIVEN_service_pun_WHEN_create_actor_id_THEN_segment_is_s() {
        // GIVEN
        let host_name = "example.com";
        let name = "testy_mctestalot";
        let pun = create_preferred_user_name(name, &crate::ap::ActorType::Service);

        // WHEN
        let actor_id = create_actor_id(host_name, &pun);

        // THEN
        assert_eq!("https://example.com/pub/s/testy_mctestalot", actor_id);
    }

    #[test]
    fn GIVEN_application_pun_WHEN_create_actor_id_THEN_segment_is_a() {
        // GIVEN
        let host_name = "example.com";
        let name = "testy_mctestalot";
        let pun = create_preferred_user_name(name, &crate::ap::ActorType::Application);

        // WHEN
        let actor_id = create_actor_id(host_name, &pun);

        // THEN
        assert_eq!("https://example.com/pub/a/testy_mctestalot", actor_id);
    }

    #[test]
    fn GIVEN_organization_pun_WHEN_create_actor_id_THEN_segment_is_o() {
        // GIVEN
        let host_name = "example.com";
        let name = "testy_mctestalot";
        let pun = create_preferred_user_name(name, &crate::ap::ActorType::Organization);

        // WHEN
        let actor_id = create_actor_id(host_name, &pun);

        // THEN
        assert_eq!("https://example.com/pub/o/testy_mctestalot", actor_id);
    }

    #[test]
    fn GIVEN_person_actor_id_WHEN_parse_pun_THEN_return_pun() {
        // GIVEN
        let actor_id = create_actor_id(TEST_HOSTNAME, TEST_NOROLEUSER_PUN);

        // WHEN
        let pun = parse_actors_pun(&actor_id).unwrap();

        // THEN
        assert_eq!(pun, TEST_NOROLEUSER_PUN);
    }

    #[test]
    fn GIVEN_actor_id_with_no_host_WHEN_parse_pun_THEN_parsing_error() {
        // GIVEN
        let actor_id = "https:///p/test";

        // WHEN
        let pun = parse_actors_pun(&actor_id);

        // THEN
        assert!(pun.is_err());
    }

    #[test]
    fn GIVEN_actor_id_with_no_bad_pun_WHEN_parse_pun_THEN_parsing_error() {
        // GIVEN
        let actor_id = "https://example.com/p/test::::";

        // WHEN
        let pun = parse_actors_pun(&actor_id);

        // THEN
        assert!(pun.is_err());
    }

    #[test]
    fn GIVEN_actor_id_with_stuff_after_pun_WHEN_parse_pun_THEN_parsing_error() {
        // GIVEN
        let actor_id = "https://example.com/ap/p/test/stuff";

        // WHEN
        let pun = parse_actors_pun(&actor_id);

        // THEN
        assert!(pun.is_err());
    }

    #[test]
    fn test_create_ap_object_ids() {
        let host_name = "example.com";
        let local_part = "a_thing_i_did";
        assert_eq!(
            "https://example.com/pub/note/a_thing_i_did",
            create_ap_object_id(host_name, &ApObjectType::Note, local_part)
        );
        assert_eq!(
            "https://example.com/pub/article/a_thing_i_did",
            create_ap_object_id(host_name, &ApObjectType::Article, local_part)
        );
        assert_eq!(
            "https://example.com/pub/document/a_thing_i_did",
            create_ap_object_id(host_name, &ApObjectType::Document, local_part)
        );
    }

    #[test]
    fn GIVEN_ap_object_id_WHEN_parse_host_THEN_ok() {
        // GIVEN
        let local_part = "a_thing_i_did";
        let id = create_ap_object_id(TEST_HOSTNAME, &ApObjectType::Note, local_part);

        // WHEN
        let action = parse_ap_object_id_host(&id);

        // THEN
        assert!(action.is_ok());
        assert_eq!(action.unwrap(), TEST_HOSTNAME);
    }

    #[test]
    fn GIVEN_good_actor_id_with_https_WHEN_find_actors_host_THEN_ok() {
        let action = parse_actors_host("https://example.com/pub/p/foo");
        assert!(action.is_ok());
    }

    #[test]
    fn GIVEN_good_actor_id_with_http_WHEN_find_actors_host_THEN_ok() {
        let action = parse_actors_host("http://example.com/pub/p/foo");
        assert!(action.is_ok());
    }

    #[test]
    fn GIVEN_actor_id_with_no_scheme_WHEN_find_actors_host_THEN_error() {
        let action = parse_actors_host("example.com/pub/p/foo");
        assert!(action.is_err());
    }

    #[test]
    fn GIVEN_actor_id_with_no_host_WHEN_find_actors_host_THEN_error() {
        let action = parse_actors_host("http:///pub/p/foo");
        assert!(action.is_err());
    }
}