use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Parser)]
#[command(name = "sitevet", version, about = "Audit a website for SEO, security, accessibility and performance.")]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
#[arg(long, global = true)]
pub plain: bool,
#[arg(long, short, global = true)]
pub verbose: bool,
}
#[derive(Subcommand)]
pub enum Command {
Audit(Box<AuditArgs>),
Rules(RulesArgs),
Init(InitArgs),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReportFormat {
Json,
Md,
Html,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FailOn {
Error,
Warning,
}
#[derive(Args, Default)]
pub struct AuditArgs {
pub url: String,
#[arg(long, value_enum)]
pub format: Option<ReportFormat>,
#[arg(long, short)]
pub out: Option<PathBuf>,
#[arg(long)]
pub config: Option<PathBuf>,
#[arg(long, value_parser = clap::value_parser!(u8).range(0..=100))]
pub fail_under: Option<u8>,
#[arg(long, value_enum)]
pub fail_on: Option<FailOn>,
#[arg(long, short)]
pub quiet: bool,
#[arg(long, value_delimiter = ',', help_heading = "Audit options")]
pub only: Option<Vec<String>>,
#[arg(long, value_delimiter = ',', help_heading = "Audit options")]
pub skip: Option<Vec<String>>,
#[arg(long, help_heading = "Audit options")]
pub max_pages: Option<u32>,
#[arg(long, help_heading = "Audit options")]
pub max_depth: Option<u32>,
#[arg(long, help_heading = "Audit options")]
pub concurrency: Option<u32>,
#[arg(long, help_heading = "Audit options")]
pub delay_ms: Option<u64>,
#[arg(long, help_heading = "Audit options")]
pub timeout_ms: Option<u64>,
#[arg(long, help_heading = "Audit options")]
pub user_agent: Option<String>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true", help_heading = "Audit options")]
pub ignore_robots: Option<bool>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true", help_heading = "Audit options")]
pub crawl_sitemap: Option<bool>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true", help_heading = "Audit options")]
pub allow_private_network: Option<bool>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true", help_heading = "Audit options")]
pub browser: Option<bool>,
#[arg(long, env = "SITEVET_CHROME", help_heading = "Audit options")]
pub chrome_path: Option<PathBuf>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true", help_heading = "Audit options")]
pub render_crawl: Option<bool>,
#[arg(long, env = "SITEVET_PSI_KEY", hide_env_values = true, help_heading = "Audit options")]
pub psi_key: Option<String>,
#[arg(long, help_heading = "Audit options")]
pub psi_max_pages: Option<u32>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true", help_heading = "Audit options")]
pub media: Option<bool>,
#[arg(long, help_heading = "Audit options")]
pub media_max_requests: Option<u32>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true", help_heading = "Audit options")]
pub screenshots: Option<bool>,
#[arg(long, help_heading = "Audit options")]
pub screenshot_dir: Option<PathBuf>,
#[arg(long, help_heading = "Audit options")]
pub click_depth_warn: Option<u32>,
#[arg(long, help_heading = "Audit options")]
pub slow_resource_ms: Option<u64>,
#[arg(long, help_heading = "Audit options")]
pub runtime_threads: Option<usize>,
}
impl AuditArgs {
pub fn apply(&self, o: &mut sitevet::Options) {
macro_rules! set {
($($field:ident),*) => { $( if let Some(v) = &self.$field { o.$field = v.clone(); } )* };
}
macro_rules! set_some {
($($field:ident),*) => { $( if let Some(v) = &self.$field { o.$field = Some(v.clone()); } )* };
}
set!(
only,
skip,
max_pages,
max_depth,
concurrency,
delay_ms,
timeout_ms,
ignore_robots,
crawl_sitemap,
browser
);
set!(render_crawl, psi_max_pages, media, media_max_requests, screenshots, click_depth_warn, slow_resource_ms);
set!(runtime_threads);
set_some!(user_agent, allow_private_network, chrome_path, psi_key, screenshot_dir);
}
}
#[derive(Args)]
pub struct RulesArgs {
#[arg(long)]
pub category: Option<String>,
#[arg(long, value_enum, default_value = "table")]
pub format: ListFormat,
}
#[derive(Clone, Copy, ValueEnum)]
pub enum ListFormat {
Table,
Md,
Json,
}
#[derive(Args)]
pub struct InitArgs {
#[arg(long)]
pub force: bool,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::{Cli, Command};
#[test]
fn flags_and_toml_set_the_same_options() {
let toml = r#"
only = ["security", "links"]
skip = ["legal"]
max_pages = 7
max_depth = 2
concurrency = 3
delay_ms = 250
timeout_ms = 900
user_agent = "bot/1"
ignore_robots = true
crawl_sitemap = false
allow_private_network = true
browser = true
chrome_path = "/opt/chrome"
render_crawl = true
psi_key = "k"
psi_max_pages = 9
media = true
media_max_requests = 50
screenshots = true
screenshot_dir = "shots"
click_depth_warn = 4
slow_resource_ms = 500
runtime_threads = 4
"#;
let (from_toml, _) = crate::config::parse(toml).unwrap();
let cli = Cli::try_parse_from([
"sitevet",
"audit",
"https://example.com",
"--only",
"security,links",
"--skip",
"legal",
"--max-pages",
"7",
"--max-depth",
"2",
"--concurrency",
"3",
"--delay-ms",
"250",
"--timeout-ms",
"900",
"--user-agent",
"bot/1",
"--ignore-robots",
"--crawl-sitemap=false",
"--allow-private-network",
"--browser",
"--chrome-path",
"/opt/chrome",
"--render-crawl",
"--psi-key",
"k",
"--psi-max-pages",
"9",
"--media",
"--media-max-requests",
"50",
"--screenshots",
"--screenshot-dir",
"shots",
"--click-depth-warn",
"4",
"--slow-resource-ms",
"500",
"--runtime-threads",
"4",
])
.unwrap();
let Command::Audit(args) = cli.command else { panic!("not audit") };
let mut from_flags = sitevet::Options::default();
args.apply(&mut from_flags);
let json = |o: &sitevet::Options| serde_json::to_value(o).unwrap();
assert_eq!(json(&from_flags), json(&from_toml));
assert_eq!(from_flags.psi_key, from_toml.psi_key, "psi_key is never serialised, so compare it directly");
assert_ne!(json(&from_flags), json(&sitevet::Options::default()));
}
#[test]
fn flags_override_the_file() {
let (mut options, _) = crate::config::parse("max_pages = 7\nbrowser = true").unwrap();
let cli =
Cli::try_parse_from(["sitevet", "audit", "https://e.com", "--max-pages", "3", "--browser=false"]).unwrap();
let Command::Audit(args) = cli.command else { panic!("not audit") };
args.apply(&mut options);
assert_eq!((options.max_pages, options.browser), (3, false));
}
}