1use std::borrow::Cow;
2use std::{
3 ops::{Deref, DerefMut},
4 str::FromStr,
5};
6
7use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone};
8use rustc_hash::FxHashMap;
9use serde::Deserialize;
10use serde::de::value::MapAccessDeserializer;
11use uv_normalize::PackageName;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ExcludeNewerValueChange {
15 SpanChanged(ExcludeNewerSpan, ExcludeNewerSpan),
17 SpanAdded(ExcludeNewerSpan),
19 SpanRemoved,
21 RelativeTimestampChanged(Timestamp, Timestamp, ExcludeNewerSpan),
23 AbsoluteTimestampChanged(Timestamp, Timestamp),
25}
26
27impl ExcludeNewerValueChange {
28 pub fn is_relative_timestamp_change(&self) -> bool {
29 matches!(self, Self::RelativeTimestampChanged(_, _, _))
30 }
31}
32
33impl std::fmt::Display for ExcludeNewerValueChange {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::SpanChanged(old, new) => {
37 write!(f, "change of exclude newer span from `{old}` to `{new}`")
38 }
39 Self::SpanAdded(span) => {
40 write!(f, "addition of exclude newer span `{span}`")
41 }
42 Self::SpanRemoved => {
43 write!(f, "removal of exclude newer span")
44 }
45 Self::RelativeTimestampChanged(old, new, span) => {
46 write!(
47 f,
48 "change of calculated ({span}) exclude newer timestamp from `{old}` to `{new}`"
49 )
50 }
51 Self::AbsoluteTimestampChanged(old, new) => {
52 write!(
53 f,
54 "change of exclude newer timestamp from `{old}` to `{new}`"
55 )
56 }
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ExcludeNewerChange {
63 GlobalChanged(ExcludeNewerValueChange),
64 GlobalAdded(ExcludeNewerValue),
65 GlobalRemoved,
66 Package(ExcludeNewerPackageChange),
67}
68
69impl ExcludeNewerChange {
70 pub fn is_relative_timestamp_change(&self) -> bool {
72 match self {
73 Self::GlobalChanged(change) => change.is_relative_timestamp_change(),
74 Self::GlobalAdded(_) | Self::GlobalRemoved => false,
75 Self::Package(change) => change.is_relative_timestamp_change(),
76 }
77 }
78}
79
80impl std::fmt::Display for ExcludeNewerChange {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::GlobalChanged(change) => {
84 write!(f, "{change}")
85 }
86 Self::GlobalAdded(value) => {
87 write!(f, "addition of global exclude newer {value}")
88 }
89 Self::GlobalRemoved => write!(f, "removal of global exclude newer"),
90 Self::Package(change) => {
91 write!(f, "{change}")
92 }
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum ExcludeNewerPackageChange {
99 PackageAdded(PackageName, PackageExcludeNewer),
100 PackageRemoved(PackageName),
101 PackageChanged(PackageName, Box<PackageExcludeNewerChange>),
102}
103
104impl ExcludeNewerPackageChange {
105 pub fn is_relative_timestamp_change(&self) -> bool {
106 match self {
107 Self::PackageAdded(_, _) | Self::PackageRemoved(_) => false,
108 Self::PackageChanged(_, change) => change.is_relative_timestamp_change(),
109 }
110 }
111}
112
113impl std::fmt::Display for ExcludeNewerPackageChange {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Self::PackageAdded(name, PackageExcludeNewer::Enabled(value)) => {
117 write!(
118 f,
119 "addition of exclude newer `{}` for package `{name}`",
120 value.as_ref()
121 )
122 }
123 Self::PackageAdded(name, PackageExcludeNewer::Disabled) => {
124 write!(
125 f,
126 "addition of exclude newer exclusion for package `{name}`"
127 )
128 }
129 Self::PackageRemoved(name) => {
130 write!(f, "removal of exclude newer for package `{name}`")
131 }
132 Self::PackageChanged(name, change) => write!(f, "{change} for package `{name}`"),
133 }
134 }
135}
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct ExcludeNewerValue {
139 timestamp: Timestamp,
141 span: Option<ExcludeNewerSpan>,
143}
144
145impl ExcludeNewerValue {
146 pub fn into_parts(self) -> (Timestamp, Option<ExcludeNewerSpan>) {
147 (self.timestamp, self.span)
148 }
149
150 pub fn compare(&self, other: &Self) -> Option<ExcludeNewerValueChange> {
151 match (&self.span, &other.span) {
152 (None, Some(span)) => Some(ExcludeNewerValueChange::SpanAdded(*span)),
153 (Some(_), None) => Some(ExcludeNewerValueChange::SpanRemoved),
154 (Some(self_span), Some(other_span)) if self_span != other_span => Some(
155 ExcludeNewerValueChange::SpanChanged(*self_span, *other_span),
156 ),
157 (Some(_), Some(span)) if self.timestamp != other.timestamp => {
158 Some(ExcludeNewerValueChange::RelativeTimestampChanged(
159 self.timestamp,
160 other.timestamp,
161 *span,
162 ))
163 }
164 (None, None) if self.timestamp != other.timestamp => Some(
165 ExcludeNewerValueChange::AbsoluteTimestampChanged(self.timestamp, other.timestamp),
166 ),
167 (Some(_), Some(_)) | (None, None) => None,
168 }
169 }
170}
171
172#[derive(Debug, Copy, Clone)]
173pub struct ExcludeNewerSpan(Span);
174
175impl std::fmt::Display for ExcludeNewerSpan {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 self.0.fmt(f)
178 }
179}
180
181impl PartialEq for ExcludeNewerSpan {
182 fn eq(&self, other: &Self) -> bool {
183 self.0.fieldwise() == other.0.fieldwise()
184 }
185}
186
187impl Eq for ExcludeNewerSpan {}
188
189impl serde::Serialize for ExcludeNewerSpan {
190 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
194 where
195 S: serde::Serializer,
196 {
197 serializer.serialize_str(&self.0.to_string())
198 }
199}
200
201impl<'de> serde::Deserialize<'de> for ExcludeNewerSpan {
202 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
203 where
204 D: serde::Deserializer<'de>,
205 {
206 let s = <Cow<'_, str>>::deserialize(deserializer)?;
207 let span: Span = s.parse().map_err(serde::de::Error::custom)?;
208 Ok(Self(span))
209 }
210}
211
212impl serde::Serialize for ExcludeNewerValue {
213 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
214 where
215 S: serde::Serializer,
216 {
217 self.timestamp.serialize(serializer)
218 }
219}
220
221impl<'de> serde::Deserialize<'de> for ExcludeNewerValue {
222 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
223 where
224 D: serde::Deserializer<'de>,
225 {
226 #[derive(serde::Deserialize)]
229 struct TableForm {
230 timestamp: Timestamp,
231 span: Option<ExcludeNewerSpan>,
232 }
233
234 #[derive(serde::Deserialize)]
235 #[serde(untagged)]
236 enum Helper {
237 String(String),
238 Table(Box<TableForm>),
239 }
240
241 match Helper::deserialize(deserializer)? {
242 Helper::String(s) => Self::from_str(&s).map_err(serde::de::Error::custom),
243 Helper::Table(table) => Ok(Self::new(table.timestamp, table.span)),
244 }
245 }
246}
247
248impl ExcludeNewerValue {
249 pub fn timestamp_millis(&self) -> i64 {
251 self.timestamp.as_millisecond()
252 }
253
254 pub fn timestamp(&self) -> Timestamp {
256 self.timestamp
257 }
258
259 pub fn span(&self) -> Option<&ExcludeNewerSpan> {
261 self.span.as_ref()
262 }
263
264 pub fn new(timestamp: Timestamp, span: Option<ExcludeNewerSpan>) -> Self {
266 Self { timestamp, span }
267 }
268}
269
270impl From<Timestamp> for ExcludeNewerValue {
271 fn from(timestamp: Timestamp) -> Self {
272 Self {
273 timestamp,
274 span: None,
275 }
276 }
277}
278
279fn format_exclude_newer_error(
281 input: &str,
282 date_err: &jiff::Error,
283 span_err: &jiff::Error,
284) -> String {
285 let trimmed = input.trim();
286
287 let after_sign = trimmed.trim_start_matches(['+', '-']);
289 if after_sign.starts_with('P') || after_sign.starts_with('p') {
290 return format!("`{input}` could not be parsed as an ISO 8601 duration: {span_err}");
291 }
292
293 let after_sign_trimmed = after_sign.trim_start();
296 let mut chars = after_sign_trimmed.chars().peekable();
297
298 if chars.peek().is_some_and(char::is_ascii_digit) {
300 while chars.peek().is_some_and(char::is_ascii_digit) {
302 chars.next();
303 }
304 while chars.peek().is_some_and(|c| c.is_whitespace()) {
306 chars.next();
307 }
308 if chars.peek().is_some_and(char::is_ascii_alphabetic) {
310 return format!("`{input}` could not be parsed as a duration: {span_err}");
311 }
312 }
313
314 let mut chars = after_sign.chars();
316 let looks_like_date = chars.next().is_some_and(|c| c.is_ascii_digit())
317 && chars.next().is_some_and(|c| c.is_ascii_digit())
318 && chars.next().is_some_and(|c| c.is_ascii_digit())
319 && chars.next().is_some_and(|c| c.is_ascii_digit())
320 && chars.next().is_some_and(|c| c == '-');
321
322 if looks_like_date {
323 return format!("`{input}` could not be parsed as a valid date: {date_err}");
324 }
325
326 format!(
328 "`{input}` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`)"
329 )
330}
331
332impl FromStr for ExcludeNewerValue {
333 type Err = String;
334
335 fn from_str(input: &str) -> Result<Self, Self::Err> {
341 if let Ok(timestamp) = input.parse::<Timestamp>() {
343 return Ok(Self::new(timestamp, None));
344 }
345
346 let date_err = match input.parse::<jiff::civil::Date>() {
354 Ok(date) => {
355 let timestamp = date
356 .checked_add(1.day())
357 .and_then(|date| date.to_zoned(TimeZone::system()))
358 .map(|zdt| zdt.timestamp())
359 .map_err(|err| {
360 format!(
361 "`{input}` parsed to date `{date}`, but could not \
362 be converted to a timestamp: {err}",
363 )
364 })?;
365 return Ok(Self::new(timestamp, None));
366 }
367 Err(err) => err,
368 };
369
370 let span_err = match input.parse::<Span>() {
372 Ok(span) => {
373 let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
375 test_time
376 .parse::<Timestamp>()
377 .expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
378 .to_zoned(TimeZone::UTC)
379 } else {
380 Timestamp::now().to_zoned(TimeZone::UTC)
381 };
382
383 if span.get_years() != 0 {
387 let years = span
388 .total((Unit::Year, &now))
389 .map(f64::ceil)
390 .unwrap_or(1.0)
391 .abs();
392 let days = years * 365.0;
393 return Err(format!(
394 "Duration `{input}` uses unit 'years' which is not allowed; use days instead, e.g., `{days:.0} days`.",
395 ));
396 }
397 if span.get_months() != 0 {
398 let months = span
399 .total((Unit::Month, &now))
400 .map(f64::ceil)
401 .unwrap_or(1.0)
402 .abs();
403 let days = months * 30.0;
404 return Err(format!(
405 "Duration `{input}` uses 'months' which is not allowed; use days instead, e.g., `{days:.0} days`."
406 ));
407 }
408
409 let cutoff = now.checked_sub(span.abs()).map_err(|err| {
415 format!("Duration `{input}` is too large to subtract from current time: {err}")
416 })?;
417
418 return Ok(Self::new(cutoff.into(), Some(ExcludeNewerSpan(span))));
419 }
420 Err(err) => err,
421 };
422
423 Err(format_exclude_newer_error(input, &date_err, &span_err))
425 }
426}
427
428impl std::fmt::Display for ExcludeNewerValue {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 self.timestamp.fmt(f)
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
439pub enum PackageExcludeNewer {
440 Disabled,
442 Enabled(Box<ExcludeNewerValue>),
444}
445
446#[cfg(feature = "schemars")]
447impl schemars::JsonSchema for PackageExcludeNewer {
448 fn schema_name() -> Cow<'static, str> {
449 Cow::Borrowed("PackageExcludeNewer")
450 }
451
452 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
453 schemars::json_schema!({
454 "oneOf": [
455 {
456 "type": "boolean",
457 "const": false,
458 "description": "Disable exclude-newer for this package."
459 },
460 generator.subschema_for::<ExcludeNewerValue>(),
461 ]
462 })
463 }
464}
465
466#[derive(Debug, Clone, PartialEq, Eq)]
468#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
469pub struct ExcludeNewerPackageEntry {
470 pub package: PackageName,
471 pub setting: PackageExcludeNewer,
472}
473
474impl FromStr for ExcludeNewerPackageEntry {
475 type Err = String;
476
477 fn from_str(s: &str) -> Result<Self, Self::Err> {
479 let Some((package, value)) = s.split_once('=') else {
480 return Err(format!(
481 "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE` or `PACKAGE=false`"
482 ));
483 };
484
485 let package = PackageName::from_str(package).map_err(|err| {
486 format!("Invalid `exclude-newer-package` package name `{package}`: {err}")
487 })?;
488
489 let setting = if value == "false" {
490 PackageExcludeNewer::Disabled
491 } else {
492 PackageExcludeNewer::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err(
493 |err| format!("Invalid `exclude-newer-package` value `{value}`: {err}"),
494 )?))
495 };
496
497 Ok(Self { package, setting })
498 }
499}
500
501impl From<(PackageName, PackageExcludeNewer)> for ExcludeNewerPackageEntry {
502 fn from((package, setting): (PackageName, PackageExcludeNewer)) -> Self {
503 Self { package, setting }
504 }
505}
506
507impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry {
508 fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self {
509 Self {
510 package,
511 setting: PackageExcludeNewer::Enabled(Box::new(timestamp)),
512 }
513 }
514}
515
516impl<'de> serde::Deserialize<'de> for PackageExcludeNewer {
517 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
518 where
519 D: serde::Deserializer<'de>,
520 {
521 struct Visitor;
522
523 impl<'de> serde::de::Visitor<'de> for Visitor {
524 type Value = PackageExcludeNewer;
525
526 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
527 formatter.write_str(
528 "a date/timestamp/duration string, false to disable exclude-newer, or a table \
529 with timestamp/span",
530 )
531 }
532
533 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
534 where
535 E: serde::de::Error,
536 {
537 ExcludeNewerValue::from_str(v)
538 .map(|ts| PackageExcludeNewer::Enabled(Box::new(ts)))
539 .map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}")))
540 }
541
542 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
543 where
544 E: serde::de::Error,
545 {
546 if v {
547 Err(E::custom(
548 "expected false to disable exclude-newer, got true",
549 ))
550 } else {
551 Ok(PackageExcludeNewer::Disabled)
552 }
553 }
554
555 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
556 where
557 A: serde::de::MapAccess<'de>,
558 {
559 Ok(PackageExcludeNewer::Enabled(Box::new(
560 ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?,
561 )))
562 }
563 }
564
565 deserializer.deserialize_any(Visitor)
566 }
567}
568
569impl serde::Serialize for PackageExcludeNewer {
570 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
571 where
572 S: serde::Serializer,
573 {
574 match self {
575 Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer),
576 Self::Disabled => serializer.serialize_bool(false),
577 }
578 }
579}
580
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub enum PackageExcludeNewerChange {
583 Disabled { was: ExcludeNewerValue },
584 Enabled { now: ExcludeNewerValue },
585 TimestampChanged(ExcludeNewerValueChange),
586}
587
588impl PackageExcludeNewerChange {
589 pub fn is_relative_timestamp_change(&self) -> bool {
590 match self {
591 Self::Disabled { .. } | Self::Enabled { .. } => false,
592 Self::TimestampChanged(change) => change.is_relative_timestamp_change(),
593 }
594 }
595}
596
597impl std::fmt::Display for PackageExcludeNewerChange {
598 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
599 match self {
600 Self::Disabled { was } => {
601 write!(f, "add exclude newer exclusion (was `{was}`)")
602 }
603 Self::Enabled { now } => {
604 write!(f, "remove exclude newer exclusion (now `{now}`)")
605 }
606 Self::TimestampChanged(change) => write!(f, "{change}"),
607 }
608 }
609}
610
611#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
612#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
613pub struct ExcludeNewerPackage(FxHashMap<PackageName, PackageExcludeNewer>);
614
615impl Deref for ExcludeNewerPackage {
616 type Target = FxHashMap<PackageName, PackageExcludeNewer>;
617
618 fn deref(&self) -> &Self::Target {
619 &self.0
620 }
621}
622
623impl DerefMut for ExcludeNewerPackage {
624 fn deref_mut(&mut self) -> &mut Self::Target {
625 &mut self.0
626 }
627}
628
629impl FromIterator<ExcludeNewerPackageEntry> for ExcludeNewerPackage {
630 fn from_iter<T: IntoIterator<Item = ExcludeNewerPackageEntry>>(iter: T) -> Self {
631 Self(
632 iter.into_iter()
633 .map(|entry| (entry.package, entry.setting))
634 .collect(),
635 )
636 }
637}
638
639impl IntoIterator for ExcludeNewerPackage {
640 type Item = (PackageName, PackageExcludeNewer);
641 type IntoIter = std::collections::hash_map::IntoIter<PackageName, PackageExcludeNewer>;
642
643 fn into_iter(self) -> Self::IntoIter {
644 self.0.into_iter()
645 }
646}
647
648impl<'a> IntoIterator for &'a ExcludeNewerPackage {
649 type Item = (&'a PackageName, &'a PackageExcludeNewer);
650 type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PackageExcludeNewer>;
651
652 fn into_iter(self) -> Self::IntoIter {
653 self.0.iter()
654 }
655}
656
657impl ExcludeNewerPackage {
658 pub fn into_inner(self) -> FxHashMap<PackageName, PackageExcludeNewer> {
660 self.0
661 }
662
663 pub fn is_empty(&self) -> bool {
665 self.0.is_empty()
666 }
667
668 pub fn compare(&self, other: &Self) -> Option<ExcludeNewerPackageChange> {
669 for (package, setting) in self {
670 match (setting, other.get(package)) {
671 (
672 PackageExcludeNewer::Enabled(self_timestamp),
673 Some(PackageExcludeNewer::Enabled(other_timestamp)),
674 ) => {
675 if let Some(change) = self_timestamp.compare(other_timestamp) {
676 return Some(ExcludeNewerPackageChange::PackageChanged(
677 package.clone(),
678 Box::new(PackageExcludeNewerChange::TimestampChanged(change)),
679 ));
680 }
681 }
682 (
683 PackageExcludeNewer::Enabled(self_timestamp),
684 Some(PackageExcludeNewer::Disabled),
685 ) => {
686 return Some(ExcludeNewerPackageChange::PackageChanged(
687 package.clone(),
688 Box::new(PackageExcludeNewerChange::Disabled {
689 was: self_timestamp.as_ref().clone(),
690 }),
691 ));
692 }
693 (
694 PackageExcludeNewer::Disabled,
695 Some(PackageExcludeNewer::Enabled(other_timestamp)),
696 ) => {
697 return Some(ExcludeNewerPackageChange::PackageChanged(
698 package.clone(),
699 Box::new(PackageExcludeNewerChange::Enabled {
700 now: other_timestamp.as_ref().clone(),
701 }),
702 ));
703 }
704 (PackageExcludeNewer::Disabled, Some(PackageExcludeNewer::Disabled)) => {}
705 (_, None) => {
706 return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone()));
707 }
708 }
709 }
710
711 for (package, value) in other {
712 if !self.contains_key(package) {
713 return Some(ExcludeNewerPackageChange::PackageAdded(
714 package.clone(),
715 value.clone(),
716 ));
717 }
718 }
719
720 None
721 }
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
726#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
727pub struct ExcludeNewer {
728 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub global: Option<ExcludeNewerValue>,
731 #[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
733 pub package: ExcludeNewerPackage,
734}
735
736impl ExcludeNewer {
737 pub fn global(global: ExcludeNewerValue) -> Self {
739 Self {
740 global: Some(global),
741 package: ExcludeNewerPackage::default(),
742 }
743 }
744
745 pub fn new(global: Option<ExcludeNewerValue>, package: ExcludeNewerPackage) -> Self {
747 Self { global, package }
748 }
749
750 pub fn from_args(
752 global: Option<ExcludeNewerValue>,
753 package: Vec<ExcludeNewerPackageEntry>,
754 ) -> Self {
755 let package: ExcludeNewerPackage = package.into_iter().collect();
756
757 Self { global, package }
758 }
759
760 pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option<ExcludeNewerValue> {
765 match self.package.get(package_name) {
766 Some(PackageExcludeNewer::Enabled(timestamp)) => Some(timestamp.as_ref().clone()),
767 Some(PackageExcludeNewer::Disabled) => None,
768 None => self.global.clone(),
769 }
770 }
771
772 pub fn is_empty(&self) -> bool {
774 self.global.is_none() && self.package.is_empty()
775 }
776
777 pub fn compare(&self, other: &Self) -> Option<ExcludeNewerChange> {
778 match (&self.global, &other.global) {
779 (Some(self_global), Some(other_global)) => {
780 if let Some(change) = self_global.compare(other_global) {
781 return Some(ExcludeNewerChange::GlobalChanged(change));
782 }
783 }
784 (None, Some(global)) => {
785 return Some(ExcludeNewerChange::GlobalAdded(global.clone()));
786 }
787 (Some(_), None) => return Some(ExcludeNewerChange::GlobalRemoved),
788 (None, None) => (),
789 }
790 self.package
791 .compare(&other.package)
792 .map(ExcludeNewerChange::Package)
793 }
794}
795
796impl std::fmt::Display for ExcludeNewer {
797 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
798 if let Some(global) = &self.global {
799 write!(f, "global: {global}")?;
800 if !self.package.is_empty() {
801 write!(f, ", ")?;
802 }
803 }
804 let mut first = true;
805 for (name, setting) in &self.package {
806 if !first {
807 write!(f, ", ")?;
808 }
809 match setting {
810 PackageExcludeNewer::Enabled(timestamp) => {
811 write!(f, "{name}: {}", timestamp.as_ref())?;
812 }
813 PackageExcludeNewer::Disabled => {
814 write!(f, "{name}: disabled")?;
815 }
816 }
817 first = false;
818 }
819 Ok(())
820 }
821}
822
823#[cfg(feature = "schemars")]
824impl schemars::JsonSchema for ExcludeNewerValue {
825 fn schema_name() -> Cow<'static, str> {
826 Cow::Borrowed("ExcludeNewerValue")
827 }
828
829 fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
830 schemars::json_schema!({
831 "type": "string",
832 "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`), as well as relative durations (e.g., `1 week`, `30 days`, `6 months`). Relative durations are resolved to a timestamp at lock time.",
833 })
834 }
835}