use std::{
borrow::Cow,
convert::Infallible,
ffi::OsString,
fmt::{self, Display, Formatter},
path::{Path, PathBuf},
str::FromStr,
};
use serde::{Deserialize, Deserializer, Serialize};
use url::Url;
use crate::serde::FromStrVisitor;
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum Context {
Path(PathBuf),
#[allow(rustdoc::bare_urls, clippy::doc_markdown)]
Url(Url),
}
impl Context {
pub fn parse<T>(context: T) -> Self
where
T: AsRef<str> + Into<PathBuf>,
{
context
.as_ref()
.parse()
.map_or_else(|_| Self::Path(context.into()), Self::Url)
}
#[must_use]
pub const fn as_path(&self) -> Option<&PathBuf> {
if let Self::Path(v) = self {
Some(v)
} else {
None
}
}
#[must_use]
pub const fn as_url(&self) -> Option<&Url> {
if let Self::Url(v) = self {
Some(v)
} else {
None
}
}
#[must_use]
pub fn is_git_repo_url(&self) -> bool {
self.as_url().is_some_and(|url| {
let scheme = url.scheme();
(scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https"))
&& Path::new(url.path())
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("git"))
})
}
#[must_use]
pub fn branch_or_tag(&self) -> Option<&str> {
if !self.is_git_repo_url() {
return None;
}
let fragment = self.as_url()?.fragment()?;
fragment
.split_once(':')
.unzip()
.0
.map_or(Some(fragment), Some)
}
#[must_use]
pub fn subdirectory(&self) -> Option<&str> {
if !self.is_git_repo_url() {
return None;
}
self.as_url()?.fragment()?.split_once(':').unzip().1
}
pub fn into_string(self) -> Result<String, Self> {
match self {
Self::Path(path) => OsString::from(path)
.into_string()
.map_err(|path| Self::Path(path.into())),
Self::Url(url) => Ok(url.into()),
}
}
}
impl Default for Context {
fn default() -> Self {
Self::Path(".".into())
}
}
impl From<&str> for Context {
fn from(value: &str) -> Self {
Self::parse(value)
}
}
impl From<String> for Context {
fn from(value: String) -> Self {
Self::parse(value)
}
}
impl From<Box<str>> for Context {
fn from(value: Box<str>) -> Self {
Self::parse(value.into_string())
}
}
impl From<Cow<'_, str>> for Context {
fn from(value: Cow<str>) -> Self {
value
.parse()
.map_or_else(|_| Self::Path(value.into_owned().into()), Self::Url)
}
}
impl From<&Path> for Context {
fn from(value: &Path) -> Self {
Self::Path(value.to_owned())
}
}
impl From<Cow<'_, Path>> for Context {
fn from(value: Cow<Path>) -> Self {
Self::Path(value.into_owned())
}
}
impl From<PathBuf> for Context {
fn from(value: PathBuf) -> Self {
Self::Path(value)
}
}
impl From<Url> for Context {
fn from(value: Url) -> Self {
Self::Url(value)
}
}
impl FromStr for Context {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.into())
}
}
impl<'de> Deserialize<'de> for Context {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
FromStrVisitor::new("a string representing a path or URL").deserialize(deserializer)
}
}
impl Display for Context {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Path(path) => path.display().fmt(f),
Self::Url(url) => url.fmt(f),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn url_components() {
let context = Context::Url(
"https://github.com/example/example.git#branch_or_tag:subdirectory"
.parse()
.unwrap(),
);
assert_eq!(context.branch_or_tag(), Some("branch_or_tag"));
assert_eq!(context.subdirectory(), Some("subdirectory"));
}
}