mandate 0.1.0

Convert Markdown or YAML manuals into roff manpages
Documentation
#![forbid(unsafe_code)]

use clap::Parser;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;

#[derive(Debug, Parser)]
#[command(name = "mandate", version)]
struct Cli {
    #[arg(short = 'i', long = "input", value_name = "PATH", default_value = "-")]
    input: String,

    #[arg(short = 'p', long = "program", value_name = "NAME")]
    program: String,

    #[arg(
        short = 's',
        long = "section",
        value_name = "SECTION",
        default_value = "1"
    )]
    section: String,

    #[arg(short = 't', long = "title", value_name = "TITLE")]
    title: String,

    #[arg(short = 'm', long = "manual-section", value_name = "MANUAL")]
    manual_section: Option<String>,

    #[arg(long = "source", value_name = "SOURCE")]
    source: Option<String>,

    #[arg(short = 'o', long = "output", value_name = "PATH")]
    output: Option<PathBuf>,

    #[arg(long = "validate")]
    validate: bool,

    #[arg(long = "schema", value_name = "PATH")]
    schema: Option<PathBuf>,
}

fn read_input(path: &str) -> io::Result<String> {
    if path == "-" {
        let mut buf = String::new();
        io::stdin().read_to_string(&mut buf)?;
        Ok(buf)
    } else {
        fs::read_to_string(path)
    }
}

fn write_output(path: Option<PathBuf>, contents: &str) -> io::Result<()> {
    match path {
        Some(path) => fs::write(path, contents),
        None => {
            print!("{contents}");
            Ok(())
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    let input = read_input(&cli.input)?;

    let options = mandate::ManpageOptions::new(
        cli.program,
        cli.section,
        cli.title,
        cli.manual_section,
        cli.source,
    );

    let output = match input_kind(&cli.input) {
        InputKind::Yaml => {
            if cli.validate {
                validate_yaml(&input, cli.schema.as_ref())?;
            }
            mandate::convert_yaml_to_roff(&input, &options)?
        }
        InputKind::Markdown => mandate::convert_markdown_to_roff(&input, &options)?,
        InputKind::Auto => {
            if cli.validate {
                match validate_yaml(&input, cli.schema.as_ref()) {
                    Ok(()) => mandate::convert_yaml_to_roff(&input, &options)?,
                    Err(mandate::MandateError::Yaml(_)) => {
                        mandate::convert_markdown_to_roff(&input, &options)?
                    }
                    Err(err) => return Err(Box::new(err)),
                }
            } else {
                mandate::convert_yaml_to_roff(&input, &options)
                    .or_else(|_| mandate::convert_markdown_to_roff(&input, &options))?
            }
        }
    };
    write_output(cli.output, &output)?;
    Ok(())
}

#[derive(Debug, Clone, Copy)]
enum InputKind {
    Yaml,
    Markdown,
    Auto,
}

fn input_kind(path: &str) -> InputKind {
    let lower = path.to_ascii_lowercase();
    if lower == "-" {
        return InputKind::Auto;
    }
    if lower.ends_with(".yaml") || lower.ends_with(".yml") {
        return InputKind::Yaml;
    }
    if lower.ends_with(".md") || lower.ends_with(".markdown") {
        return InputKind::Markdown;
    }
    InputKind::Markdown
}

fn validate_yaml(input: &str, schema: Option<&PathBuf>) -> Result<(), mandate::MandateError> {
    match schema {
        Some(path) => mandate::validate_yaml_with_schema(input, path),
        None => mandate::validate_yaml_with_schema_str(input, mandate::BUILTIN_SCHEMA),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn input_kind_detects_extensions() {
        assert!(matches!(input_kind("-"), InputKind::Auto));
        assert!(matches!(input_kind("manual.yaml"), InputKind::Yaml));
        assert!(matches!(input_kind("manual.yml"), InputKind::Yaml));
        assert!(matches!(input_kind("manual.md"), InputKind::Markdown));
        assert!(matches!(input_kind("manual.markdown"), InputKind::Markdown));
        assert!(matches!(input_kind("manual.txt"), InputKind::Markdown));
    }
}