Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src storage git transport local url.rs
//! Git local transport URLs.
use std::fmt;
use std::str::FromStr;

use thiserror::Error;

use crate::{
    crypto,
    identity::{IdError, RepoId},
};

/// Repository namespace.
type Namespace = crypto::PublicKey;

#[derive(Debug, Error)]
pub enum UrlError {
    /// Invalid format.
    #[error("invalid url format: expected `rad://<repo>[/<namespace>]`")]
    InvalidFormat,
    /// Unsupported URL scheme.
    #[error("unsupported scheme: expected `rad://`")]
    UnsupportedScheme,
    /// Invalid repository identifier.
    #[error("repo: {0}")]
    InvalidRepository(#[source] IdError),
    /// Invalid namespace.
    #[error("namespace: {0}")]
    InvalidNamespace(#[source] crypto::PublicKeyError),
}

/// A git local transport URL.
///
/// * Used to content-address a repository, eg. when sharing projects.
/// * Used as a remote url in a git working copy.
///
/// `rad://<repo>[/<namespace>]`
///
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Url {
    /// Repository identifier.
    pub repo: RepoId,
    /// Repository sub-tree.
    pub namespace: Option<Namespace>,
}

impl Url {
    /// URL scheme.
    pub const SCHEME: &'static str = "rad";

    /// Return this URL with the given namespace added.
    pub fn with_namespace(mut self, namespace: Namespace) -> Self {
        self.namespace = Some(namespace);
        self
    }
}

impl From<RepoId> for Url {
    fn from(repo: RepoId) -> Self {
        Self {
            repo,
            namespace: None,
        }
    }
}

impl fmt::Display for Url {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(ns) = self.namespace {
            write!(f, "{}://{}/{}", Self::SCHEME, self.repo.canonical(), ns)
        } else {
            write!(f, "{}://{}", Self::SCHEME, self.repo.canonical())
        }
    }
}

impl FromStr for Url {
    type Err = UrlError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let rest = s
            .strip_prefix("rad://")
            .ok_or(UrlError::UnsupportedScheme)?;
        let components = rest.split('/').collect::<Vec<_>>();

        let (resource, namespace) = match components.as_slice() {
            [resource] => (resource, None),
            [resource, namespace] => (resource, Some(namespace)),
            _ => return Err(UrlError::InvalidFormat),
        };

        let resource = RepoId::from_canonical(resource).map_err(UrlError::InvalidRepository)?;
        let namespace = namespace
            .map(|pk| Namespace::from_str(pk).map_err(UrlError::InvalidNamespace))
            .transpose()?;

        Ok(Url {
            repo: resource,
            namespace,
        })
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
    use super::*;

    #[test]
    fn test_url_parse() {
        let repo = RepoId::from_canonical("z2w8RArM3gaBXZxXhQUswE3hhLcss").unwrap();
        let namespace =
            Namespace::from_str("z6Mkifeb5NPS6j7JP72kEQEeuqMTpCAVcHsJi1C86jGTzHRi").unwrap();

        let url = format!("rad://{}", repo.canonical());
        let url = Url::from_str(&url).unwrap();

        assert_eq!(url.repo, repo);
        assert_eq!(url.namespace, None);

        let url = format!("rad://{}/{namespace}", repo.canonical());
        let url = Url::from_str(&url).unwrap();

        assert_eq!(url.repo, repo);
        assert_eq!(url.namespace, Some(namespace));

        assert!(
            format!("heartwood://{}", repo.canonical())
                .parse::<Url>()
                .is_err()
        );
        assert!(
            format!("git://{}", repo.canonical())
                .parse::<Url>()
                .is_err()
        );
        assert!(format!("rad://{namespace}").parse::<Url>().is_err());
        assert!(
            format!("rad://{}/{namespace}/fnord", repo.canonical())
                .parse::<Url>()
                .is_err()
        );
    }

    #[test]
    fn test_url_to_string() {
        let repo = RepoId::from_canonical("z2w8RArM3gaBXZxXhQUswE3hhLcss").unwrap();
        let namespace =
            Namespace::from_str("z6Mkifeb5NPS6j7JP72kEQEeuqMTpCAVcHsJi1C86jGTzHRi").unwrap();

        let url = Url {
            repo,
            namespace: None,
        };
        assert_eq!(url.to_string(), format!("rad://{}", repo.canonical()));

        let url = Url {
            repo,
            namespace: Some(namespace),
        };
        assert_eq!(
            url.to_string(),
            format!("rad://{}/{namespace}", repo.canonical())
        );
    }
}