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 { 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(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for PackageName { fn deserialize(deserializer: D) -> Result 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(self, v: &str) -> Result 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()), ), } } }