1pub 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}