dxm-resources 0.2.3

Crate for installing third-party resources for FXServer.
Documentation
use std::{error::Error, fmt::Display};

use reqwest::blocking::Client;

mod api;

#[derive(Debug)]
pub enum InvalidGithubUrlErrorKind {
    InvalidLink,
    NoAuthor,
    NoName,
}

impl Display for InvalidGithubUrlErrorKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let str = match self {
            InvalidGithubUrlErrorKind::InvalidLink => "invalid link",
            InvalidGithubUrlErrorKind::NoAuthor => "no repository author",
            InvalidGithubUrlErrorKind::NoName => "no repository name",
        };

        write!(f, "{}", str)?;

        Ok(())
    }
}

#[derive(Debug)]
pub struct InvalidGithubUrlError {
    kind: InvalidGithubUrlErrorKind,
}

impl InvalidGithubUrlError {
    pub fn new(kind: InvalidGithubUrlErrorKind) -> Self {
        Self { kind }
    }

    pub fn kind(&self) -> &InvalidGithubUrlErrorKind {
        &self.kind
    }
}

impl Display for InvalidGithubUrlError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "invalid github url: {}", self.kind())?;

        Ok(())
    }
}

impl Error for InvalidGithubUrlError {}

pub fn resolve_download_url<S>(client: &Client, url: S) -> Result<Option<String>, Box<dyn Error>>
where
    S: AsRef<str>,
{
    let url = url.as_ref();

    if let Some((prefix, repo)) = url.trim().split_once("github.com") {
        if !prefix.is_empty() && !prefix.ends_with("//") {
            Err(InvalidGithubUrlError::new(
                InvalidGithubUrlErrorKind::InvalidLink,
            ))?;
        }

        let parts: Vec<&str> = repo.split('/').collect();

        let repo_author = parts
            .get(1)
            .filter(|s| !s.is_empty())
            .ok_or_else(|| InvalidGithubUrlError::new(InvalidGithubUrlErrorKind::NoAuthor))?;
        let repo_name = parts
            .get(2)
            .filter(|s| !s.is_empty())
            .ok_or_else(|| InvalidGithubUrlError::new(InvalidGithubUrlErrorKind::NoName))?;
        let repo_name = repo_name.strip_suffix(".git").unwrap_or(repo_name);
        let repo = format!("{}/{}", repo_author, repo_name);

        let link_type = parts.get(3).unwrap_or(&"");
        let is_release = link_type == &"releases";
        let link_data = parts.get(4).unwrap_or(&"");
        let release_data = if link_data == &"tag" || link_data == &"download" {
            parts.get(5).unwrap_or(&"")
        } else {
            link_data
        };

        if link_type == &"archive" {
            return Ok(Some(url.into()));
        }

        let result = if link_type.is_empty()
            || link_data.is_empty()
            || (is_release && release_data.is_empty())
        {
            api::get_latest_release_archive_url(client, &repo)
                .or_else(|_| api::get_default_branch_archive_url(client, repo))?
        } else if is_release {
            api::get_release_archive_url(client, repo, release_data)?
        } else {
            api::get_branch_or_commit_archive_url(client, repo, link_data)?
        };

        Ok(Some(result))
    } else {
        Ok(None)
    }
}

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

    #[test]
    #[should_panic = "InvalidLink"]
    fn resolve_returns_error_for_invalid_link() {
        resolve_download_url(&Client::new(), "/github.com/").unwrap();
    }

    #[test]
    #[should_panic = "NoAuthor"]
    fn resolve_returns_error_for_no_author() {
        resolve_download_url(&Client::new(), "https://github.com").unwrap();
    }

    #[test]
    #[should_panic = "NoAuthor"]
    fn resolve_returns_error_for_empty_author() {
        resolve_download_url(&Client::new(), "https://github.com/").unwrap();
    }

    #[test]
    #[should_panic = "NoName"]
    fn resolve_returns_error_for_no_repository() {
        resolve_download_url(&Client::new(), "https://github.com/example").unwrap();
    }

    #[test]
    #[should_panic = "NoName"]
    fn resolve_returns_error_for_empty_repository() {
        resolve_download_url(&Client::new(), "https://github.com/example/").unwrap();
    }
}