notabene 0.2.0

A fast linter for changelogs in Keep a Changelog format
Documentation
use std::path::PathBuf;

use clap::builder::{Styles, ValueParser};
use clap::{Arg, ArgGroup, Command, value_parser};

use crate::rule::{RULES_BY_CODE, Rule};

mod commands;
mod config;
mod error;
mod report;

pub fn main() -> error::Result<()> {
    let matches = Command::new("nb")
        .arg_required_else_help(true)
        .subcommand_required(true)
        .styles(Styles::plain())
        .subcommand(
            Command::new("lint")
                .alias("check")
                .about("Lint a changelog")
                .arg(Arg::new("FILE").value_parser(value_parser!(PathBuf)))
                .arg(
                    Arg::new("config_file")
                        .long("config-file")
                        .short('c')
                        .value_parser(value_parser!(PathBuf)),
                )
                .arg(
                    Arg::new("output_format")
                        .long("output-format")
                        .value_parser(ValueParser::new(parse_output_format))
                        .default_value("full"),
                )
                .arg(
                    Arg::new("select")
                        .long("select")
                        .value_parser(ValueParser::new(parse_rule_code))
                        .value_delimiter(','),
                )
                .arg(
                    Arg::new("ignore")
                        .long("ignore")
                        .value_parser(ValueParser::new(parse_rule_code))
                        .value_delimiter(','),
                ),
        )
        .subcommand(
            Command::new("rule")
                .about("Explain a rule")
                .group(ArgGroup::new("rule").required(true).args(["RULE", "all"]))
                .arg(
                    Arg::new("RULE")
                        .help("The rule code (e.g., E100)")
                        .value_parser(ValueParser::new(parse_rule_code))
                        .conflicts_with("all"),
                )
                .arg(
                    Arg::new("all")
                        .short('a')
                        .long("all")
                        .help("Explain all rules")
                        .action(clap::ArgAction::SetTrue),
                ),
        )
        .get_matches();
    match matches.subcommand() {
        Some(("lint", submatches)) => commands::lint(submatches),
        Some(("rule", submatches)) => commands::rule(submatches),
        _ => unreachable!(),
    }
}

fn parse_rule_code(code: &str) -> Result<Rule, String> {
    RULES_BY_CODE
        .get(&code.to_uppercase())
        .ok_or(
            Rule::ALL
                .iter()
                .map(|rule| rule.code())
                .collect::<Vec<&str>>()
                .join(", ")
                .to_string(),
        )
        .copied()
}

fn parse_output_format(format: &str) -> Result<report::Format, String> {
    use report::Format::*;
    match format.to_lowercase().as_str() {
        "full" => Ok(Full),
        "json" => Ok(Json),
        "jsonl" => Ok(JsonLines),
        "short" => Ok(Short),
        _ => Err("full, json, jsonl, short".to_string()),
    }
}