Repository: rust-lang/annotate-snippets-rs (rust)

Issue:
`footer` is too specialized for rustc's needs
`footer` only allows `Label`s when rustc has a more general concept of "children" that can either be `Label`s or more `Message`s

Pull request description:
fix!: Generalize footer to be any Message 
Note: the focus of this is not to get full parity with rustc but to get the API in place for us to later get parity with rustc.

Fixes #96

Patch:
diff --git a/examples/footer.rs b/examples/footer.rs
index 8b4d078..3580905 100644
--- a/examples/footer.rs
+++ b/examples/footer.rs
@@ -1,4 +1,4 @@
-use annotate_snippets::{Label, Level, Renderer, Snippet};
+use annotate_snippets::{Level, Renderer, Snippet};
 
 fn main() {
     let message =
@@ -13,7 +13,7 @@ fn main() {
                         "expected struct `annotate_snippets::snippet::Slice`, found reference",
                     )),
             )
-            .footer(Label::note(
+            .footer(Level::Note.title(
                 "expected type: `snippet::Annotation`\n   found type: `__&__snippet::Annotation`",
             ));
 
diff --git a/src/renderer/display_list.rs b/src/renderer/display_list.rs
index d54282f..d0444a3 100644
--- a/src/renderer/display_list.rs
+++ b/src/renderer/display_list.rs
@@ -106,39 +106,12 @@ impl<'a> DisplayList<'a> {
     const WARNING_TXT: &'static str = "warning";
 
     pub(crate) fn new(
-        snippet::Message {
-            level,
-            id,
-            title,
-            footer,
-            snippets,
-        }: snippet::Message<'a>,
+        message: snippet::Message<'a>,
         stylesheet: &'a Stylesheet,
         anonymized_line_numbers: bool,
         margin: Option<Margin>,
     ) -> DisplayList<'a> {
-        let mut body = vec![];
-
-        body.push(format_title(
-            snippet::Label {
-                level,
-                label: title,
-            },
-            id,
-        ));
-
-        for (idx, snippet) in snippets.into_iter().enumerate() {
-            body.append(&mut format_slice(
-                snippet,
-                idx == 0,
-                !footer.is_empty(),
-                margin,
-            ));
-        }
-
-        for annotation in footer {
-            body.append(&mut format_footer(annotation));
-        }
+        let body = format_message(message, margin, true);
 
         Self {
             body,
@@ -725,40 +698,64 @@ impl<'a> Iterator for CursorLines<'a> {
     }
 }
 
-fn format_label(
-    label: Option<&str>,
-    style: Option<DisplayTextStyle>,
-) -> Vec<DisplayTextFragment<'_>> {
-    let mut result = vec![];
-    if let Some(label) = label {
-        let element_style = style.unwrap_or(DisplayTextStyle::Regular);
-        result.push(DisplayTextFragment {
-            content: label,
-            style: element_style,
-        });
+fn format_message(
+    snippet::Message {
+        level,
+        id,
+        title,
+        footer,
+        snippets,
+    }: snippet::Message<'_>,
+    margin: Option<Margin>,
+    primary: bool,
+) -> Vec<DisplayLine<'_>> {
+    let mut body = vec![];
+
+    if !snippets.is_empty() || primary {
+        body.push(format_title(level, id, title));
+    } else {
+        body.extend(format_footer(level, id, title));
     }
-    result
+
+    for (idx, snippet) in snippets.into_iter().enumerate() {
+        body.extend(format_snippet(
+            snippet,
+            idx == 0,
+            !footer.is_empty(),
+            margin,
+        ));
+    }
+
+    for annotation in footer {
+        body.extend(format_message(annotation, margin, false));
+    }
+
+    body
 }
 
-fn format_title<'a>(title: snippet::Label<'a>, id: Option<&'a str>) -> DisplayLine<'a> {
+fn format_title<'a>(level: crate::Level, id: Option<&'a str>, label: &'a str) -> DisplayLine<'a> {
     DisplayLine::Raw(DisplayRawLine::Annotation {
         annotation: Annotation {
-            annotation_type: DisplayAnnotationType::from(title.level),
+            annotation_type: DisplayAnnotationType::from(level),
             id,
-            label: format_label(Some(title.label), Some(DisplayTextStyle::Emphasis)),
+            label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)),
         },
         source_aligned: false,
         continuation: false,
     })
 }
 
-fn format_footer(footer: snippet::Label<'_>) -> Vec<DisplayLine<'_>> {
+fn format_footer<'a>(
+    level: crate::Level,
+    id: Option<&'a str>,
+    label: &'a str,
+) -> Vec<DisplayLine<'a>> {
     let mut result = vec![];
-    for (i, line) in footer.label.lines().enumerate() {
+    for (i, line) in label.lines().enumerate() {
         result.push(DisplayLine::Raw(DisplayRawLine::Annotation {
             annotation: Annotation {
-                annotation_type: DisplayAnnotationType::from(footer.level),
-                id: None,
+                annotation_type: DisplayAnnotationType::from(level),
+                id,
                 label: format_label(Some(line), None),
             },
             source_aligned: true,
@@ -768,7 +765,22 @@ fn format_footer(footer: snippet::Label<'_>) -> Vec<DisplayLine<'_>> {
     result
 }
 
-fn format_slice(
+fn format_label(
+    label: Option<&str>,
+    style: Option<DisplayTextStyle>,
+) -> Vec<DisplayTextFragment<'_>> {
+    let mut result = vec![];
+    if let Some(label) = label {
+        let element_style = style.unwrap_or(DisplayTextStyle::Regular);
+        result.push(DisplayTextFragment {
+            content: label,
+            style: element_style,
+        });
+    }
+    result
+}
+
+fn format_snippet(
     snippet: snippet::Snippet<'_>,
     is_first: bool,
     has_footer: bool,
@@ -777,14 +789,14 @@ fn format_slice(
     let main_range = snippet.annotations.first().map(|x| x.range.start);
     let origin = snippet.origin;
     let need_empty_header = origin.is_some() || is_first;
-    let mut body = format_body(snippet, need_empty_header, has_footer, margin);
+    let body = format_body(snippet, need_empty_header, has_footer, margin);
     let header = format_header(origin, main_range, &body, is_first);
     let mut result = vec![];
 
     if let Some(header) = header {
         result.push(header);
     }
-    result.append(&mut body);
+    result.extend(body);
     result
 }
 
@@ -1444,7 +1456,7 @@ mod tests {
     fn test_format_label() {
         let input = snippet::Level::Error
             .title("")
-            .footer(snippet::Label::error("This __is__ a title"));
+            .footer(snippet::Level::Error.title("This __is__ a title"));
         let output = from_display_lines(vec![
             DisplayLine::Raw(DisplayRawLine::Annotation {
                 annotation: Annotation {
diff --git a/src/snippet.rs b/src/snippet.rs
index 6ba2d2d..e7d4bef 100644
--- a/src/snippet.rs
+++ b/src/snippet.rs
@@ -20,7 +20,7 @@ pub struct Message<'a> {
     pub(crate) id: Option<&'a str>,
     pub(crate) title: &'a str,
     pub(crate) snippets: Vec<Snippet<'a>>,
-    pub(crate) footer: Vec<Label<'a>>,
+    pub(crate) footer: Vec<Message<'a>>,
 }
 
 impl<'a> Message<'a> {
@@ -39,52 +39,17 @@ impl<'a> Message<'a> {
         self
     }
 
-    pub fn footer(mut self, footer: Label<'a>) -> Self {
+    pub fn footer(mut self, footer: Message<'a>) -> Self {
         self.footer.push(footer);
         self
     }
 
-    pub fn footers(mut self, footer: impl IntoIterator<Item = Label<'a>>) -> Self {
+    pub fn footers(mut self, footer: impl IntoIterator<Item = Message<'a>>) -> Self {
         self.footer.extend(footer);
         self
     }
 }
 
-pub struct Label<'a> {
-    pub(crate) level: Level,
-    pub(crate) label: &'a str,
-}
-
-impl<'a> Label<'a> {
-    pub fn new(level: Level, label: &'a str) -> Self {
-        Self { level, label }
-    }
-    pub fn error(label: &'a str) -> Self {
-        Self::new(Level::Error, label)
-    }
-
-    pub fn warning(label: &'a str) -> Self {
-        Self::new(Level::Warning, label)
-    }
-
-    pub fn info(label: &'a str) -> Self {
-        Self::new(Level::Info, label)
-    }
-
-    pub fn note(label: &'a str) -> Self {
-        Self::new(Level::Note, label)
-    }
-
-    pub fn help(label: &'a str) -> Self {
-        Self::new(Level::Help, label)
-    }
-
-    pub fn label(mut self, label: &'a str) -> Self {
-        self.label = label;
-        self
-    }
-}
-
 /// Structure containing the slice of text to be annotated and
 /// basic information about the location of the slice.
 ///


Test patch:
diff --git a/tests/fixtures/deserialize.rs b/tests/fixtures/deserialize.rs
index 6bfe76f..165c341 100644
--- a/tests/fixtures/deserialize.rs
+++ b/tests/fixtures/deserialize.rs
@@ -1,7 +1,7 @@
 use serde::{Deserialize, Deserializer, Serialize};
 use std::ops::Range;
 
-use annotate_snippets::{renderer::Margin, Annotation, Label, Level, Message, Renderer, Snippet};
+use annotate_snippets::{renderer::Margin, Annotation, Level, Message, Renderer, Snippet};
 
 #[derive(Deserialize)]
 pub struct Fixture<'a> {
@@ -20,10 +20,9 @@ pub struct MessageDef<'a> {
     #[serde(default)]
     #[serde(borrow)]
     pub id: Option<&'a str>,
-    #[serde(deserialize_with = "deserialize_labels")]
     #[serde(default)]
     #[serde(borrow)]
-    pub footer: Vec<Label<'a>>,
+    pub footer: Vec<MessageDef<'a>>,
     #[serde(deserialize_with = "deserialize_snippets")]
     #[serde(borrow)]
     pub snippets: Vec<Snippet<'a>>,
@@ -43,28 +42,11 @@ impl<'a> From<MessageDef<'a>> for Message<'a> {
             message = message.id(id);
         }
         message = message.snippets(snippets);
-        message = message.footers(footer);
+        message = message.footers(footer.into_iter().map(Into::into));
         message
     }
 }
 
-fn deserialize_labels<'de, D>(deserializer: D) -> Result<Vec<Label<'de>>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    struct Wrapper<'a>(
-        #[serde(with = "LabelDef")]
-        #[serde(borrow)]
-        LabelDef<'a>,
-    );
-
-    let v = Vec::deserialize(deserializer)?;
-    Ok(v.into_iter()
-        .map(|Wrapper(a)| Label::new(a.level, a.label))
-        .collect())
-}
-
 fn deserialize_snippets<'de, D>(deserializer: D) -> Result<Vec<Snippet<'de>>, D::Error>
 where
     D: Deserializer<'de>,