aiken/crates/aiken-project/src/package_name.rs

151 lines
4.4 KiB
Rust

use owo_colors::{OwoColorize, Stream::Stdout};
use serde::{de::Visitor, Deserialize, Serialize};
use std::{
fmt::{self, Display},
str::FromStr,
};
use thiserror::Error;
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub struct PackageName {
pub owner: String,
pub repo: String,
}
impl PackageName {
fn validate(&self) -> Result<(), Error> {
let r = regex::Regex::new("^[a-z0-9_-]+$").expect("regex could not be compiled");
if !(r.is_match(&self.owner) && r.is_match(&self.repo)) {
return Err(Error::InvalidProjectName {
reason: InvalidProjectNameReason::Format,
name: self.to_string(),
});
}
Ok(())
}
}
impl FromStr for PackageName {
type Err = Error;
fn from_str(name: &str) -> Result<Self, Error> {
let mut name_split = name.split('/');
let owner = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
let repo = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
let package_name = PackageName { owner, repo };
package_name.validate()?;
Ok(package_name)
}
}
impl Display for PackageName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.owner, self.repo)
}
}
impl Serialize for PackageName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PackageName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct PackageNameVisitor;
impl<'de> Visitor<'de> for PackageNameVisitor {
type Value = PackageName;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter
.write_str("a string representing an owner and repo, ex: aiken-lang/stdlib")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let mut name = v.split('/');
let owner = name.next().ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
})?;
let repo = name.next().ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
})?;
Ok(PackageName {
owner: owner.to_string(),
repo: repo.to_string(),
})
}
}
deserializer.deserialize_str(PackageNameVisitor)
}
}
#[derive(Debug, Error, miette::Diagnostic)]
pub enum Error {
#[error(
"{} is not a valid project name: {}",
name.if_supports_color(Stdout, |s| s.red()),
reason.to_string()
)]
InvalidProjectName {
name: String,
reason: InvalidProjectNameReason,
},
#[error(
"A project named {} already exists.",
name.if_supports_color(Stdout, |s| s.red())
)]
ProjectExists { name: String },
}
#[derive(Debug, Clone, Copy)]
pub enum InvalidProjectNameReason {
Reserved,
Format,
}
impl fmt::Display for InvalidProjectNameReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
InvalidProjectNameReason::Reserved => write!(f, "It's a reserved word in Aiken."),
InvalidProjectNameReason::Format => write!(
f,
"It is malformed.\n\nProjects must be named as:\n\n\t\
{}/{}\n\nEach part must start with a lowercase letter \
and may only contain lowercase letters, numbers, hyphens or underscores.\
\nFor example,\n\n\t{}",
"{owner}".if_supports_color(Stdout, |s| s.bright_blue()),
"{project}".if_supports_color(Stdout, |s| s.bright_blue()),
"aiken-lang/stdlib".if_supports_color(Stdout, |s| s.bright_blue()),
),
}
}
}