Add support for different output formats (compact, detailed, markdown) (#375)

This commit is contained in:
Matthias 2021-11-18 00:44:48 +01:00 committed by GitHub
parent d3ed133f10
commit b97fda34d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 421 additions and 119 deletions

31
Cargo.lock generated
View file

@ -1486,6 +1486,7 @@ dependencies = [
"serde",
"serde_json",
"structopt",
"tabled",
"tempfile",
"tokio",
"toml",
@ -1803,6 +1804,15 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "papergrid"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe93a33fc67c8a9d21f3fefa54c4a991e08d2eba5f2fd6f327ca520862eddfb"
dependencies = [
"unicode-width",
]
[[package]]
name = "parking"
version = "2.0.0"
@ -2582,6 +2592,27 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tabled"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465c45048a4b177eee43527a3741866dd66839f15c3223b5047dbc43d8a5076e"
dependencies = [
"papergrid",
"tabled_derive",
]
[[package]]
name = "tabled_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b366a04a152b687a209b2459f29da3650b74bc859390308949305211e28af4b9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tempfile"
version = "3.2.0"

View file

@ -230,7 +230,8 @@ OPTIONS:
-c, --config <config-file> Configuration file to use [default: ./lychee.toml]
--exclude <exclude>... Exclude URLs from checking (supports regex)
--exclude-file <exclude-file>... A file or files that contains URLs to exclude from checking
-f, --format <format> Output file format of status report (json, string) [default: string]
-f, --format <format> Output format of final status report (compact, detailed, json, markdown)
[default: compact]
--github-token <github-token> GitHub API token to use when checking github.com links, to avoid rate
limiting [env: GITHUB_TOKEN=]
-h, --headers <headers>... Custom request headers

View file

@ -36,6 +36,7 @@ ring = "0.16.20"
serde = { version = "1.0.125", features = ["derive"] }
serde_json = "1.0.70"
structopt = "0.3.25"
tabled = "0.3.0"
tokio = { version = "1.14.0", features = ["full"] }
toml = "0.5.8"
once_cell = "1.8.0"

21
lychee-bin/src/color.rs Normal file
View file

@ -0,0 +1,21 @@
use console::Style;
use once_cell::sync::Lazy;
pub(crate) static NORMAL: Lazy<Style> = Lazy::new(Style::new);
pub(crate) static DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
pub(crate) static GREEN: Lazy<Style> = Lazy::new(|| Style::new().green().bright());
pub(crate) static BOLD_GREEN: Lazy<Style> = Lazy::new(|| Style::new().green().bold().bright());
pub(crate) static YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow().bright());
pub(crate) static BOLD_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow().bold().bright());
pub(crate) static PINK: Lazy<Style> = Lazy::new(|| Style::new().color256(197).bright());
pub(crate) static BOLD_PINK: Lazy<Style> = Lazy::new(|| Style::new().color256(197).bold().bright());
// Write output using predefined colors
macro_rules! color {
($f:ident, $color:ident, $text:tt, $($tts:tt)*) => {
write!($f, "{}", $color.apply_to(format!($text, $($tts)*)))
};
}
pub(crate) use color;

View file

@ -60,6 +60,7 @@
// required for apple silicon
use ring as _;
use stats::color_response;
use std::fs::File;
use std::io::{self, BufRead, Write};
@ -76,14 +77,17 @@ use ring as _; // required for apple silicon
use structopt::StructOpt;
use tokio::sync::mpsc;
mod color;
mod options;
mod parse;
mod stats;
mod writer;
use crate::parse::{parse_basic_auth, parse_headers, parse_statuscodes, parse_timeout};
use crate::{
options::{Config, Format, LycheeOptions},
stats::{color_response, ResponseStats},
stats::ResponseStats,
writer::StatsWriter,
};
/// A C-like enum that can be cast to `i32` and used as process exit code.
@ -157,13 +161,6 @@ fn show_progress(progress_bar: &Option<ProgressBar>, response: &Response, verbos
}
}
fn fmt(stats: &ResponseStats, format: &Format) -> Result<String> {
Ok(match format {
Format::String => stats.to_string(),
Format::Json => serde_json::to_string_pretty(&stats)?,
})
}
async fn run(cfg: &Config, inputs: Vec<Input>) -> Result<i32> {
let mut headers = parse_headers(&cfg.headers)?;
if let Some(auth) = &cfg.basic_auth {
@ -261,13 +258,22 @@ async fn run(cfg: &Config, inputs: Vec<Input>) -> Result<i32> {
pb.finish_and_clear();
}
write_stats(&stats, cfg)?;
let writer: Box<dyn StatsWriter> = match cfg.format {
Format::Compact => Box::new(writer::Compact::new()),
Format::Detailed => Box::new(writer::Detailed::new()),
Format::Json => Box::new(writer::Json::new()),
Format::Markdown => Box::new(writer::Markdown::new()),
};
if stats.is_success() {
Ok(ExitCode::Success as i32)
let code = if stats.is_success() {
ExitCode::Success
} else {
Ok(ExitCode::LinkCheckFailure as i32)
}
ExitCode::LinkCheckFailure
};
write_stats(&*writer, stats, cfg)?;
Ok(code as i32)
}
/// Dump all detected links to stdout without checking them
@ -289,18 +295,19 @@ fn dump_links<'a>(links: impl Iterator<Item = &'a Request>) -> ExitCode {
}
/// Write final statistics to stdout or to file
fn write_stats(stats: &ResponseStats, cfg: &Config) -> Result<()> {
let formatted = fmt(stats, &cfg.format)?;
fn write_stats(writer: &dyn StatsWriter, stats: ResponseStats, cfg: &Config) -> Result<()> {
let is_empty = stats.is_empty();
let formatted = writer.write(stats)?;
if let Some(output) = &cfg.output {
fs::write(output, formatted).context("Cannot write status output to file")?;
} else {
if cfg.verbose && !stats.is_empty() {
if cfg.verbose && !is_empty {
// separate summary from the verbose list of links above
println!();
}
// we assume that the formatted stats don't have a final newline
println!("{}", stats);
println!("{}", formatted);
}
Ok(())
}

View file

@ -22,16 +22,20 @@ lazy_static! {
#[derive(Debug, Deserialize)]
pub(crate) enum Format {
String,
Compact,
Detailed,
Json,
Markdown,
}
impl FromStr for Format {
type Err = Error;
fn from_str(format: &str) -> Result<Self, Self::Err> {
match format {
"string" => Ok(Format::String),
"compact" | "string" => Ok(Format::Compact),
"detailed" => Ok(Format::Detailed),
"json" => Ok(Format::Json),
"markdown" | "md" => Ok(Format::Markdown),
_ => Err(anyhow!("Could not parse format {}", format)),
}
}
@ -39,7 +43,7 @@ impl FromStr for Format {
impl Default for Format {
fn default() -> Self {
Format::String
Format::Compact
}
}
@ -262,8 +266,8 @@ pub(crate) struct Config {
#[serde(default)]
pub(crate) output: Option<PathBuf>,
/// Output file format of status report (json, string)
#[structopt(short, long, default_value = "string")]
/// Output format of final status report (compact, detailed, json, markdown)
#[structopt(short, long, default_value = "compact")]
#[serde(default)]
pub(crate) format: Format,

View file

@ -1,22 +1,9 @@
use std::{
collections::{HashMap, HashSet},
fmt::{self, Display},
};
use std::collections::{HashMap, HashSet};
use console::Style;
use lychee_lib::{Input, Response, ResponseBody, Status};
use once_cell::sync::Lazy;
use pad::{Alignment, PadStr};
use serde::Serialize;
static GREEN: Lazy<Style> = Lazy::new(|| Style::new().green().bright());
static DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
static NORMAL: Lazy<Style> = Lazy::new(Style::new);
static YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow().bright());
static RED: Lazy<Style> = Lazy::new(|| Style::new().red().bright());
// Maximum padding for each entry in the final statistics output
const MAX_PADDING: usize = 20;
use crate::color::{DIM, GREEN, NORMAL, PINK, YELLOW};
pub(crate) fn color_response(response: &ResponseBody) -> String {
let out = match response.status {
@ -24,22 +11,22 @@ pub(crate) fn color_response(response: &ResponseBody) -> String {
Status::Excluded | Status::Unsupported(_) => DIM.apply_to(response),
Status::Redirected(_) => NORMAL.apply_to(response),
Status::UnknownStatusCode(_) | Status::Timeout(_) => YELLOW.apply_to(response),
Status::Error(_) => RED.apply_to(response),
Status::Error(_) => PINK.apply_to(response),
};
out.to_string()
}
#[derive(Default, Serialize)]
pub(crate) struct ResponseStats {
total: usize,
successful: usize,
failures: usize,
unknown: usize,
timeouts: usize,
redirects: usize,
excludes: usize,
errors: usize,
fail_map: HashMap<Input, HashSet<ResponseBody>>,
pub(crate) total: usize,
pub(crate) successful: usize,
pub(crate) failures: usize,
pub(crate) unknown: usize,
pub(crate) timeouts: usize,
pub(crate) redirects: usize,
pub(crate) excludes: usize,
pub(crate) errors: usize,
pub(crate) fail_map: HashMap<Input, HashSet<ResponseBody>>,
}
impl ResponseStats {
@ -50,8 +37,9 @@ impl ResponseStats {
pub(crate) fn add(&mut self, response: Response) {
let Response(source, ResponseBody { ref status, .. }) = response;
// Silently skip unsupported URIs
if status.is_unsupported() {
// Silently skip unsupported URIs
return;
}
@ -87,49 +75,6 @@ impl ResponseStats {
}
}
fn write_stat(f: &mut fmt::Formatter, title: &str, stat: usize, newline: bool) -> fmt::Result {
let fill = title.chars().count();
f.write_str(title)?;
f.write_str(
&stat
.to_string()
.pad(MAX_PADDING - fill, '.', Alignment::Right, false),
)?;
if newline {
f.write_str("\n")?;
}
Ok(())
}
impl Display for ResponseStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let separator = "-".repeat(MAX_PADDING + 1);
writeln!(f, "\u{1f4dd} Summary")?; // 📝
writeln!(f, "{}", separator)?;
write_stat(f, "\u{1f50d} Total", self.total, true)?; // 🔍
write_stat(f, "\u{2705} Successful", self.successful, true)?; // ✅
write_stat(f, "\u{23f3} Timeouts", self.timeouts, true)?; // ⏳
write_stat(f, "\u{1f500} Redirected", self.redirects, true)?; // 🔀
write_stat(f, "\u{1f47b} Excluded", self.excludes, true)?; // 👻
write_stat(f, "\u{26a0} Unknown", self.unknown, true)?; // ⚠️
write_stat(f, "\u{1f6ab} Errors", self.errors + self.failures, false)?; // 🚫
for (input, responses) in &self.fail_map {
// Using leading newlines over trailing ones (e.g. `writeln!`)
// lets us avoid extra newlines without any additional logic.
write!(f, "\n\nErrors in {}", input)?;
for response in responses {
write!(f, "\n{}", color_response(response))?;
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::collections::{HashMap, HashSet};

View file

@ -0,0 +1,86 @@
use std::{
collections::HashMap,
fmt::{self, Display},
};
use crate::{
color::{color, BOLD_GREEN, BOLD_PINK, BOLD_YELLOW, NORMAL, PINK},
stats::{color_response, ResponseStats},
};
use super::StatsWriter;
use anyhow::Result;
struct CompactResponseStats(ResponseStats);
// Helper function, which prints the detailed list of errors
pub(crate) fn print_errors(stats: &ResponseStats) -> String {
let mut errors = HashMap::new();
errors.insert("HTTP", stats.failures);
errors.insert("Redirects", stats.redirects);
errors.insert("Timeouts", stats.timeouts);
errors.insert("Unknown", stats.unknown);
// Creates an output like `(HTTP:3|Timeouts:1|Unknown:1)`
let mut err: Vec<_> = errors
.into_iter()
.filter(|(_, v)| *v > 0)
.map(|(k, v)| format!("{}:{}", k, v))
.collect();
err.sort();
err.join("|")
}
impl Display for CompactResponseStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stats = &self.0;
if !stats.fail_map.is_empty() {
color!(
f,
BOLD_PINK,
"Issues found in {} input(s). Find details below.\n\n",
stats.fail_map.len()
)?;
}
for (input, responses) in &stats.fail_map {
color!(f, BOLD_YELLOW, "[{}]:\n", input)?;
for response in responses {
writeln!(f, "{}", color_response(response))?;
}
writeln!(f)?;
}
color!(f, NORMAL, "\u{1F50D} {} Total", stats.total)?;
color!(f, BOLD_GREEN, " \u{2705} {} OK", stats.successful)?;
let total_errors = stats.errors + stats.failures;
let err_str = if total_errors == 1 { "Error" } else { "Errors" };
color!(f, BOLD_PINK, " \u{1f6ab} {} {}", total_errors, err_str)?;
if total_errors > 0 {
write!(f, " ")?;
color!(f, PINK, "({})", print_errors(stats))?;
}
if stats.excludes > 0 {
color!(f, BOLD_YELLOW, " \u{1F4A4} {} Excluded", stats.excludes)?;
}
Ok(())
}
}
pub(crate) struct Compact;
impl Compact {
pub(crate) const fn new() -> Self {
Compact {}
}
}
impl StatsWriter for Compact {
fn write(&self, stats: ResponseStats) -> Result<String> {
let compact = CompactResponseStats(stats);
Ok(compact.to_string())
}
}

View file

@ -0,0 +1,72 @@
use super::StatsWriter;
use crate::stats::{color_response, ResponseStats};
use anyhow::Result;
use pad::{Alignment, PadStr};
use std::fmt::{self, Display};
// Maximum padding for each entry in the final statistics output
const MAX_PADDING: usize = 20;
fn write_stat(f: &mut fmt::Formatter, title: &str, stat: usize, newline: bool) -> fmt::Result {
let fill = title.chars().count();
f.write_str(title)?;
f.write_str(
&stat
.to_string()
.pad(MAX_PADDING - fill, '.', Alignment::Right, false),
)?;
if newline {
f.write_str("\n")?;
}
Ok(())
}
impl Display for DetailedResponseStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stats = &self.0;
let separator = "-".repeat(MAX_PADDING + 1);
writeln!(f, "\u{1f4dd} Summary")?; // 📝
writeln!(f, "{}", separator)?;
write_stat(f, "\u{1f50d} Total", stats.total, true)?; // 🔍
write_stat(f, "\u{2705} Successful", stats.successful, true)?; // ✅
write_stat(f, "\u{23f3} Timeouts", stats.timeouts, true)?; // ⏳
write_stat(f, "\u{1f500} Redirected", stats.redirects, true)?; // 🔀
write_stat(f, "\u{1f47b} Excluded", stats.excludes, true)?; // 👻
write_stat(f, "\u{2753} Unknown", stats.unknown, true)?; //❓
write_stat(f, "\u{1f6ab} Errors", stats.errors + stats.failures, false)?; // 🚫
for (input, responses) in &stats.fail_map {
// Using leading newlines over trailing ones (e.g. `writeln!`)
// lets us avoid extra newlines without any additional logic.
write!(f, "\n\nErrors in {}", input)?;
for response in responses {
write!(f, "\n{}", color_response(response))?;
}
}
Ok(())
}
}
/// Wrap as newtype because multiple `Display` implementations are not allowed
/// for `ResponseStats`
struct DetailedResponseStats(ResponseStats);
pub(crate) struct Detailed;
impl Detailed {
pub(crate) const fn new() -> Self {
Detailed {}
}
}
impl StatsWriter for Detailed {
fn write(&self, stats: ResponseStats) -> Result<String> {
let detailed = DetailedResponseStats(stats);
Ok(detailed.to_string())
}
}

View file

@ -0,0 +1,18 @@
use anyhow::{Context, Result};
use super::StatsWriter;
use crate::stats::ResponseStats;
pub(crate) struct Json;
impl Json {
pub(crate) const fn new() -> Self {
Json {}
}
}
impl StatsWriter for Json {
fn write(&self, stats: ResponseStats) -> Result<String> {
serde_json::to_string_pretty(&stats).context("Cannot format stats as JSON")
}
}

View file

@ -0,0 +1,100 @@
use std::fmt::{self, Display};
use super::StatsWriter;
use anyhow::Result;
use tabled::{style::Line, Alignment, Full, Modify, Table, Tabled};
use crate::stats::ResponseStats;
#[derive(Tabled)]
struct StatsTableEntry {
#[header("Status")]
status: &'static str,
#[header("Count")]
count: usize,
}
fn stats_table(stats: &ResponseStats) -> String {
let stats = vec![
StatsTableEntry {
status: "\u{1f50d} Total",
count: stats.total,
},
StatsTableEntry {
status: "\u{2705} Successful",
count: stats.successful,
},
StatsTableEntry {
status: "\u{23f3} Timeouts",
count: stats.timeouts,
},
StatsTableEntry {
status: "\u{1f500} Redirected",
count: stats.redirects,
},
StatsTableEntry {
status: "\u{1f47b} Excluded",
count: stats.excludes,
},
StatsTableEntry {
status: "\u{2753} Unknown",
count: stats.unknown,
},
StatsTableEntry {
status: "\u{1f6ab} Errors",
count: stats.errors + stats.failures,
},
];
let style = tabled::Style::github_markdown().header(Some(Line::bordered('-', '|', '|', '|')));
Table::new(stats)
.with(Modify::new(Full).with(Alignment::left()))
.with(style)
.to_string()
}
struct MarkdownResponseStats(ResponseStats);
impl Display for MarkdownResponseStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stats = &self.0;
writeln!(f, "## Summary")?;
writeln!(f)?;
writeln!(f, "{}", stats_table(&self.0))?;
if !&stats.fail_map.is_empty() {
writeln!(f, "## Errors per input")?;
for (input, responses) in &stats.fail_map {
// Using leading newlines over trailing ones (e.g. `writeln!`)
// lets us avoid extra newlines without any additional logic.
writeln!(f, "### Errors in {}", input)?;
for response in responses {
writeln!(
f,
"* [{}]({}): {}",
response.uri, response.uri, response.status
)?;
}
writeln!(f)?;
}
}
Ok(())
}
}
pub(crate) struct Markdown;
impl Markdown {
pub(crate) const fn new() -> Self {
Markdown {}
}
}
impl StatsWriter for Markdown {
fn write(&self, stats: ResponseStats) -> Result<String> {
let markdown = MarkdownResponseStats(stats);
Ok(markdown.to_string())
}
}

View file

@ -0,0 +1,16 @@
mod compact;
mod detailed;
mod json;
mod markdown;
use crate::stats::ResponseStats;
use anyhow::Result;
pub(crate) use compact::Compact;
pub(crate) use detailed::Detailed;
pub(crate) use json::Json;
pub(crate) use markdown::Markdown;
pub(crate) trait StatsWriter {
fn write(&self, stats: ResponseStats) -> Result<String>;
}

View file

@ -146,9 +146,9 @@ mod cli {
.env_clear()
.assert()
.success()
.stdout(contains("Total............2"))
.stdout(contains("Successful.......1"))
.stdout(contains("Excluded.........1"));
.stdout(contains("2 Total"))
.stdout(contains("1 OK"))
.stdout(contains("1 Excluded"));
}
#[test]
@ -163,8 +163,8 @@ mod cli {
.env_clear()
.assert()
.success()
.stdout(contains("Total............3"))
.stdout(contains("Successful.......3"));
.stdout(contains("3 Total"))
.stdout(contains("3 OK"));
}
#[test]
@ -210,9 +210,9 @@ mod cli {
.env_clear()
.assert()
.success()
.stdout(contains("Total............3"))
.stdout(contains("Successful.......2"))
.stdout(contains("Excluded.........1"));
.stdout(contains("3 Total"))
.stdout(contains("2 OK"))
.stdout(contains("1 Excluded"));
}
#[test]
@ -225,8 +225,8 @@ mod cli {
.env_clear()
.assert()
.success()
.stdout(contains("Total............1"))
.stdout(contains("Successful.......1"));
.stdout(contains("1 Total"))
.stdout(contains("1 OK"));
}
#[test]
@ -248,8 +248,8 @@ mod cli {
.env_clear()
.assert()
.success()
.stdout(contains("Total............1"))
.stdout(contains("Successful.......1"));
.stdout(contains("1 TOTAL"))
.stdout(contains("1 OK"));
}
#[test]
@ -263,8 +263,8 @@ mod cli {
.assert()
.failure()
.code(2)
.stdout(contains("https://github.com/mre/idiomatic-rust-doesnt-exist-man \
(GitHub token not specified. To check GitHub links reliably, use `--github-token` flag / `GITHUB_TOKEN` env var.)"));
.stdout(contains("https://github.com/mre/idiomatic-rust-doesnt-exist-man: \
GitHub token not specified. To check GitHub links reliably, use `--github-token` flag / `GITHUB_TOKEN` env var."));
}
#[tokio::test]
@ -309,7 +309,7 @@ mod cli {
#[test]
fn test_missing_file_error() {
let mut cmd = main_command();
let filename = format!("non-existing-file-{}", uuid::Uuid::new_v4().to_string());
let filename = format!("non-existing-file-{}", uuid::Uuid::new_v4());
cmd.arg(&filename)
.assert()
@ -324,7 +324,7 @@ mod cli {
#[test]
fn test_missing_file_ok_if_skip_missing() {
let mut cmd = main_command();
let filename = format!("non-existing-file-{}", uuid::Uuid::new_v4().to_string());
let filename = format!("non-existing-file-{}", uuid::Uuid::new_v4());
cmd.arg(&filename).arg("--skip-missing").assert().success();
}
@ -347,7 +347,7 @@ mod cli {
.arg("--verbose")
.assert()
.success()
.stdout(contains("Total............2"));
.stdout(contains("2 Total"));
Ok(())
}
@ -371,7 +371,7 @@ mod cli {
.arg("--glob-ignore-case")
.assert()
.success()
.stdout(contains("Total............2"));
.stdout(contains("2 Total"));
Ok(())
}
@ -394,7 +394,7 @@ mod cli {
.arg("--verbose")
.assert()
.success()
.stdout(contains("Total............1"));
.stdout(contains("1 Total"));
Ok(())
}
@ -432,7 +432,7 @@ mod cli {
.arg(".*")
.assert()
.success()
.stdout(contains("Excluded........11"));
.stdout(contains("11 Excluded"));
Ok(())
}
@ -448,7 +448,7 @@ mod cli {
.arg("https://ldra.com/")
.assert()
.success()
.stdout(contains("Excluded.........2"));
.stdout(contains("2 Excluded"));
Ok(())
}
@ -464,7 +464,7 @@ mod cli {
.arg(excludes_path)
.assert()
.success()
.stdout(contains("Excluded.........2"));
.stdout(contains("2 Excluded"));
Ok(())
}
@ -482,7 +482,7 @@ mod cli {
.arg(excludes_path2)
.assert()
.success()
.stdout(contains("Excluded.........3"));
.stdout(contains("3 Excluded"));
Ok(())
}
@ -512,7 +512,7 @@ mod cli {
.env_clear()
.assert()
.success()
.stdout(contains("Total............0"));
.stdout(contains("0 Total"));
Ok(())
}

View file

@ -28,7 +28,7 @@ mod cli {
.env_clear()
.assert()
.success()
.stdout(contains("Total............1"))
.stdout(contains("1 Total"))
.stdout(contains("foo.html"));
Ok(())

View file

@ -254,7 +254,7 @@ impl Client {
.repo(owner, repo)
.get()
.await
.map_or_else(|e| e.into(), |_| Status::Ok(StatusCode::OK)),
.map_or_else(std::convert::Into::into, |_| Status::Ok(StatusCode::OK)),
None => ErrorKind::MissingGitHubToken.into(),
}
}

View file

@ -60,7 +60,7 @@ impl Display for ResponseBody {
write!(f, " [{}]", code)
}
Status::Timeout(Some(code)) => write!(f, " [{}]", code),
Status::Error(e) => write!(f, " ({})", e),
Status::Error(e) => write!(f, ": {}", e),
_ => Ok(()),
}
}