use crate::ser_display_deser_fromstr;
use std::{fmt::Display, str::FromStr};
#[derive(Debug)]
pub enum ErrorReason {
Scope,
Name,
}
impl Display for ErrorReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErrorReason::Scope => write!(f, "scope"),
ErrorReason::Name => write!(f, "name"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PackageName(String, String);
ser_display_deser_fromstr!(PackageName);
impl FromStr for PackageName {
type Err = errors::PackageNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (scope, name) = s
.split_once('/')
.ok_or_else(|| Self::Err::InvalidFormat(s.to_string()))?;
for (reason, part) in [(ErrorReason::Scope, scope), (ErrorReason::Name, name)] {
let min_len = match reason {
ErrorReason::Scope => 3,
ErrorReason::Name => 1,
};
if !(min_len..=32).contains(&part.len()) {
return Err(match reason {
ErrorReason::Scope => Self::Err::InvalidScopeLength(part.to_string()),
ErrorReason::Name => Self::Err::InvalidNameLength(part.to_string()),
});
}
if part.chars().all(|c| c.is_ascii_digit()) {
return Err(Self::Err::OnlyDigits(reason, part.to_string()));
}
if part.starts_with('_') || part.ends_with('_') {
return Err(Self::Err::PrePostfixUnderscore(reason, part.to_string()));
}
if !part
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
return Err(Self::Err::InvalidCharacters(reason, part.to_string()));
}
}
Ok(Self(scope.to_string(), name.to_string()))
}
}
impl Display for PackageName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.0, self.1)
}
}
#[cfg(test)]
impl schemars::JsonSchema for PackageName {
fn schema_name() -> std::borrow::Cow<'static, str> {
"PackageName".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": r"^(?!_)(?![0-9]+\/)[a-z0-9_]{3,32}(?<!_)\/(?!_)(?![0-9]+\/)[a-z0-9_]{1,32}(?<!_)$"
})
}
}
impl PackageName {
#[must_use]
pub fn as_str(&self) -> (&str, &str) {
(&self.0, &self.1)
}
#[must_use]
pub fn escaped(&self) -> String {
format!("{}+{}", self.0, self.1)
}
#[must_use]
pub fn scope(&self) -> &str {
&self.0
}
#[must_use]
pub fn name(&self) -> &str {
&self.1
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
#[cfg_attr(test, schemars(untagged))]
pub enum PackageNames {
Pesde(PackageName),
#[cfg(feature = "wally-compat")]
Wally(wally::WallyPackageName),
}
ser_display_deser_fromstr!(PackageNames);
impl PackageNames {
#[must_use]
pub fn as_str(&self) -> (&str, &str) {
match self {
PackageNames::Pesde(name) => name.as_str(),
#[cfg(feature = "wally-compat")]
PackageNames::Wally(name) => name.as_str(),
}
}
#[must_use]
pub fn escaped(&self) -> String {
match self {
PackageNames::Pesde(name) => name.escaped(),
#[cfg(feature = "wally-compat")]
PackageNames::Wally(name) => name.escaped(),
}
}
pub fn from_escaped(s: &str) -> Result<Self, errors::PackageNamesError> {
PackageNames::from_str(s.replacen('+', "/", 1).as_str())
}
#[must_use]
pub fn scope(&self) -> &str {
match self {
PackageNames::Pesde(name) => name.scope(),
#[cfg(feature = "wally-compat")]
PackageNames::Wally(name) => name.scope(),
}
}
#[must_use]
pub fn name(&self) -> &str {
match self {
PackageNames::Pesde(name) => name.name(),
#[cfg(feature = "wally-compat")]
PackageNames::Wally(name) => name.name(),
}
}
}
impl Display for PackageNames {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackageNames::Pesde(name) => write!(f, "{name}"),
#[cfg(feature = "wally-compat")]
PackageNames::Wally(name) => write!(f, "{name}"),
}
}
}
impl FromStr for PackageNames {
type Err = errors::PackageNamesError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
#[cfg(feature = "wally-compat")]
if let Some(wally_name) = s
.strip_prefix("wally#")
.or_else(|| s.contains('-').then_some(s))
.and_then(|s| wally::WallyPackageName::from_str(s).ok())
{
return Ok(PackageNames::Wally(wally_name));
}
if let Ok(name) = PackageName::from_str(s) {
Ok(PackageNames::Pesde(name))
} else {
Err(errors::PackageNamesError::InvalidPackageName(s.to_string()))
}
}
}
#[cfg(feature = "wally-compat")]
pub mod wally {
use std::{fmt::Display, str::FromStr};
use crate::{
names::{errors, ErrorReason},
ser_display_deser_fromstr,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct WallyPackageName(String, String);
ser_display_deser_fromstr!(WallyPackageName);
impl FromStr for WallyPackageName {
type Err = errors::WallyPackageNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (scope, name) = s
.strip_prefix("wally#")
.unwrap_or(s)
.split_once('/')
.ok_or_else(|| Self::Err::InvalidFormat(s.to_string()))?;
for (reason, part) in [(ErrorReason::Scope, scope), (ErrorReason::Name, name)] {
if part.is_empty() || part.len() > 64 {
return Err(Self::Err::InvalidLength(reason, part.to_string()));
}
if !part
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(Self::Err::InvalidCharacters(reason, part.to_string()));
}
}
Ok(Self(scope.to_string(), name.to_string()))
}
}
impl Display for WallyPackageName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "wally#{}/{}", self.0, self.1)
}
}
#[cfg(test)]
impl schemars::JsonSchema for WallyPackageName {
fn schema_name() -> std::borrow::Cow<'static, str> {
"WallyPackageName".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": r"^(wally#)?[a-z0-9-]{1,64}\/[a-z0-9-]{1,64}$"
})
}
}
impl WallyPackageName {
#[must_use]
pub fn as_str(&self) -> (&str, &str) {
(&self.0, &self.1)
}
#[must_use]
pub fn escaped(&self) -> String {
format!("wally#{}+{}", self.0, self.1)
}
#[must_use]
pub fn scope(&self) -> &str {
&self.0
}
#[must_use]
pub fn name(&self) -> &str {
&self.1
}
}
}
pub mod errors {
use thiserror::Error;
use crate::names::ErrorReason;
#[derive(Debug, Error)]
pub enum PackageNameError {
#[error("package name `{0}` is not in the format `scope/name`")]
InvalidFormat(String),
#[error("package {0} `{1}` contains characters outside a-z, 0-9, and _")]
InvalidCharacters(ErrorReason, String),
#[error("package {0} `{1}` contains only digits")]
OnlyDigits(ErrorReason, String),
#[error("package {0} `{1}` starts or ends with an underscore")]
PrePostfixUnderscore(ErrorReason, String),
#[error("package scope `{0}` is not within 3-32 characters long")]
InvalidScopeLength(String),
#[error("package name `{0}` is not within 1-32 characters long")]
InvalidNameLength(String),
}
#[cfg(feature = "wally-compat")]
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Error)]
pub enum WallyPackageNameError {
#[error("wally package name `{0}` is not in the format `scope/name`")]
InvalidFormat(String),
#[error("wally package {0} `{1}` contains characters outside a-z, 0-9, and -")]
InvalidCharacters(ErrorReason, String),
#[error("wally package {0} `{1}` is not within 1-64 characters long")]
InvalidLength(ErrorReason, String),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum PackageNamesError {
#[error("invalid package name {0}")]
InvalidPackageName(String),
}
}