use crate::writers::DisplayExt;
use backtrace::Backtrace;
use std::{fmt, panic::Location};
#[cfg(feature = "capture-spantrace")]
use tracing_error::SpanTrace;
use url::Url;
type Display<'a> = Box<dyn std::fmt::Display + Send + Sync + 'a>;
pub(crate) struct IssueSection<'a> {
url: &'a str,
msg: &'a str,
location: Option<&'a Location<'a>>,
backtrace: Option<&'a Backtrace>,
#[cfg(feature = "capture-spantrace")]
span_trace: Option<&'a SpanTrace>,
metadata: &'a [(String, Display<'a>)],
}
impl<'a> IssueSection<'a> {
pub(crate) fn new(url: &'a str, msg: &'a str) -> Self {
IssueSection {
url,
msg,
location: None,
backtrace: None,
#[cfg(feature = "capture-spantrace")]
span_trace: None,
metadata: &[],
}
}
pub(crate) fn with_location(mut self, location: impl Into<Option<&'a Location<'a>>>) -> Self {
self.location = location.into();
self
}
pub(crate) fn with_backtrace(mut self, backtrace: impl Into<Option<&'a Backtrace>>) -> Self {
self.backtrace = backtrace.into();
self
}
#[cfg(feature = "capture-spantrace")]
pub(crate) fn with_span_trace(mut self, span_trace: impl Into<Option<&'a SpanTrace>>) -> Self {
self.span_trace = span_trace.into();
self
}
pub(crate) fn with_metadata(mut self, metadata: &'a [(String, Display<'a>)]) -> Self {
self.metadata = metadata;
self
}
}
impl fmt::Display for IssueSection<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let location = self
.location
.map(|loc| ("location".to_string(), Box::new(loc) as _));
let metadata = self.metadata.iter().chain(location.as_ref());
let metadata = MetadataSection { metadata }.to_string();
let mut body = Body::new();
body.push_section("Error", ConsoleSection(self.msg))?;
if !self.metadata.is_empty() {
body.push_section("Metadata", metadata)?;
}
#[cfg(feature = "capture-spantrace")]
if let Some(st) = self.span_trace {
body.push_section(
"SpanTrace",
Collapsed(ConsoleSection(st.with_header("SpanTrace:\n"))),
)?;
}
if let Some(bt) = self.backtrace {
body.push_section(
"Backtrace",
Collapsed(ConsoleSection(
DisplayFromDebug(bt).with_header("Backtrace:\n"),
)),
)?;
}
let url_result = Url::parse_with_params(
self.url,
&[("title", "<autogenerated-issue>"), ("body", &body.body)],
);
let url: &dyn fmt::Display = match &url_result {
Ok(url_struct) => url_struct,
Err(_) => &self.url,
};
url.with_header("Consider reporting this error using this URL: ")
.fmt(f)
}
}
struct Body {
body: String,
}
impl Body {
fn new() -> Self {
Body {
body: String::new(),
}
}
fn push_section<T>(&mut self, header: &'static str, section: T) -> fmt::Result
where
T: fmt::Display,
{
use std::fmt::Write;
let separator = if self.body.is_empty() { "" } else { "\n\n" };
let header = header
.with_header("## ")
.with_header(separator)
.with_footer("\n");
write!(&mut self.body, "{}", section.with_header(header))
}
}
struct MetadataSection<T> {
metadata: T,
}
impl<'a, T> MetadataSection<T>
where
T: IntoIterator<Item = &'a (String, Display<'a>)>,
{
#[allow(clippy::inherent_to_string, clippy::wrong_self_convention)]
fn to_string(self) -> String {
use std::fmt::Write;
let mut out = String::new();
let f = &mut out;
writeln!(f, "|key|value|").expect("writing to a string doesn't panic");
writeln!(f, "|--|--|").expect("writing to a string doesn't panic");
for (key, value) in self.metadata {
writeln!(f, "|**{}**|{}|", key, value).expect("writing to a string doesn't panic");
}
out
}
}
struct ConsoleSection<T>(T);
impl<T> fmt::Display for ConsoleSection<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(&self.0).with_header("```\n").with_footer("\n```").fmt(f)
}
}
struct Collapsed<T>(T);
impl<T> fmt::Display for Collapsed<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(&self.0)
.with_header("\n<details>\n\n")
.with_footer("\n</details>")
.fmt(f)
}
}
struct DisplayFromDebug<T>(T);
impl<T> fmt::Display for DisplayFromDebug<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}