From c4b8055c89ea499f67b1f0ff08b9f04e64056702 Mon Sep 17 00:00:00 2001 From: Luke Hsiao Date: Thu, 10 Aug 2023 13:25:13 -0600 Subject: [PATCH 1/2] feat: provide `miette`-powered error diagnostics While we're at it, refactor the code into more reasonable functions. Also switches to use more structured logging. Signed-off-by: Luke Hsiao --- Cargo.lock | 117 ++++++++++++++++++++- Cargo.toml | 1 + README.md | 19 ++-- src/lib.rs | 293 +++++++++++++++++++++++++++++----------------------- src/main.rs | 30 +++--- 5 files changed, 306 insertions(+), 154 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 694bb79..1c67145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,15 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "base64" version = "0.21.2" @@ -281,7 +290,7 @@ dependencies = [ "clap_lex", "once_cell", "strsim 0.10.0", - "terminal_size", + "terminal_size 0.2.6", ] [[package]] @@ -799,6 +808,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "itoa" version = "1.0.9" @@ -910,6 +925,38 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "backtrace", + "backtrace-ext", + "is-terminal", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size 0.1.17", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote 1.0.32", + "syn 2.0.28", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -987,6 +1034,7 @@ dependencies = [ "html-escape", "indicatif", "log", + "miette", "rayon", "serde", "serde_json", @@ -1005,6 +1053,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1495,6 +1549,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "spin" version = "0.5.2" @@ -1539,6 +1599,34 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "supports-color" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4950e7174bffabe99455511c39707310e7e9b440364a2fcb1cc21521be57b354" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" +dependencies = [ + "is-terminal", +] + +[[package]] +name = "supports-unicode" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" +dependencies = [ + "is-terminal", +] + [[package]] name = "syn" version = "0.11.11" @@ -1637,6 +1725,16 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "terminal_size" version = "0.2.6" @@ -1647,6 +1745,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "textwrap" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.44" @@ -1847,6 +1956,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.22" diff --git a/Cargo.toml b/Cargo.toml index 47c925f..d51e9ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ clap-verbosity-flag = "2.0.1" html-escape = "0.2.13" indicatif = { version = "0.17.6", features = ["rayon"] } log = "0.4.19" +miette = { version = "5.10.0", features = ["fancy"] } rayon = "1.7.0" serde = { version = "1.0.183", features = ["derive"] } serde_json = "1.0.104" diff --git a/README.md b/README.md index 416145a..ba318c1 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,17 @@
+`openring-rs` is a tool for generating a webring from RSS feeds, so you can populate a template with +articles from those feeds and embed them in your own blog. An example template is provided in +`in.html`. + This is a rust-port of Drew DeVault's [openring](https://git.sr.ht/~sircmpwn/openring), with the primary differences being: - the template is provided as a argument, not read from stdin - we show a little progress bar - we fetch all feeds concurrently -- we allow filtering feeds with `--before`. - -`openring-rs` is a tool for generating a webring from RSS feeds, so you can populate a template with -articles from those feeds and embed them in your own blog. An example template is provided in -`in.html`. +- we allow filtering feeds with `--before` +- we provide better error messages (via [miette](https://github.com/zkat/miette)) ## Install @@ -43,14 +44,14 @@ Usage: openring [OPTIONS] --template-file Options: -n, --num-articles Total number of articles to fetch [default: 3] -p, --per-source Number of most recent articles to get from each feed [default: 1] - -S, --url-file File with URLs of RSS feeds to read (one URL per line) + -S, --url-file File with URLs of RSS feeds to read (one URL per line, lines starting with '#' or "//" ignored) -t, --template-file Tera template file - -s, --urls A specific URL to consider (can be repeated) + -s, --urls A single URL to consider (can be repeated to specify multiple) -b, --before Only include articles before this date (in YYYY-MM-DD format) -v, --verbose... More output per occurrence -q, --quiet... Less output per occurrence - -h, --help Print help information (use `--help` for more detail) - -V, --version Print version information + -h, --help Print help (see more with '--help') + -V, --version Print version ``` ## Using Tera Templates diff --git a/src/lib.rs b/src/lib.rs index 06e94be..b6ecb1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ -use std::fs::{self, File}; - -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; -use std::time::Duration; +use std::{ + fs::{self, File}, + io::{BufRead, BufReader}, + path::PathBuf, + result, + time::Duration, +}; -use anyhow::{anyhow, bail, Context, Result}; use chrono::{ naive::{NaiveDate, NaiveDateTime}, DateTime, FixedOffset, Local, TimeZone, @@ -12,23 +13,65 @@ use chrono::{ use clap::{builder::ValueHint, crate_name, crate_version, Parser}; use clap_verbosity_flag::{Verbosity, WarnLevel}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use miette::{Diagnostic, NamedSource, SourceSpan}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use serde::Serialize; use syndication::Feed; use tera::Tera; use thiserror::Error; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, info, warn}; use ureq::{Agent, AgentBuilder}; use url::Url; -#[derive(Error, Debug)] +type Result = result::Result; + +#[derive(Error, Debug, Diagnostic)] pub enum OpenringError { #[error("No valid published or updated date found.")] DateError, #[error("No feed urls were provided. Provide feeds with -s or -S .")] FeedMissing, + #[error("Failed to parse naive date.")] + NaiveDateError(#[from] chrono::ParseError), #[error(transparent)] - ChronoError(#[from] chrono::ParseError), + #[diagnostic(transparent)] + ChronoError(#[from] ChronoError), + #[error(transparent)] + #[diagnostic(transparent)] + FeedUrlError(#[from] FeedUrlError), + #[error("Failed to open file.")] + #[diagnostic(code(openring::io_error))] + IoError(#[from] std::io::Error), + #[error("Failed to parse URL.")] + #[diagnostic(code(openring::url_parse_error))] + UrlParseError(#[from] url::ParseError), + #[error("Failed to parse tera template.")] + #[diagnostic(code(openring::template_error))] + TemplateError(#[from] tera::Error), +} + +#[derive(Error, Diagnostic, Debug)] +#[error("Failed to parse datetime.")] +#[diagnostic(code(openring::chrono_error))] +pub struct ChronoError { + #[source_code] + pub src: NamedSource, + #[label("this date is invalid")] + pub span: SourceSpan, + #[help] + pub help: String, +} + +#[derive(Error, Diagnostic, Debug)] +#[error("Failed to parse feed url.")] +#[diagnostic(code(openring::url_parse_error))] +pub struct FeedUrlError { + #[source_code] + pub src: NamedSource, + #[label("this url is invalid")] + pub span: SourceSpan, + #[help] + pub help: String, } #[derive(Parser, Debug)] @@ -40,13 +83,13 @@ pub struct Args { /// Number of most recent articles to get from each feed #[arg(short, long, default_value_t = 1)] per_source: usize, - /// File with URLs of RSS feeds to read (one URL per line) + /// File with URLs of RSS feeds to read (one URL per line, lines starting with '#' or "//" ignored) #[arg(short = 'S', long, value_name = "FILE", value_hint=ValueHint::FilePath)] url_file: Option, /// Tera template file #[arg(short, long, value_parser, value_name = "FILE", value_hint=ValueHint::FilePath)] template_file: PathBuf, - /// A specific URL to consider (can be repeated) + /// A single URL to consider (can be repeated to specify multiple) #[arg(short = 's', long, value_hint=ValueHint::Url)] urls: Vec, /// Only include articles before this date (in YYYY-MM-DD format). @@ -74,36 +117,42 @@ fn parse_naive_date(input: &str) -> Result { NaiveDate::parse_from_str(input, "%Y-%m-%d").map_err(|e| e.into()) } -pub fn run(args: Args) -> Result<()> { - trace!("Args: {:#?}", args); - let mut urls = args.urls; - - if let Some(file) = args.url_file { - let file = File::open(file)?; - let reader = BufReader::new(file); +fn parse_urls_from_file(path: PathBuf) -> Result> { + let file = File::open(path.clone())?; + let reader = BufReader::new(file); - let mut file_urls: Vec = reader - .lines() - .map(|s| s.expect("Failed to parse line.")) - .map(|l| Url::parse(&l).expect("Failed to parse url")) - .collect(); - urls.append(&mut file_urls); - }; - debug!( - "Fetching these urls: {:#?}", - urls.iter().map(|url| url.as_str()).collect::>() - ); - - if urls.is_empty() { - bail!(OpenringError::FeedMissing) - } - - let template = fs::read_to_string(&args.template_file) - .with_context(|| format!("Failed to read file `{:?}`", args.template_file))?; - let mut context = tera::Context::new(); - - info!("Fetching feeds..."); + reader + .lines() + // Allow '#' or "//" comments in the urls file + .filter(|l| { + let line = l.as_ref().unwrap(); + let trimmed = line.trim(); + !(trimmed.starts_with('#') || trimmed.starts_with("//")) + }) + .map(|line| { + let line = &line.unwrap(); + Url::parse(line).map_err(|e| { + // Give a nice diagnostic error + let file_src = fs::read_to_string(path.clone()).unwrap(); + let offset = file_src.find(line).unwrap(); + FeedUrlError { + src: NamedSource::new( + path.clone().into_os_string().to_string_lossy(), + file_src, + ), + span: (offset..offset + line.len()).into(), + help: e.to_string(), + } + .into() + }) + }) + .collect() +} +// Get all feeds from URLs concurrently. +// +// Skips feeds if there are errors. Shows progress. +fn get_feeds_from_urls(urls: Vec) -> Result> { let agent: Agent = AgentBuilder::new() .timeout(Duration::from_secs(10)) .user_agent(concat!(crate_name!(), '/', crate_version!())) @@ -111,7 +160,7 @@ pub fn run(args: Args) -> Result<()> { let m = MultiProgress::new(); - let feeds: Vec = urls + let feeds: Vec<(Feed, Url)> = urls .par_iter() .enumerate() .filter_map(|(idx, url)| { @@ -122,7 +171,7 @@ pub fn run(args: Args) -> Result<()> { let body = match agent.get(url.as_str()).call() { Ok(r) => r.into_string().ok(), Err(e) => { - warn!("Failed to get feed `{}`\n\nCaused By:\n{}", url.as_str(), e); + warn!(url=%url.as_str(), error=%e, "Failed to get feed"); None } }; @@ -132,13 +181,13 @@ pub fn run(args: Args) -> Result<()> { match feed_str.parse::() { Ok(feed) => { pb.finish_and_clear(); - Some(feed) + Some((feed, url.clone())) } Err(e) => { warn!( - "Failed to parse RSS/Atom feed from `{}`\n\nCaused By:\n{}", - url.as_str(), - e + url=%url.as_str(), + error=%e, + "Failed to parse RSS/Atom feed." ); pb.finish_with_message(format!( "Failed to parse feed from `{}`", @@ -154,9 +203,46 @@ pub fn run(args: Args) -> Result<()> { }) .collect(); m.clear()?; + Ok(feeds) +} + +// Parse the date, falling back to naive parsing if necessary. +fn parse_date(date: &str) -> Result> { + date.parse::>() + .or_else(|_| DateTime::parse_from_rfc2822(date)) + .or_else(|_| DateTime::parse_from_rfc3339(date)) + .or_else(|_| { + debug!(?date, "attempting to parse non-standard date"); + let naive_dt = NaiveDateTime::parse_from_str(date, "%Y-%m-%dT%H:%M:%S")?; + let fixed_offset = + FixedOffset::east_opt(Local::now().offset().local_minus_utc()).unwrap(); + fixed_offset + .from_local_datetime(&naive_dt) + .earliest() + .ok_or(OpenringError::DateError) + }) +} + +pub fn run(args: Args) -> Result<()> { + debug!(?args); + let mut urls = args.urls; + + if let Some(path) = args.url_file { + let mut file_urls = parse_urls_from_file(path)?; + urls.append(&mut file_urls); + }; + + if urls.is_empty() { + return Err(OpenringError::FeedMissing); + } + + let feeds = get_feeds_from_urls(urls)?; + + let template = fs::read_to_string(&args.template_file)?; + let mut context = tera::Context::new(); let mut articles = Vec::new(); - for feed in feeds { + for (feed, url) in feeds { match feed { Feed::RSS(c) => { let items = if c.items().len() >= args.per_source { @@ -164,29 +250,13 @@ pub fn run(args: Args) -> Result<()> { } else { c.items() }; - let source_link = Url::parse(c.link()) - .with_context(|| format!("Failed to parse url `{}`", c.link()))?; + let source_link = Url::parse(c.link())?; let source_title = c.title().to_string(); for item in items { if let (Some(link), Some(title), Some(date)) = (item.link(), item.title(), item.pub_date()) { - let date = date - .parse::>() - .or_else(|_| DateTime::parse_from_rfc2822(date)) - .or_else(|_| { - let naive_dt = - NaiveDateTime::parse_from_str(date, "%Y-%m-%dT%H:%M:%S")?; - let fixed_offset = - FixedOffset::east_opt(Local::now().offset().local_minus_utc()) - .unwrap(); - fixed_offset - .from_local_datetime(&naive_dt) - .earliest() - .ok_or(anyhow!("Failed to parse naive datetime.")) - }) - .with_context(|| format!("Failed to parse date `{}`", date))?; - + let date = parse_date(date)?; // Skip articles after args.before, if present if let Some(before) = args.before { if date.date_naive() > before { @@ -199,7 +269,7 @@ pub fn run(args: Args) -> Result<()> { None => match item.content() { Some(s) => s.to_string(), None => { - warn!("Skipping `{}` from `{}`, no summary or content provided in feed.", link, source_link); + warn!(?link, ?source_link, "Skipping link from feed: no summary or content provided in feed."); continue; } }, @@ -210,8 +280,7 @@ pub fn run(args: Args) -> Result<()> { &mut safe_summary, ); articles.push(Article { - link: Url::parse(link) - .with_context(|| format!("Failed to parse url `{}`", c.link()))?, + link: Url::parse(link)?, title: title.to_string(), summary: safe_summary.trim().to_string(), source_link: source_link.clone(), @@ -219,15 +288,15 @@ pub fn run(args: Args) -> Result<()> { date, }); } else { - debug!("Skipping `{:#?}`, must have link, title, and date", item); + debug!(?item, "Skipping. Must have link, title, and date"); } } } - Feed::Atom(f) => { + Feed::Atom(ref f) => { let items = &f.entries()[0..args.per_source]; let feed_links = f.links(); if !feed_links.is_empty() { - trace!("Feed links: {:#?}", feed_links); + debug!(?feed_links); let source_link = Url::parse( f.links() @@ -235,60 +304,28 @@ pub fn run(args: Args) -> Result<()> { .find(|l| l.rel() == "alternate") .unwrap() .href(), - ) - .with_context(|| format!("Failed to parse url `{}`", f.links()[0].href()))?; + )?; let source_title = f.title().to_string(); for item in items { if !item.links().is_empty() { - let date = item - .updated() - .parse::>() - .or_else(|_| DateTime::parse_from_rfc2822(item.updated())) - .or_else(|_| { - let naive_dt = NaiveDateTime::parse_from_str( - item.updated(), - "%Y-%m-%dT%H:%M:%S", - )?; - let fixed_offset = FixedOffset::east_opt( - Local::now().offset().local_minus_utc(), - ) - .unwrap(); - fixed_offset - .from_local_datetime(&naive_dt) - .earliest() - .ok_or(anyhow!("Failed to parse naive datetime.")) - }) - .or_else(|_| { - debug!("Using published date, rather than last updated date."); - if let Some(date) = item.published() { - date.parse::>() - .or_else(|_| DateTime::parse_from_rfc2822(date)) - .or_else(|_| { - let naive_dt = NaiveDateTime::parse_from_str( - date, - "%Y-%m-%dT%H:%M:%S", - )?; - let fixed_offset = FixedOffset::east_opt( - Local::now().offset().local_minus_utc(), - ) - .unwrap(); - fixed_offset - .from_local_datetime(&naive_dt) - .earliest() - .ok_or(anyhow!( - "Failed to parse naive datetime." - )) - }) - .with_context(|| { - format!("Failed to parse date `{}`", date) - }) - } else { - Err(OpenringError::DateError.into()) - } - }) - .with_context(|| { - format!("Failed to parse date `{}`", item.updated()) - })?; + let date = parse_date(item.updated()).or_else(|_| { + debug!("using published date, rather than last updated date"); + if let Some(date) = item.published() { + parse_date(date).map_err(|e| { + let feed_src = feed.to_string(); + let start = feed_src.find(date).unwrap(); + let len = date.len(); + ChronoError { + src: NamedSource::new(url.as_str(), feed_src), + span: (start..start + len).into(), + help: e.to_string(), + } + .into() + }) + } else { + Err(OpenringError::DateError) + } + })?; // Skip articles after args.before, if present if let Some(before) = args.before { @@ -302,7 +339,7 @@ pub fn run(args: Args) -> Result<()> { None => match item.content().map(|c| c.value()) { Some(Some(v)) => v.to_string(), _ => { - warn!("Skipping `{}` from `{}`, no summary or content provided in feed.", item.links()[0].href(), source_link); + warn!(link=%item.links()[0].href(), ?source_link, "Skipping link from feed: no summary or content provided in feed."); continue; } }, @@ -313,7 +350,7 @@ pub fn run(args: Args) -> Result<()> { ammonia::clean(&summary), &mut safe_summary, ); - info!("{:#?}", safe_summary); + info!(summary = %summary); // Uses the last link, since blogspot puts the article link last. let link = Url::parse( item.links() @@ -321,10 +358,7 @@ pub fn run(args: Args) -> Result<()> { .find(|l| l.rel() == "alternate") .unwrap() .href(), - ) - .with_context(|| { - format!("Failed to parse url `{}`", f.links()[0].href()) - })?; + )?; articles.push(Article { link, title: item.title().to_string(), @@ -334,7 +368,7 @@ pub fn run(args: Args) -> Result<()> { date, }); } else { - debug!("Skipping `{:#?}`, must have links", item); + debug!(?item, "Skipping. Must have links."); } } } @@ -351,8 +385,7 @@ pub fn run(args: Args) -> Result<()> { context.insert("articles", articles); // TODO: this validation of the template should come before all the time spent fetching feeds. - let output = Tera::one_off(&template, &context, true) - .with_context(|| format!("Failed to parse Tera template:\n{}", template))?; + let output = Tera::one_off(&template, &context, true)?; println!("{output}"); Ok(()) } diff --git a/src/main.rs b/src/main.rs index f609b46..56fcbfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ -use anyhow::Result; - use clap::Parser; +use miette::{Context, Result}; use openring::{self, Args}; @@ -8,19 +7,22 @@ fn main() -> Result<()> { let args = Args::parse(); tracing_subscriber::fmt() - .with_env_filter(convert_filter(args.verbose.log_level_filter())) + .with_env_filter(format!( + "openring={},html5ever=off,ureq=off", + convert_filter(args.verbose.log_level_filter()) + )) .init(); - openring::run(args) + // I feel like I shouldn't need wrap_err, but it doesn't work without it. + openring::run(args).wrap_err("runtime error") } -fn convert_filter(filter: log::LevelFilter) -> String { - let filter = match filter { - log::LevelFilter::Off => "off", - log::LevelFilter::Error => "error", - log::LevelFilter::Warn => "warn", - log::LevelFilter::Info => "info", - log::LevelFilter::Debug => "debug", - log::LevelFilter::Trace => "trace", - }; - format!("openring={},html5ever=off,ureq=off", filter) +fn convert_filter(filter: log::LevelFilter) -> tracing_subscriber::filter::LevelFilter { + match filter { + log::LevelFilter::Off => tracing_subscriber::filter::LevelFilter::OFF, + log::LevelFilter::Error => tracing_subscriber::filter::LevelFilter::ERROR, + log::LevelFilter::Warn => tracing_subscriber::filter::LevelFilter::WARN, + log::LevelFilter::Info => tracing_subscriber::filter::LevelFilter::INFO, + log::LevelFilter::Debug => tracing_subscriber::filter::LevelFilter::DEBUG, + log::LevelFilter::Trace => tracing_subscriber::filter::LevelFilter::TRACE, + } } From 4f5cef57251a8e89e8e1f91e959fb861292c7934 Mon Sep 17 00:00:00 2001 From: Luke Hsiao Date: Thu, 10 Aug 2023 23:21:24 -0600 Subject: [PATCH 2/2] build(Justfile): add `link-check` target Signed-off-by: Luke Hsiao --- Justfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Justfile b/Justfile index 3d96015..00e2c92 100644 --- a/Justfile +++ b/Justfile @@ -7,6 +7,10 @@ _default: check: cargo clippy --locked -- -D warnings +# Check links in markdown files +link-check: + -lychee -E '**/*.md' + # Runs nextest test: cargo nextest run