mirror of
https://github.com/eyre-rs/eyre.git
synced 2025-09-27 13:01:29 +00:00
Implement github issue generator (#62)
* Initial implementation of github issue generator * document the hookbuilder methods * fix doctest * make issue generation feature optional * update changelog * (cargo-release) version 0.5.4-rc.1 * add doc cfg attr to new methods * (cargo-release) version 0.5.4-rc.2 * supress issue sections when they'd be empty * (cargo-release) version 0.5.4-rc.3 * (cargo-release) version 0.5.4 * (cargo-release) start next development iteration 0.5.5-alpha.0
This commit is contained in:
parent
05809302b3
commit
9c738c3a39
@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.5.4] - 2020-09-17
|
||||
### Added
|
||||
- Add new "issue-url" feature for generating issue creation links in error
|
||||
reports pre-populated with information about the error
|
||||
|
||||
## [0.5.3] - 2020-09-14
|
||||
### Added
|
||||
- add `panic_section` method to `HookBuilder` for overriding the printer for
|
||||
@ -25,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
better compatibility with the Display trait
|
||||
|
||||
<!-- next-url -->
|
||||
[Unreleased]: https://github.com/yaahc/color-eyre/compare/v0.5.3...HEAD
|
||||
[Unreleased]: https://github.com/yaahc/color-eyre/compare/v0.5.4...HEAD
|
||||
[0.5.4]: https://github.com/yaahc/color-eyre/compare/v0.5.3...v0.5.4
|
||||
[0.5.3]: https://github.com/yaahc/color-eyre/compare/v0.5.2...v0.5.3
|
||||
[0.5.2]: https://github.com/yaahc/color-eyre/releases/tag/v0.5.2
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "color-eyre"
|
||||
version = "0.5.4-alpha.0"
|
||||
version = "0.5.5-alpha.0"
|
||||
authors = ["Jane Lusby <jlusby@yaah.dev>"]
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -14,6 +14,7 @@ keywords = []
|
||||
[features]
|
||||
default = ["capture-spantrace"]
|
||||
capture-spantrace = ["tracing-error", "color-spantrace"]
|
||||
issue-url = ["url"]
|
||||
|
||||
[dependencies]
|
||||
eyre = "0.6.0"
|
||||
@ -23,6 +24,7 @@ indenter = "0.3.0"
|
||||
owo-colors = "1.0.3"
|
||||
color-spantrace = { version = "0.1.4", optional = true }
|
||||
once_cell = "1.4.0"
|
||||
url = { version = "2.1.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.2.5"
|
||||
|
70
examples/github_issue.rs
Normal file
70
examples/github_issue.rs
Normal file
@ -0,0 +1,70 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
use color_eyre::eyre;
|
||||
use eyre::{Report, Result};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument]
|
||||
#[cfg(feature = "issue-url")]
|
||||
fn main() -> Result<(), Report> {
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
install_tracing();
|
||||
|
||||
color_eyre::config::HookBuilder::default()
|
||||
.issue_url("https://github.com/yaahc/jane-eyre/issues/new")
|
||||
.add_issue_metadata("version", "0.1.0")
|
||||
.install()?;
|
||||
|
||||
let report = read_config().unwrap_err();
|
||||
eprintln!("Error: {:?}", report);
|
||||
|
||||
read_config2();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "issue-url"))]
|
||||
fn main() {
|
||||
unimplemented!("this example requires the \"issue-url\" feature")
|
||||
}
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
fn install_tracing() {
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
let fmt_layer = fmt::layer().with_target(false);
|
||||
let filter_layer = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new("info"))
|
||||
.unwrap();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(fmt_layer)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn read_file(path: &str) -> Result<String> {
|
||||
Ok(std::fs::read_to_string(path)?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn read_config() -> Result<()> {
|
||||
read_file("fake_file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn read_file2(path: &str) {
|
||||
if let Err(e) = std::fs::read_to_string(path) {
|
||||
panic!("{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn read_config2() {
|
||||
read_file2("fake_file")
|
||||
}
|
114
src/config.rs
114
src/config.rs
@ -250,6 +250,10 @@ pub struct HookBuilder {
|
||||
display_env_section: bool,
|
||||
panic_section: Option<Box<dyn Display + Send + Sync + 'static>>,
|
||||
panic_message: Box<dyn PanicMessage>,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: Option<String>,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata: Vec<(String, Box<dyn Display + Send + Sync + 'static>)>,
|
||||
}
|
||||
|
||||
impl HookBuilder {
|
||||
@ -284,6 +288,10 @@ impl HookBuilder {
|
||||
display_env_section: true,
|
||||
panic_section: None,
|
||||
panic_message: Box::new(DefaultPanicMessage),
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: None,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,6 +368,57 @@ impl HookBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an upstream github repo and enable issue reporting url generation
|
||||
///
|
||||
/// # Details
|
||||
///
|
||||
/// Once enabled, color-eyre will generate urls that will create customized
|
||||
/// issues pre-populated with information about the associated error report.
|
||||
///
|
||||
/// Additional information can be added to the metadata table in the
|
||||
/// generated urls by calling `add_issue_metadata` when configuring the
|
||||
/// HookBuilder.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// color_eyre::config::HookBuilder::default()
|
||||
/// .issue_url("https://github.com/yaahc/jane-eyre/issues/new")
|
||||
/// .install()
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
#[cfg(feature = "issue-url")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
|
||||
pub fn issue_url<S: ToString>(mut self, url: S) -> Self {
|
||||
self.issue_url = Some(url.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a new entry to the metadata table in generated github issue urls
|
||||
///
|
||||
/// **Note**: this metadata will be ignored if no `issue_url` is set.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// color_eyre::config::HookBuilder::default()
|
||||
/// .issue_url("https://github.com/yaahc/jane-eyre/issues/new")
|
||||
/// .add_issue_metadata("version", "0.1.0")
|
||||
/// .install()
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
#[cfg(feature = "issue-url")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
|
||||
pub fn add_issue_metadata<K, V>(mut self, key: K, value: V) -> Self
|
||||
where
|
||||
K: Display,
|
||||
V: Display + Send + Sync + 'static,
|
||||
{
|
||||
let pair = (key.to_string(), Box::new(value) as _);
|
||||
self.issue_metadata.push(pair);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configures the default capture mode for `SpanTraces` in error reports and panics
|
||||
pub fn capture_span_trace_by_default(mut self, cond: bool) -> Self {
|
||||
self.capture_span_trace_by_default = cond;
|
||||
@ -423,6 +482,8 @@ impl HookBuilder {
|
||||
}
|
||||
|
||||
pub(crate) fn into_hooks(self) -> (PanicHook, EyreHook) {
|
||||
#[cfg(feature = "issue-url")]
|
||||
let metadata = Arc::new(self.issue_metadata);
|
||||
let panic_hook = PanicHook {
|
||||
filters: self.filters.into_iter().map(Into::into).collect(),
|
||||
section: self.panic_section,
|
||||
@ -430,12 +491,20 @@ impl HookBuilder {
|
||||
capture_span_trace_by_default: self.capture_span_trace_by_default,
|
||||
display_env_section: self.display_env_section,
|
||||
panic_message: self.panic_message,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: self.issue_url.clone(),
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata: metadata.clone(),
|
||||
};
|
||||
|
||||
let eyre_hook = EyreHook {
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
capture_span_trace_by_default: self.capture_span_trace_by_default,
|
||||
display_env_section: self.display_env_section,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: self.issue_url,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata: metadata,
|
||||
};
|
||||
|
||||
(panic_hook, eyre_hook)
|
||||
@ -533,6 +602,7 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
|
||||
hook.panic_message.display(printer.0, out)?;
|
||||
|
||||
let v = panic_verbosity();
|
||||
let capture_bt = v != Verbosity::Minimal;
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
let span_trace = if hook.spantrace_capture_enabled() {
|
||||
@ -541,6 +611,12 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
|
||||
None
|
||||
};
|
||||
|
||||
let bt = if capture_bt {
|
||||
Some(backtrace::Backtrace::new())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut separated = out.header("\n\n");
|
||||
|
||||
if let Some(ref section) = hook.section {
|
||||
@ -558,10 +634,7 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
|
||||
}
|
||||
}
|
||||
|
||||
let capture_bt = v != Verbosity::Minimal;
|
||||
|
||||
if capture_bt {
|
||||
let bt = backtrace::Backtrace::new();
|
||||
if let Some(bt) = bt.as_ref() {
|
||||
let fmted_bt = hook.format_backtrace(&bt);
|
||||
write!(
|
||||
indented(&mut separated.ready()).with_format(Format::Uniform { indentation: " " }),
|
||||
@ -580,6 +653,27 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
|
||||
write!(&mut separated.ready(), "{}", env_section)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "issue-url")]
|
||||
if let Some(url) = &hook.issue_url {
|
||||
let payload = printer
|
||||
.0
|
||||
.payload()
|
||||
.downcast_ref::<String>()
|
||||
.map(String::as_str)
|
||||
.or_else(|| printer.0.payload().downcast_ref::<&str>().cloned())
|
||||
.unwrap_or("<non string panic payload>");
|
||||
|
||||
let issue_section = crate::section::github::IssueSection::new(url, payload)
|
||||
.with_backtrace(bt.as_ref())
|
||||
.with_location(printer.0.location())
|
||||
.with_metadata(&**hook.issue_metadata);
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
let issue_section = issue_section.with_span_trace(span_trace.as_ref());
|
||||
|
||||
write!(&mut separated.ready(), "{}", issue_section)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -590,6 +684,10 @@ pub(crate) struct PanicHook {
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
capture_span_trace_by_default: bool,
|
||||
display_env_section: bool,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: Option<String>,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
|
||||
}
|
||||
|
||||
impl PanicHook {
|
||||
@ -615,6 +713,10 @@ pub(crate) struct EyreHook {
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
capture_span_trace_by_default: bool,
|
||||
display_env_section: bool,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: Option<String>,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
|
||||
}
|
||||
|
||||
impl EyreHook {
|
||||
@ -641,6 +743,10 @@ impl EyreHook {
|
||||
span_trace,
|
||||
sections: Vec::new(),
|
||||
display_env_section: self.display_env_section,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: self.issue_url.clone(),
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata: self.issue_metadata.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,12 @@ use std::fmt::Write;
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
use tracing_error::{ExtractSpanTrace, SpanTrace};
|
||||
|
||||
impl std::fmt::Debug for Handler {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("redacted")
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
/// Return a reference to the captured `Backtrace` type
|
||||
pub fn backtrace(&self) -> Option<&Backtrace> {
|
||||
@ -25,8 +31,6 @@ impl Handler {
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler {}
|
||||
|
||||
impl eyre::EyreHandler for Handler {
|
||||
fn debug(
|
||||
&self,
|
||||
@ -38,14 +42,16 @@ impl eyre::EyreHandler for Handler {
|
||||
}
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
let errors = eyre::Chain::new(error)
|
||||
.filter(|e| e.span_trace().is_none())
|
||||
.enumerate();
|
||||
let errors = || {
|
||||
eyre::Chain::new(error)
|
||||
.filter(|e| e.span_trace().is_none())
|
||||
.enumerate()
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "capture-spantrace"))]
|
||||
let errors = eyre::Chain::new(error).enumerate();
|
||||
let errors = || eyre::Chain::new(error).enumerate();
|
||||
|
||||
for (n, error) in errors {
|
||||
for (n, error) in errors() {
|
||||
writeln!(f)?;
|
||||
write!(indented(f).ind(n), "{}", error.bright_red())?;
|
||||
}
|
||||
@ -118,6 +124,24 @@ impl eyre::EyreHandler for Handler {
|
||||
write!(&mut separated.ready(), "{}", env_section)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "issue-url")]
|
||||
if let Some(url) = &self.issue_url {
|
||||
let mut payload = String::from("Error: ");
|
||||
for (n, error) in errors() {
|
||||
writeln!(&mut payload)?;
|
||||
write!(indented(&mut payload).ind(n), "{}", error)?;
|
||||
}
|
||||
|
||||
let issue_section = crate::section::github::IssueSection::new(url, &payload)
|
||||
.with_backtrace(self.backtrace.as_ref())
|
||||
.with_metadata(&**self.issue_metadata);
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
let issue_section = issue_section.with_span_trace(span_trace);
|
||||
|
||||
write!(&mut separated.ready(), "{}", issue_section)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -334,7 +334,7 @@
|
||||
//! [`examples/custom_filter.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_filter.rs
|
||||
//! [`examples/custom_section.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_section.rs
|
||||
//! [`examples/multiple_errors.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/multiple_errors.rs
|
||||
#![doc(html_root_url = "https://docs.rs/color-eyre/0.5.3")]
|
||||
#![doc(html_root_url = "https://docs.rs/color-eyre/0.5.4")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
@ -359,6 +359,7 @@
|
||||
while_true
|
||||
)]
|
||||
#![allow(clippy::try_err)]
|
||||
|
||||
use backtrace::Backtrace;
|
||||
pub use eyre;
|
||||
#[doc(hidden)]
|
||||
@ -393,13 +394,17 @@ mod writers;
|
||||
/// [`tracing-error`]: https://docs.rs/tracing-error
|
||||
/// [`color_eyre::Report`]: type.Report.html
|
||||
/// [`color_eyre::Result`]: type.Result.html
|
||||
#[derive(Debug)]
|
||||
pub struct Handler {
|
||||
backtrace: Option<Backtrace>,
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
span_trace: Option<SpanTrace>,
|
||||
sections: Vec<HelpInfo>,
|
||||
display_env_section: bool,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_url: Option<String>,
|
||||
#[cfg(feature = "issue-url")]
|
||||
issue_metadata:
|
||||
std::sync::Arc<Vec<(String, Box<dyn std::fmt::Display + Send + Sync + 'static>)>>,
|
||||
}
|
||||
|
||||
static CONFIG: OnceCell<config::PanicHook> = OnceCell::new();
|
||||
|
185
src/section/github.rs
Normal file
185
src/section/github.rs
Normal file
@ -0,0 +1,185 @@
|
||||
use crate::writers::DisplayExt;
|
||||
use backtrace::Backtrace;
|
||||
use std::{fmt, panic::Location};
|
||||
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>,
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
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>)>,
|
||||
{
|
||||
// This is implemented as a free functions so it can consume the `metadata`
|
||||
// iterator, rather than being forced to leave it unmodified if its behind a
|
||||
// `&self` shared reference via the Display trait
|
||||
#[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)
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
use crate::writers::WriterExt;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
#[cfg(feature = "issue-url")]
|
||||
pub(crate) mod github;
|
||||
pub(crate) mod help;
|
||||
|
||||
/// An indented section with a header for an error report
|
||||
|
@ -28,6 +28,27 @@ impl<W> WriterExt for W {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait DisplayExt: Sized + Display {
|
||||
fn with_header<H: Display>(self, header: H) -> Header<Self, H>;
|
||||
fn with_footer<F: Display>(self, footer: F) -> Footer<Self, F>;
|
||||
}
|
||||
|
||||
impl<T> DisplayExt for T
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
fn with_footer<F: Display>(self, footer: F) -> Footer<Self, F> {
|
||||
Footer { body: self, footer }
|
||||
}
|
||||
|
||||
fn with_header<H: Display>(self, header: H) -> Header<Self, H> {
|
||||
Header {
|
||||
body: self,
|
||||
h: header,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ReadyHeaderWriter<'a, 'b, H: ?Sized, W>(&'b mut HeaderWriter<'a, H, W>);
|
||||
|
||||
impl<'a, H: ?Sized, W> HeaderWriter<'a, H, W> {
|
||||
@ -59,6 +80,77 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FooterWriter<W> {
|
||||
inner: W,
|
||||
had_output: bool,
|
||||
}
|
||||
|
||||
impl<W> fmt::Write for FooterWriter<W>
|
||||
where
|
||||
W: fmt::Write,
|
||||
{
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
if !self.had_output && !s.is_empty() {
|
||||
self.had_output = true;
|
||||
}
|
||||
|
||||
self.inner.write_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(explicit_outlives_requirements)]
|
||||
pub(crate) struct Footer<B, H>
|
||||
where
|
||||
B: Display,
|
||||
H: Display,
|
||||
{
|
||||
body: B,
|
||||
footer: H,
|
||||
}
|
||||
|
||||
impl<B, H> fmt::Display for Footer<B, H>
|
||||
where
|
||||
B: Display,
|
||||
H: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut inner_f = FooterWriter {
|
||||
inner: &mut *f,
|
||||
had_output: false,
|
||||
};
|
||||
|
||||
write!(&mut inner_f, "{}", self.body)?;
|
||||
|
||||
if inner_f.had_output {
|
||||
self.footer.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(explicit_outlives_requirements)]
|
||||
pub(crate) struct Header<B, H>
|
||||
where
|
||||
B: Display,
|
||||
H: Display,
|
||||
{
|
||||
body: B,
|
||||
h: H,
|
||||
}
|
||||
|
||||
impl<B, H> fmt::Display for Header<B, H>
|
||||
where
|
||||
B: Display,
|
||||
H: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f.header(&self.h).ready(), "{}", self.body)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
pub(crate) struct FormattedSpanTrace<'a>(pub(crate) &'a SpanTrace);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user