Skip to main content

uv_errors/
lib.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use owo_colors::OwoColorize;
5
6/// An error that may carry user-facing hints.
7///
8/// Implement this on error types that want to surface contextual suggestions
9/// (e.g., "try `--prerelease=allow`") to the diagnostics layer. Hints are
10/// rendered after the error output, each prefixed with `hint:`.
11pub trait Hint {
12    /// Return any hints associated with this error.
13    fn hints(&self) -> Hints<'_> {
14        Hints::none()
15    }
16}
17
18/// A collection of user-facing hint messages.
19///
20/// Each hint is rendered on its own line, prefixed with the styled `hint:` label.
21pub struct Hints<'a>(Vec<Cow<'a, str>>);
22
23impl Hints<'_> {
24    /// No hints.
25    pub fn none() -> Self {
26        Self(Vec::new())
27    }
28
29    /// Add a single owned hint.
30    pub fn push(&mut self, hint: String) {
31        self.0.push(Cow::Owned(hint));
32    }
33
34    /// Convert all borrowed hints to owned, extending the lifetime to `'static`.
35    pub fn into_owned(self) -> Hints<'static> {
36        Hints(
37            self.0
38                .into_iter()
39                .map(|cow| Cow::Owned(cow.into_owned()))
40                .collect(),
41        )
42    }
43
44    /// Whether the collection is empty.
45    pub fn is_empty(&self) -> bool {
46        self.0.is_empty()
47    }
48
49    /// Extend with another set of hints, converting borrowed hints to owned.
50    pub fn extend(&mut self, other: Hints<'_>) {
51        for hint in other.0 {
52            let hint = Cow::Owned(hint.into_owned());
53            if !self.0.iter().any(|existing| existing == &hint) {
54                self.0.push(hint);
55            }
56        }
57    }
58}
59
60/// A display adapter for an error followed by its hints.
61///
62/// Error renderers line-terminate the error before rendering [`Hints`]. Use
63/// this adapter when an error and its hints need to be formatted together.
64pub struct ErrorWithHints<'a, E> {
65    error: E,
66    hints: Hints<'a>,
67}
68
69impl<'a, E> ErrorWithHints<'a, E> {
70    /// Format an error followed by any hints.
71    pub fn new(error: E, hints: Hints<'a>) -> Self {
72        Self { error, hints }
73    }
74}
75
76impl<E: fmt::Display> fmt::Display for ErrorWithHints<'_, E> {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}", self.error)?;
79        if !self.hints.is_empty() {
80            writeln!(f)?;
81            write!(f, "{}", self.hints)?;
82        }
83        Ok(())
84    }
85}
86
87impl<'a> From<&'a str> for Hints<'a> {
88    fn from(hint: &'a str) -> Self {
89        Self(vec![Cow::Borrowed(hint)])
90    }
91}
92
93impl From<String> for Hints<'_> {
94    fn from(hint: String) -> Self {
95        Self(vec![Cow::Owned(hint)])
96    }
97}
98
99impl FromIterator<String> for Hints<'_> {
100    fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
101        Self(iter.into_iter().map(Cow::Owned).collect())
102    }
103}
104
105impl fmt::Display for Hints<'_> {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        for hint in &self.0 {
108            write!(f, "\n{HintPrefix} {hint}")?;
109        }
110        Ok(())
111    }
112}
113
114impl<'a> IntoIterator for Hints<'a> {
115    type Item = Cow<'a, str>;
116    type IntoIter = std::vec::IntoIter<Cow<'a, str>>;
117
118    fn into_iter(self) -> Self::IntoIter {
119        self.0.into_iter()
120    }
121}
122
123/// A styled `hint:` prefix for use in user-facing messages.
124pub struct HintPrefix;
125
126impl fmt::Display for HintPrefix {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(f, "{}{}", "hint".bold().cyan(), ":".bold())
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::{ErrorWithHints, HintPrefix, Hints};
135
136    #[test]
137    fn extend_deduplicates_matching_hints() {
138        let mut hints = Hints::from("same");
139        hints.extend(Hints::from("same"));
140        hints.extend(Hints::from("other"));
141
142        let hints = hints
143            .into_iter()
144            .map(std::borrow::Cow::into_owned)
145            .collect::<Vec<_>>();
146        assert_eq!(hints, vec!["same".to_string(), "other".to_string()]);
147    }
148
149    #[test]
150    fn error_with_hints_separates_hints_from_error() {
151        assert_eq!(
152            ErrorWithHints::new("error", Hints::from("fix it")).to_string(),
153            format!("error\n\n{HintPrefix} fix it")
154        );
155        assert_eq!(
156            ErrorWithHints::new("error", Hints::none()).to_string(),
157            "error"
158        );
159    }
160}