use std::{
fmt::{Debug, Display},
fs::File,
io::{BufRead, BufReader, Read},
ops::{Deref, DerefMut},
path::Path,
str::FromStr,
};
use thiserror::Error;
use url::Url;
#[derive(Clone, PartialEq, Eq)]
pub struct PacstallRepos(Vec<PacstallRepo>);
#[derive(Clone, PartialEq, Eq)]
pub struct PacstallRepo {
url: Url,
alias: Option<String>,
}
#[derive(Debug, Error)]
pub enum RepoEntryError {
#[error("missing URL")]
MissingUrl,
#[error("missing @ sign")]
MissingAtSign,
#[error("too many parts")]
TooManyParts,
#[error(transparent)]
ParseError(#[from] url::ParseError),
#[error("empty alias")]
EmptyAlias,
#[error("path is not absolute")]
NotAbsolute,
#[error("not a path")]
NotPath,
}
#[derive(Debug, Error)]
pub enum RepoFileParseError {
#[error("parse error")]
ParseError(#[from] RepoEntryError),
#[error("io error")]
IoError(#[from] std::io::Error),
}
impl Deref for PacstallRepos {
type Target = Vec<PacstallRepo>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PacstallRepos {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Display for PacstallRepos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for repo in &self.0 {
if !first {
writeln!(f)?;
}
write!(f, "{repo}")?;
first = false;
}
Ok(())
}
}
impl Display for PacstallRepo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.alias {
Some(alias) => write!(f, "{} @{alias}", self.url),
None => write!(f, "{}", self.url),
}
}
}
impl PartialEq<Url> for PacstallRepo {
fn eq(&self, other: &Url) -> bool {
self.url == *other
}
}
impl Default for PacstallRepo {
fn default() -> Self {
Self {
url: Url::parse("https://raw.githubusercontent.com/pacstall/pacstall-programs/master")
.expect("could not parse default URL"),
alias: Some(String::from("pacstall")),
}
}
}
impl Default for PacstallRepos {
fn default() -> Self {
Self(vec![PacstallRepo::default()])
}
}
impl From<PacstallRepo> for PacstallRepos {
fn from(value: PacstallRepo) -> Self {
Self(vec![value])
}
}
impl FromIterator<PacstallRepo> for PacstallRepos {
fn from_iter<T: IntoIterator<Item = PacstallRepo>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl IntoIterator for PacstallRepos {
type Item = PacstallRepo;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromStr for PacstallRepo {
type Err = RepoEntryError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split_whitespace();
let url = Url::parse(parts.next().ok_or(Self::Err::MissingUrl)?)?;
match parts.next() {
Some(alias) => {
if !alias.starts_with('@') {
return Err(Self::Err::MissingAtSign);
}
if parts.next().is_some() {
return Err(Self::Err::TooManyParts);
}
Ok(Self {
url,
alias: Some(alias.strip_prefix('@').unwrap().to_string()),
})
}
None => Ok(Self { url, alias: None }),
}
}
}
impl TryFrom<File> for PacstallRepos {
type Error = RepoFileParseError;
fn try_from(value: File) -> Result<Self, Self::Error> {
Self::open(value)
}
}
impl PacstallRepo {
pub fn from_url<S: Into<String>>(url: Url, alias: Option<S>) -> Self {
Self {
url,
alias: alias.map(Into::into),
}
}
pub fn from_path<P, S>(path: P, alias: Option<S>) -> Result<Self, ()>
where
P: AsRef<Path>,
S: Into<String>,
{
Ok(Self {
url: Url::from_directory_path(path)?,
alias: alias.map(Into::into),
})
}
pub fn set_alias<S: Into<String>>(
&mut self,
alias: S,
) -> Result<Option<String>, RepoEntryError> {
let old = self.alias.clone();
let alias: String = alias.into();
if alias.is_empty() {
Err(RepoEntryError::EmptyAlias)
} else {
self.alias = Some(alias);
Ok(old)
}
}
#[must_use]
pub fn has_alias(&self) -> bool {
self.alias.is_some()
}
#[must_use]
pub fn alias(&self) -> Option<&str> {
self.alias.as_deref()
}
#[must_use]
pub fn url(&self) -> &Url {
&self.url
}
#[must_use]
pub fn is_path(&self) -> bool {
self.url.scheme() == "file"
}
pub fn as_path(&self) -> Result<&Path, RepoEntryError> {
if self.is_path() {
Ok(Path::new(self.url.path()))
} else {
Err(RepoEntryError::NotPath)
}
}
}
impl PacstallRepos {
pub fn open<R: Read>(contents: R) -> Result<Self, RepoFileParseError> {
Ok(Self(
BufReader::new(contents)
.lines()
.map(|line| {
line.map_err(RepoFileParseError::IoError)?
.parse()
.map_err(RepoFileParseError::ParseError)
})
.collect::<Result<_, _>>()?,
))
}
}