use crate::consts;
use crate::utils::spanned::PixiSpanned;
use lazy_static::lazy_static;
use miette::Diagnostic;
use regex::Regex;
use serde::{self, Deserialize, Deserializer};
use serde_with::SerializeDisplay;
use std::borrow::Borrow;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use thiserror::Error;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, SerializeDisplay)]
pub enum EnvironmentName {
Default,
Named(String),
}
impl Hash for EnvironmentName {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state)
}
}
impl EnvironmentName {
pub fn as_str(&self) -> &str {
match self {
EnvironmentName::Default => consts::DEFAULT_ENVIRONMENT_NAME,
EnvironmentName::Named(name) => name.as_str(),
}
}
pub fn is_default(&self) -> bool {
matches!(self, EnvironmentName::Default)
}
pub fn fancy_display(&self) -> console::StyledObject<&str> {
console::style(self.as_str()).magenta()
}
}
impl Borrow<str> for EnvironmentName {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for EnvironmentName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EnvironmentName::Default => write!(f, "{}", consts::DEFAULT_ENVIRONMENT_NAME),
EnvironmentName::Named(name) => write!(f, "{}", name),
}
}
}
impl PartialEq<str> for EnvironmentName {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
#[derive(Debug, Clone, Error, Diagnostic, PartialEq)]
#[error("Failed to parse environment name '{attempted_parse}', please use only lowercase letters, numbers and dashes")]
pub struct ParseEnvironmentNameError {
pub attempted_parse: String,
}
impl FromStr for EnvironmentName {
type Err = ParseEnvironmentNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
static ref REGEX: Regex = Regex::new(r"^[a-z0-9-]+$").expect("Regex should be able to compile"); }
if !REGEX.is_match(s) {
return Err(ParseEnvironmentNameError {
attempted_parse: s.to_string(),
});
}
match s {
consts::DEFAULT_ENVIRONMENT_NAME => Ok(EnvironmentName::Default),
_ => Ok(EnvironmentName::Named(s.to_string())),
}
}
}
impl<'de> Deserialize<'de> for EnvironmentName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let name = String::deserialize(deserializer)?;
EnvironmentName::from_str(&name).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone)]
pub struct Environment {
pub name: EnvironmentName,
pub features: Vec<String>,
pub features_source_loc: Option<std::ops::Range<usize>>,
pub solve_group: Option<usize>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub(super) struct TomlEnvironment {
#[serde(default)]
pub features: PixiSpanned<Vec<String>>,
pub solve_group: Option<String>,
}
pub(super) enum TomlEnvironmentMapOrSeq {
Map(TomlEnvironment),
Seq(Vec<String>),
}
impl<'de> Deserialize<'de> for TomlEnvironmentMapOrSeq {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
serde_untagged::UntaggedEnumVisitor::new()
.map(|map| map.deserialize().map(TomlEnvironmentMapOrSeq::Map))
.seq(|seq| seq.deserialize().map(TomlEnvironmentMapOrSeq::Seq))
.expecting("either a map or a sequence")
.deserialize(deserializer)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_environment_name_from_str() {
assert_eq!(
EnvironmentName::from_str("default").unwrap(),
EnvironmentName::Default
);
assert_eq!(
EnvironmentName::from_str("foo").unwrap(),
EnvironmentName::Named("foo".to_string())
);
assert_eq!(
EnvironmentName::from_str("foo_bar").unwrap_err(),
ParseEnvironmentNameError {
attempted_parse: "foo_bar".to_string()
}
);
assert!(EnvironmentName::from_str("foo-bar").is_ok());
assert!(EnvironmentName::from_str("foo1").is_ok());
assert!(EnvironmentName::from_str("py39").is_ok());
assert!(EnvironmentName::from_str("foo bar").is_err());
assert!(EnvironmentName::from_str("foo_bar").is_err());
assert!(EnvironmentName::from_str("foo/bar").is_err());
assert!(EnvironmentName::from_str("foo\\bar").is_err());
assert!(EnvironmentName::from_str("foo:bar").is_err());
assert!(EnvironmentName::from_str("foo;bar").is_err());
assert!(EnvironmentName::from_str("foo?bar").is_err());
assert!(EnvironmentName::from_str("foo!bar").is_err());
assert!(EnvironmentName::from_str("py3.9").is_err());
assert!(EnvironmentName::from_str("py-3.9").is_err());
assert!(EnvironmentName::from_str("py_3.9").is_err());
assert!(EnvironmentName::from_str("py/3.9").is_err());
assert!(EnvironmentName::from_str("py\\3.9").is_err());
assert!(EnvironmentName::from_str("Py").is_err());
assert!(EnvironmentName::from_str("Py3").is_err());
assert!(EnvironmentName::from_str("Py39").is_err());
assert!(EnvironmentName::from_str("Py-39").is_err());
}
#[test]
fn test_environment_name_as_str() {
assert_eq!(EnvironmentName::Default.as_str(), "default");
assert_eq!(EnvironmentName::Named("foo".to_string()).as_str(), "foo");
}
#[test]
fn test_deserialize_environment_name() {
assert_eq!(
serde_json::from_str::<EnvironmentName>("\"default\"").unwrap(),
EnvironmentName::Default
);
assert_eq!(
serde_json::from_str::<EnvironmentName>("\"foo\"").unwrap(),
EnvironmentName::Named("foo".to_string())
);
}
}