uv_resolver/
exclude_newer.rs

1#[cfg(feature = "schemars")]
2use std::borrow::Cow;
3use std::{
4    ops::{Deref, DerefMut},
5    str::FromStr,
6};
7
8use jiff::{Timestamp, ToSpan, tz::TimeZone};
9use rustc_hash::FxHashMap;
10use uv_normalize::PackageName;
11
12/// A timestamp that excludes files newer than it.
13#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
14pub struct ExcludeNewerTimestamp(Timestamp);
15
16impl ExcludeNewerTimestamp {
17    /// Returns the timestamp in milliseconds.
18    pub fn timestamp_millis(&self) -> i64 {
19        self.0.as_millisecond()
20    }
21}
22
23impl From<Timestamp> for ExcludeNewerTimestamp {
24    fn from(timestamp: Timestamp) -> Self {
25        Self(timestamp)
26    }
27}
28
29impl FromStr for ExcludeNewerTimestamp {
30    type Err = String;
31
32    /// Parse an [`ExcludeNewerTimestamp`] from a string.
33    ///
34    /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same
35    /// format (e.g., `2006-12-02`).
36    fn from_str(input: &str) -> Result<Self, Self::Err> {
37        // NOTE(burntsushi): Previously, when using Chrono, we tried
38        // to parse as a date first, then a timestamp, and if both
39        // failed, we combined both of the errors into one message.
40        // But in Jiff, if an RFC 3339 timestamp could be parsed, then
41        // it must necessarily be the case that a date can also be
42        // parsed. So we can collapse the error cases here. That is,
43        // if we fail to parse a timestamp and a date, then it should
44        // be sufficient to just report the error from parsing the date.
45        // If someone tried to write a timestamp but committed an error
46        // in the non-date portion, the date parsing below will still
47        // report a holistic error that will make sense to the user.
48        // (I added a snapshot test for that case.)
49        if let Ok(timestamp) = input.parse::<Timestamp>() {
50            return Ok(Self(timestamp));
51        }
52        let date = input
53            .parse::<jiff::civil::Date>()
54            .map_err(|err| format!("`{input}` could not be parsed as a valid date: {err}"))?;
55        let timestamp = date
56            .checked_add(1.day())
57            .and_then(|date| date.to_zoned(TimeZone::system()))
58            .map(|zdt| zdt.timestamp())
59            .map_err(|err| {
60                format!(
61                    "`{input}` parsed to date `{date}`, but could not \
62                     be converted to a timestamp: {err}",
63                )
64            })?;
65        Ok(Self(timestamp))
66    }
67}
68
69impl std::fmt::Display for ExcludeNewerTimestamp {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        self.0.fmt(f)
72    }
73}
74
75/// A package-specific exclude-newer entry.
76#[derive(Debug, Clone, PartialEq, Eq)]
77#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
78pub struct ExcludeNewerPackageEntry {
79    pub package: PackageName,
80    pub timestamp: ExcludeNewerTimestamp,
81}
82
83impl FromStr for ExcludeNewerPackageEntry {
84    type Err = String;
85
86    /// Parses a [`ExcludeNewerPackageEntry`] from a string in the format `PACKAGE=DATE`.
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        let Some((package, date)) = s.split_once('=') else {
89            return Err(format!(
90                "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE`"
91            ));
92        };
93
94        let package = PackageName::from_str(package).map_err(|err| {
95            format!("Invalid `exclude-newer-package` package name `{package}`: {err}")
96        })?;
97        let timestamp = ExcludeNewerTimestamp::from_str(date)
98            .map_err(|err| format!("Invalid `exclude-newer-package` timestamp `{date}`: {err}"))?;
99
100        Ok(Self { package, timestamp })
101    }
102}
103
104impl From<(PackageName, ExcludeNewerTimestamp)> for ExcludeNewerPackageEntry {
105    fn from((package, timestamp): (PackageName, ExcludeNewerTimestamp)) -> Self {
106        Self { package, timestamp }
107    }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
111#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
112pub struct ExcludeNewerPackage(FxHashMap<PackageName, ExcludeNewerTimestamp>);
113
114impl Deref for ExcludeNewerPackage {
115    type Target = FxHashMap<PackageName, ExcludeNewerTimestamp>;
116
117    fn deref(&self) -> &Self::Target {
118        &self.0
119    }
120}
121
122impl DerefMut for ExcludeNewerPackage {
123    fn deref_mut(&mut self) -> &mut Self::Target {
124        &mut self.0
125    }
126}
127
128impl FromIterator<ExcludeNewerPackageEntry> for ExcludeNewerPackage {
129    fn from_iter<T: IntoIterator<Item = ExcludeNewerPackageEntry>>(iter: T) -> Self {
130        Self(
131            iter.into_iter()
132                .map(|entry| (entry.package, entry.timestamp))
133                .collect(),
134        )
135    }
136}
137
138impl IntoIterator for ExcludeNewerPackage {
139    type Item = (PackageName, ExcludeNewerTimestamp);
140    type IntoIter = std::collections::hash_map::IntoIter<PackageName, ExcludeNewerTimestamp>;
141
142    fn into_iter(self) -> Self::IntoIter {
143        self.0.into_iter()
144    }
145}
146
147impl<'a> IntoIterator for &'a ExcludeNewerPackage {
148    type Item = (&'a PackageName, &'a ExcludeNewerTimestamp);
149    type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerTimestamp>;
150
151    fn into_iter(self) -> Self::IntoIter {
152        self.0.iter()
153    }
154}
155
156impl ExcludeNewerPackage {
157    /// Convert to the inner `HashMap`.
158    pub fn into_inner(self) -> FxHashMap<PackageName, ExcludeNewerTimestamp> {
159        self.0
160    }
161}
162
163/// A setting that excludes files newer than a timestamp, at a global level or per-package.
164#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
165#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
166pub struct ExcludeNewer {
167    /// Global timestamp that applies to all packages if no package-specific timestamp is set.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub global: Option<ExcludeNewerTimestamp>,
170    /// Per-package timestamps that override the global timestamp.
171    #[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
172    pub package: ExcludeNewerPackage,
173}
174
175impl ExcludeNewer {
176    /// Create a new exclude newer configuration with just a global timestamp.
177    pub fn global(global: ExcludeNewerTimestamp) -> Self {
178        Self {
179            global: Some(global),
180            package: ExcludeNewerPackage::default(),
181        }
182    }
183
184    /// Create a new exclude newer configuration.
185    pub fn new(global: Option<ExcludeNewerTimestamp>, package: ExcludeNewerPackage) -> Self {
186        Self { global, package }
187    }
188
189    /// Create from CLI arguments.
190    pub fn from_args(
191        global: Option<ExcludeNewerTimestamp>,
192        package: Vec<ExcludeNewerPackageEntry>,
193    ) -> Self {
194        let package: ExcludeNewerPackage = package.into_iter().collect();
195
196        Self { global, package }
197    }
198
199    /// Returns the timestamp for a specific package, falling back to the global timestamp if set.
200    pub fn exclude_newer_package(
201        &self,
202        package_name: &PackageName,
203    ) -> Option<ExcludeNewerTimestamp> {
204        self.package.get(package_name).copied().or(self.global)
205    }
206
207    /// Returns true if this has any configuration (global or per-package).
208    pub fn is_empty(&self) -> bool {
209        self.global.is_none() && self.package.is_empty()
210    }
211}
212
213impl std::fmt::Display for ExcludeNewer {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        if let Some(global) = self.global {
216            write!(f, "global: {global}")?;
217            if !self.package.is_empty() {
218                write!(f, ", ")?;
219            }
220        }
221        let mut first = true;
222        for (name, timestamp) in &self.package {
223            if !first {
224                write!(f, ", ")?;
225            }
226            write!(f, "{name}: {timestamp}")?;
227            first = false;
228        }
229        Ok(())
230    }
231}
232
233#[cfg(feature = "schemars")]
234impl schemars::JsonSchema for ExcludeNewerTimestamp {
235    fn schema_name() -> Cow<'static, str> {
236        Cow::Borrowed("ExcludeNewerTimestamp")
237    }
238
239    fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
240        schemars::json_schema!({
241            "type": "string",
242            "pattern": r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2}))?$",
243            "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).",
244        })
245    }
246}