#[cfg(feature = "schemars")] use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::path::PathBuf; use std::str::FromStr; use std::sync::LazyLock; use serde::ser::SerializeSeq; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uv_small_str::SmallString; use crate::{ InvalidNameError, InvalidPipGroupError, InvalidPipGroupPathError, validate_and_normalize_ref, }; /// The normalized name of a dependency group. /// /// See: /// - /// - #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[rkyv(derive(Debug))] pub struct GroupName(SmallString); impl GroupName { /// Create a validated, normalized group name. /// /// At present, this is no more efficient than calling [`GroupName::from_str`]. #[allow(clippy::needless_pass_by_value)] pub fn from_owned(name: String) -> Result { validate_and_normalize_ref(&name).map(Self) } /// Return the underlying group name as a string. pub fn as_str(&self) -> &str { &self.0 } } impl FromStr for GroupName { type Err = InvalidNameError; fn from_str(name: &str) -> Result { validate_and_normalize_ref(name).map(Self) } } impl<'de> Deserialize<'de> for GroupName { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct Visitor; impl serde::de::Visitor<'_> for Visitor { type Value = GroupName; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str("a string") } fn visit_str(self, v: &str) -> Result { GroupName::from_str(v).map_err(serde::de::Error::custom) } fn visit_string(self, v: String) -> Result { GroupName::from_owned(v).map_err(serde::de::Error::custom) } } deserializer.deserialize_str(Visitor) } } impl Serialize for GroupName { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.0.serialize(serializer) } } impl std::fmt::Display for GroupName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl AsRef for GroupName { fn as_ref(&self) -> &str { &self.0 } } /// The pip-compatible variant of a [`GroupName`]. /// /// Either or :. /// If is omitted it defaults to "pyproject.toml". #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct PipGroupName { pub path: Option, pub name: GroupName, } impl FromStr for PipGroupName { type Err = InvalidPipGroupError; fn from_str(path_and_name: &str) -> Result { // The syntax is `:`. // // `:` isn't valid as part of a dependency-group name, but it can appear in a path. // Therefore we look for the first `:` starting from the end to find the delimiter. // If there is no `:` then there's no path and we use the default one. if let Some((path, name)) = path_and_name.rsplit_once(':') { // pip hard errors if the path does not end with pyproject.toml if !path.ends_with("pyproject.toml") { Err(InvalidPipGroupPathError(path.to_owned()))?; } let name = GroupName::from_str(name)?; let path = Some(PathBuf::from(path)); Ok(Self { path, name }) } else { let name = GroupName::from_str(path_and_name)?; let path = None; Ok(Self { path, name }) } } } impl<'de> Deserialize<'de> for PipGroupName { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; Self::from_str(&s).map_err(serde::de::Error::custom) } } impl Serialize for PipGroupName { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let string = self.to_string(); string.serialize(serializer) } } impl Display for PipGroupName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Some(path) = &self.path { write!(f, "{}:{}", path.display(), self.name) } else { self.name.fmt(f) } } } /// Either the literal "all" or a list of groups #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum DefaultGroups { /// All groups are defaulted All, /// A list of groups List(Vec), } #[cfg(feature = "schemars")] impl schemars::JsonSchema for DefaultGroups { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("DefaultGroups") } fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "description": "Either the literal \"all\" or a list of groups", "oneOf": [ { "description": "All groups are defaulted", "type": "string", "const": "all" }, { "description": "A list of groups", "type": "array", "items": generator.subschema_for::() } ] }) } } /// Serialize a [`DefaultGroups`] struct into a list of marker strings. impl serde::Serialize for DefaultGroups { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { Self::All => serializer.serialize_str("all"), Self::List(groups) => { let mut seq = serializer.serialize_seq(Some(groups.len()))?; for group in groups { seq.serialize_element(&group)?; } seq.end() } } } } /// Deserialize a "all" or list of [`GroupName`] into a [`DefaultGroups`] enum. impl<'de> serde::Deserialize<'de> for DefaultGroups { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct StringOrVecVisitor; impl<'de> serde::de::Visitor<'de> for StringOrVecVisitor { type Value = DefaultGroups; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str(r#"the string "all" or a list of strings"#) } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { if value != "all" { return Err(serde::de::Error::custom( r#"default-groups must be "all" or a ["list", "of", "groups"]"#, )); } Ok(DefaultGroups::All) } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut groups = Vec::new(); while let Some(elem) = seq.next_element::()? { groups.push(elem); } Ok(DefaultGroups::List(groups)) } } deserializer.deserialize_any(StringOrVecVisitor) } } impl Default for DefaultGroups { /// Note this is an "empty" default unlike other contexts where `["dev"]` is the default fn default() -> Self { Self::List(Vec::new()) } } /// The name of the global `dev-dependencies` group. /// /// Internally, we model dependency groups as a generic concept; but externally, we only expose the /// `dev-dependencies` group. pub static DEV_DEPENDENCIES: LazyLock = LazyLock::new(|| GroupName::from_str("dev").unwrap());