Skip to content

Commit

Permalink
Add support for different output formats (compact, detailed, markdown) (
Browse files Browse the repository at this point in the history
  • Loading branch information
mre authored Nov 17, 2021
1 parent d3ed133 commit b97fda3
Show file tree
Hide file tree
Showing 16 changed files with 421 additions and 119 deletions.
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lychee-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 21 additions & 0 deletions lychee-bin/src/color.rs
Original file line number Diff line number Diff line change
@@ -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;
41 changes: 24 additions & 17 deletions lychee-bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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(())
}
14 changes: 9 additions & 5 deletions lychee-bin/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,28 @@ 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)),
}
}
}

impl Default for Format {
fn default() -> Self {
Format::String
Format::Compact
}
}

Expand Down Expand Up @@ -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,

Expand Down
83 changes: 14 additions & 69 deletions lychee-bin/src/stats.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
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 {
Status::Ok(_) => GREEN.apply_to(response),
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 {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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};
Expand Down
Loading

0 comments on commit b97fda3

Please sign in to comment.