use std::str::FromStr; use thiserror::Error; use crate::prelude::RepoId; use crate::{cob, git}; #[derive(Debug, Error)] pub enum ExplorerError { #[error("invalid explorer URL {0:?}: unknown protocol")] UnknownProtocol(String), #[error("invalid explorer URL {0:?}: missing `$host` component")] MissingHost(String), #[error("invalid explorer URL {0:?}: missing `$rid` component")] MissingRid(String), #[error("invalid explorer URL {0:?}: missing `$path` component")] MissingPath(String), } /// A resource such as a branch, patch or commit. #[derive(Debug, Hash, PartialEq, Eq)] pub enum ExplorerResource { /// Git tree object. Used for the repository root. Tree { oid: git::Oid }, /// A Patch COB. Patch { id: cob::ObjectId }, } impl std::fmt::Display for ExplorerResource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Tree { oid } => { write!(f, "/tree/{oid}") } Self::Patch { id } => { write!(f, "/patches/{id}") } } } } /// A URL to a specific repository or resource within a repository. #[derive(Debug, PartialEq, Eq)] pub struct ExplorerUrl { /// URL template. pub template: Explorer, /// Host serving the repository. pub host: String, /// Repository. pub rid: RepoId, /// Resource under the repository. pub resource: Option, } impl ExplorerUrl { /// Set a resource on for this URL. pub fn resource(mut self, resource: ExplorerResource) -> Self { self.resource = Some(resource); self } } impl std::fmt::Display for ExplorerUrl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str( self.template .0 .replace("$host", &self.host) .replace("$rid", self.rid.urn().as_str()) .replace( "$path", self.resource .as_ref() .map(|r| r.to_string()) .as_deref() .unwrap_or(""), ) .as_str(), ) } } /// A public explorer, eg. `https://app.radicle.xyz`. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] #[serde(transparent)] pub struct Explorer(String); impl Default for Explorer { fn default() -> Self { Self(String::from( "https://app.radicle.xyz/nodes/$host/$rid$path", )) } } impl Explorer { /// Get the explorer URL, filling in the host and RID. pub fn url(&self, host: impl ToString, rid: RepoId) -> ExplorerUrl { ExplorerUrl { template: self.clone(), host: host.to_string(), rid, resource: None, } } } impl FromStr for Explorer { type Err = ExplorerError; fn from_str(s: &str) -> Result { let url = s.to_owned(); if !url.starts_with("http://") && !url.starts_with("https://") { return Err(ExplorerError::UnknownProtocol(url)); } if !url.contains("$host") { return Err(ExplorerError::MissingHost(url)); } if !url.contains("$rid") { return Err(ExplorerError::MissingRid(url)); } if !url.contains("$path") { return Err(ExplorerError::MissingPath(url)); } Ok(Explorer(url)) } }