eipw_snippets/
lib.rs

1// Borrowed from https://github.com/rust-lang/annotate-snippets-rs
2// At commit c84e388949f0e06f4529de060c195e12e6531fbd
3
4pub use annotate_snippets;
5
6use annotate_snippets as ann;
7
8use serde::{Deserialize, Serialize};
9
10use std::borrow::Cow;
11use std::ops::Range;
12
13#[derive(Debug, Deserialize, Serialize)]
14#[non_exhaustive]
15pub struct Message<'a> {
16    pub level: Level,
17    #[serde(default)]
18    pub id: Option<Cow<'a, str>>,
19    pub title: Cow<'a, str>,
20    pub snippets: Vec<Snippet<'a>>,
21    pub footer: Vec<Message<'a>>,
22}
23
24impl<'a> Message<'a> {
25    pub fn id(mut self, id: &'a str) -> Self {
26        self.id = Some(Cow::Borrowed(id));
27        self
28    }
29
30    pub fn snippet(mut self, slice: Snippet<'a>) -> Self {
31        self.snippets.push(slice);
32        self
33    }
34
35    pub fn snippets(mut self, slice: impl IntoIterator<Item = Snippet<'a>>) -> Self {
36        self.snippets.extend(slice);
37        self
38    }
39
40    pub fn footer(mut self, footer: Message<'a>) -> Self {
41        self.footer.push(footer);
42        self
43    }
44
45    pub fn footers(mut self, footer: impl IntoIterator<Item = Message<'a>>) -> Self {
46        self.footer.extend(footer);
47        self
48    }
49}
50
51impl<'a, 'b> From<&'b Message<'a>> for ann::Message<'b> {
52    fn from(value: &'b Message<'a>) -> Self {
53        let msg = ann::Level::from(value.level)
54            .title(&value.title)
55            .snippets(value.snippets.iter().map(Into::into))
56            .footers(value.footer.iter().map(Into::into));
57
58        if let Some(ref id) = value.id {
59            msg.id(id)
60        } else {
61            msg
62        }
63    }
64}
65
66#[derive(Debug, Deserialize, Serialize)]
67#[non_exhaustive]
68pub struct Snippet<'a> {
69    #[serde(default)]
70    pub origin: Option<Cow<'a, str>>,
71    pub line_start: usize,
72
73    pub source: Cow<'a, str>,
74    pub annotations: Vec<Annotation<'a>>,
75
76    pub fold: bool,
77}
78
79impl<'a> Snippet<'a> {
80    pub fn source(source: &'a str) -> Self {
81        Self {
82            origin: None,
83            line_start: 1,
84            source: Cow::Borrowed(source),
85            annotations: vec![],
86            fold: false,
87        }
88    }
89
90    pub fn line_start(mut self, line_start: usize) -> Self {
91        self.line_start = line_start;
92        self
93    }
94
95    pub fn origin(mut self, origin: &'a str) -> Self {
96        self.origin = Some(Cow::Borrowed(origin));
97        self
98    }
99
100    pub fn annotation(mut self, annotation: Annotation<'a>) -> Self {
101        self.annotations.push(annotation);
102        self
103    }
104
105    pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
106        self.annotations.extend(annotation);
107        self
108    }
109
110    pub fn fold(mut self, fold: bool) -> Self {
111        self.fold = fold;
112        self
113    }
114}
115
116impl<'a, 'b> From<&'b Snippet<'a>> for ann::Snippet<'b> {
117    fn from(value: &'b Snippet<'a>) -> Self {
118        let snip = Self::source(&value.source)
119            .line_start(value.line_start)
120            .annotations(value.annotations.iter().map(Into::into))
121            .fold(value.fold);
122
123        if let Some(ref origin) = value.origin {
124            snip.origin(origin)
125        } else {
126            snip
127        }
128    }
129}
130
131#[derive(Debug, Deserialize, Serialize)]
132#[non_exhaustive]
133pub struct Annotation<'a> {
134    pub range: Range<usize>,
135    #[serde(default)]
136    pub label: Option<Cow<'a, str>>,
137    pub level: Level,
138}
139
140impl<'a> Annotation<'a> {
141    pub fn label(mut self, label: &'a str) -> Self {
142        self.label = Some(Cow::Borrowed(label));
143        self
144    }
145}
146
147impl<'a, 'b> From<&'b Annotation<'a>> for ann::Annotation<'b> {
148    fn from(value: &'b Annotation<'a>) -> Self {
149        let a = ann::Level::from(value.level).span(value.range.clone());
150
151        if let Some(ref label) = value.label {
152            a.label(label)
153        } else {
154            a
155        }
156    }
157}
158
159#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)]
160pub enum Level {
161    Error,
162    Warning,
163    Info,
164    Note,
165    Help,
166}
167
168impl Level {
169    pub fn title(self, title: &str) -> Message<'_> {
170        Message {
171            level: self,
172            id: None,
173            title: Cow::Borrowed(title),
174            snippets: vec![],
175            footer: vec![],
176        }
177    }
178
179    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
180        Annotation {
181            range: span,
182            label: None,
183            level: self,
184        }
185    }
186}
187
188impl From<Level> for ann::Level {
189    fn from(value: Level) -> Self {
190        match value {
191            Level::Error => Self::Error,
192            Level::Warning => Self::Warning,
193            Level::Info => Self::Info,
194            Level::Note => Self::Note,
195            Level::Help => Self::Help,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn json_round_trip_with_escape_sequence() {
206        let title = "some \u{1f6a8} diagn\\ostic \"a rope of sand\"";
207        let id = "id-\u{1f6a8}-\"-\\";
208        let source = "for \u{1f6a8} do \\ostic \"electric boogaloo\"";
209        let origin = "\u{1f6a8}";
210
211        let annotation = Level::Help.span(0..4).label(title);
212
213        let snippet = Snippet::source(source)
214            .fold(true)
215            .origin(origin)
216            .line_start(123)
217            .annotation(annotation);
218
219        let footer = Level::Help.title(title).id(id);
220
221        let msg = Level::Error
222            .title(title)
223            .id(id)
224            .snippet(snippet)
225            .footer(footer);
226
227        let json = serde_json::to_string_pretty(&msg).unwrap();
228        let actual: Message = serde_json::from_str(&json).unwrap();
229
230        assert_eq!(actual.title, title);
231        assert_eq!(actual.id, Some(Cow::Borrowed(id)));
232        assert_eq!(actual.level, Level::Error);
233        assert_eq!(actual.footer.len(), 1);
234        assert_eq!(actual.footer[0].title, title);
235        assert_eq!(actual.footer[0].id, Some(Cow::Borrowed(id)));
236        assert_eq!(actual.snippets.len(), 1);
237        assert_eq!(actual.snippets[0].source, source);
238        assert_eq!(actual.snippets[0].origin, Some(Cow::Borrowed(origin)));
239        assert_eq!(actual.snippets[0].annotations.len(), 1);
240        assert_eq!(
241            actual.snippets[0].annotations[0].label,
242            Some(Cow::Borrowed(title))
243        );
244    }
245}