Skip to main content

uv_configuration/
prerelease.rs

1use std::ops::{Deref, DerefMut};
2use std::str::FromStr;
3
4use rustc_hash::FxHashMap;
5use uv_normalize::PackageName;
6
7#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8#[serde(deny_unknown_fields, rename_all = "kebab-case")]
9#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
10#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11pub enum PrereleaseMode {
12    /// Disallow all pre-release versions.
13    Disallow,
14
15    /// Allow all pre-release versions.
16    Allow,
17
18    /// Prefer stable versions, falling back to pre-release versions when necessary.
19    #[default]
20    IfNecessary,
21
22    /// Prefer stable versions for first-party packages with explicit pre-release specifiers,
23    /// falling back to pre-release versions when necessary. Disallow pre-release versions for all
24    /// other packages.
25    Explicit,
26
27    /// Deprecated alias for `if-necessary`.
28    #[deprecated(note = "use `if-necessary` instead")]
29    IfNecessaryOrExplicit,
30}
31
32#[allow(deprecated)]
33impl std::fmt::Display for PrereleaseMode {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::Disallow => write!(f, "disallow"),
37            Self::Allow => write!(f, "allow"),
38            Self::IfNecessary => write!(f, "if-necessary"),
39            Self::Explicit => write!(f, "explicit"),
40            Self::IfNecessaryOrExplicit => write!(f, "if-necessary-or-explicit"),
41        }
42    }
43}
44
45impl FromStr for PrereleaseMode {
46    type Err = String;
47
48    fn from_str(value: &str) -> Result<Self, Self::Err> {
49        match value {
50            "disallow" => Ok(Self::Disallow),
51            "allow" => Ok(Self::Allow),
52            "if-necessary" => Ok(Self::IfNecessary),
53            "explicit" => Ok(Self::Explicit),
54            #[allow(deprecated)]
55            "if-necessary-or-explicit" => Ok(Self::IfNecessaryOrExplicit),
56            _ => Err(format!(
57                "expected one of `disallow`, `allow`, `if-necessary`, `explicit`, or `if-necessary-or-explicit`, found `{value}`"
58            )),
59        }
60    }
61}
62
63/// A package-specific pre-release selection policy.
64#[derive(Debug, Clone, PartialEq, Eq)]
65#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
66pub struct PrereleasePackageEntry {
67    package: PackageName,
68    mode: PrereleaseMode,
69}
70
71impl FromStr for PrereleasePackageEntry {
72    type Err = String;
73
74    fn from_str(value: &str) -> Result<Self, Self::Err> {
75        let Some((package, mode)) = value.split_once('=') else {
76            return Err(format!(
77                "Invalid `prerelease-package` value `{value}`: expected format `PACKAGE=MODE`"
78            ));
79        };
80
81        let package = PackageName::from_str(package).map_err(|err| {
82            format!("Invalid `prerelease-package` package name `{package}`: {err}")
83        })?;
84        let mode = PrereleaseMode::from_str(mode)
85            .map_err(|err| format!("Invalid `prerelease-package` mode: {err}"))?;
86
87        Ok(Self { package, mode })
88    }
89}
90
91/// Pre-release selection policies that apply to individual packages.
92#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
93#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
94pub struct PrereleasePackage(FxHashMap<PackageName, PrereleaseMode>);
95
96impl Deref for PrereleasePackage {
97    type Target = FxHashMap<PackageName, PrereleaseMode>;
98
99    fn deref(&self) -> &Self::Target {
100        &self.0
101    }
102}
103
104impl DerefMut for PrereleasePackage {
105    fn deref_mut(&mut self) -> &mut Self::Target {
106        &mut self.0
107    }
108}
109
110impl FromIterator<PrereleasePackageEntry> for PrereleasePackage {
111    fn from_iter<T: IntoIterator<Item = PrereleasePackageEntry>>(iter: T) -> Self {
112        Self(
113            iter.into_iter()
114                .map(|entry| (entry.package, entry.mode))
115                .collect(),
116        )
117    }
118}
119
120impl IntoIterator for PrereleasePackage {
121    type Item = (PackageName, PrereleaseMode);
122    type IntoIter = std::collections::hash_map::IntoIter<PackageName, PrereleaseMode>;
123
124    fn into_iter(self) -> Self::IntoIter {
125        self.0.into_iter()
126    }
127}
128
129impl<'a> IntoIterator for &'a PrereleasePackage {
130    type Item = (&'a PackageName, &'a PrereleaseMode);
131    type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PrereleaseMode>;
132
133    fn into_iter(self) -> Self::IntoIter {
134        self.0.iter()
135    }
136}
137
138impl PrereleasePackage {
139    /// Returns whether no package-specific policies are configured.
140    pub fn is_empty(&self) -> bool {
141        self.0.is_empty()
142    }
143}
144
145/// A pre-release selection policy that applies globally and to individual packages.
146#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
147#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
148pub struct Prerelease {
149    /// Global policy that applies to packages without a package-specific override.
150    pub global: PrereleaseMode,
151    /// Package-specific policies that override the global policy.
152    pub package: PrereleasePackage,
153}
154
155impl Prerelease {
156    /// Returns the effective pre-release selection policy for a package.
157    pub fn mode(&self, package: &PackageName) -> PrereleaseMode {
158        self.package.get(package).copied().unwrap_or(self.global)
159    }
160}