use anyhow::{bail, Result};
use regex::Regex;
use std::path::PathBuf;
use tokio::sync::mpsc;
use tokio::task;
use clap::{AppSettings, Clap};
#[derive(Clap)]
#[clap(version = "0.3.0", author = "k-nasa")]
#[clap(setting = AppSettings::ColoredHelp)]
struct Opts {
files: Vec<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
let opts: Opts = Opts::parse();
if opts.files.len() < 2 {
println!("Usage example: lc README.md Makefile src/");
std::process::exit(1);
}
for filepath in args_to_filepaths(&opts.files) {
println!("\x1b[01;36m=== Verify {:?} === \x1b[m", filepath);
let text = match tokio::fs::read_to_string(&filepath).await {
Err(e) => {
println!("\x1b[01;31mError verify {:?}: \x1b[m{}", filepath, e);
continue;
}
Ok(text) => text,
};
let links = find_link(&text);
let (mut tx, mut rx) = mpsc::channel(10);
tokio::spawn(async move {
for link in links {
let handler = task::spawn(async move { verify_link(link).await });
match tx.send(handler).await {
Err(e) => println!("\x1b[01;31mErr \x1b[m internal error: {}", e),
Ok(_) => (),
};
}
});
while let Some(f) = rx.recv().await {
let result = f.await.unwrap();
match result {
Err(e) => println!("\x1b[01;31mErr \x1b[m {}", e),
Ok(v) => println!("\x1b[01;32mOk\x1b[m {}", v),
}
}
println!("");
}
Ok(())
}
fn args_to_filepaths(files: &[String]) -> Vec<PathBuf> {
let mut filepaths = vec![];
for filepath in files {
let path = std::path::PathBuf::from(filepath);
filepaths.append(&mut walk_dir(&path));
}
filepaths
}
fn walk_dir(path_buf: &PathBuf) -> Vec<PathBuf> {
let mut filepaths = vec![];
if path_buf.is_dir() {
for entry in std::fs::read_dir(path_buf).unwrap() {
let path = entry.unwrap().path();
filepaths.append(&mut walk_dir(&path));
}
} else {
filepaths.push(path_buf.to_path_buf());
}
filepaths
}
fn find_link(text: &str) -> Vec<String> {
let r = Regex::new(r"https?://[\w!?/\+\-_~=;\.,*&@#$%]+").unwrap();
r.find_iter(text).map(|m| m.as_str().to_string()).collect()
}
async fn verify_link(link: String) -> Result<String> {
let res = reqwest::get(&link).await?;
let status_code = res.status();
if !status_code.is_success() {
bail!("{} -> status code {}", link, status_code);
}
Ok(link.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
static DUMMY_TEXT: &str = r###"
# lc
## Overview
[](https://github.com/k-nasa/lc/actions)
[](https://crates.io/crates/lc)
Markdown link checker"###;
#[test]
fn test_find_link() {
let links = find_link(DUMMY_TEXT);
assert_eq!(
links,
vec![
"https://github.com/k-nasa/lc/workflows/CI/badge.svg",
"https://github.com/k-nasa/lc/actions",
"https://img.shields.io/crates/v/lc.svg",
"https://crates.io/crates/lc",
]
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
)
}
}