cjkfmt 0.0.9

Formatter for CJK text files
use std::{
    fs,
    io::{Read, stdin},
    path::Path,
};

use crate::language::Language;

use crate::{
    check::check_one_file, cli::utils::format_diagnostic, config::Config, document::Document,
};

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum CheckOutcome {
    Clean,
    DiagnosticsFound,
}

pub fn check_command<W, P>(
    stdout: &mut W,
    config: &Config,
    filenames: &[P],
    language: Option<Language>,
) -> anyhow::Result<CheckOutcome>
where
    W: std::io::Write,
    P: AsRef<Path>,
{
    let mut stdin = stdin();
    check_command_with_reader(stdout, config, filenames, language, &mut stdin)
}

fn check_command_with_reader<W, P, R>(
    stdout: &mut W,
    config: &Config,
    filenames: &[P],
    language: Option<Language>,
    stdin: &mut R,
) -> anyhow::Result<CheckOutcome>
where
    W: std::io::Write,
    P: AsRef<Path>,
    R: Read,
{
    let mut diagnostics = Vec::new();

    if filenames.is_empty() {
        let mut content = String::with_capacity(1024);
        stdin.read_to_string(&mut content)?;
        let language = language.unwrap_or(Language::Markdown);
        let document = Document::new(content, Some(language), None);
        diagnostics.extend(check_one_file(config, &document)?);
    } else {
        for filename in filenames {
            let filename = filename.as_ref();
            let language =
                Language::explicit_or_inferred_path(language, filename).ok_or_else(|| {
                    anyhow::anyhow!(
                        "could not infer the language for {}; specify it with --language",
                        filename.display()
                    )
                })?;
            let content = fs::read_to_string(filename)?;
            let document = Document::new(
                content,
                Some(language),
                Some(filename.to_string_lossy().to_string()),
            );
            diagnostics.extend(check_one_file(config, &document)?);
        }
    }
    let outcome = if diagnostics.is_empty() {
        CheckOutcome::Clean
    } else {
        CheckOutcome::DiagnosticsFound
    };
    for diagnostic in diagnostics {
        writeln!(stdout, "{}", format_diagnostic(&diagnostic))?;
    }
    Ok(outcome)
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use tempfile::tempdir;

    use super::*;
    use crate::{config::SpacingRule, language::Language};

    #[test]
    fn check_command_selects_json_for_an_uppercase_json_file() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("document.JSON");
        fs::write(&path, "{\"value\":\"漢A\"}\n").unwrap();

        let mut config = Config {
            max_width: 200,
            ..Config::default()
        };
        config.spacing.alphabets = SpacingRule::Require;

        let mut output = Vec::new();
        check_command(&mut output, &config, &[&path], None).unwrap();

        assert!(!String::from_utf8(output).unwrap().contains("W002"));
    }

    #[test]
    fn check_command_selects_markdown_for_mixed_case_markdown_file() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("document.MarkDown");
        fs::write(&path, "漢A\n").unwrap();

        let mut config = Config {
            max_width: 200,
            ..Config::default()
        };
        config.spacing.alphabets = SpacingRule::Require;

        let mut output = Vec::new();
        check_command(&mut output, &config, &[&path], None).unwrap();

        assert!(String::from_utf8(output).unwrap().contains("W002"));
    }

    #[test]
    fn check_command_rejects_unrecognized_named_files_without_an_override() {
        let directory = tempdir().unwrap();
        for filename in ["document.txt", "document"] {
            let path = directory.path().join(filename);
            fs::write(&path, "漢A\n").unwrap();

            let error = check_command(&mut Vec::new(), &Config::default(), &[&path], None)
                .expect_err("unsupported named files should require an explicit language");
            let message = error.to_string();
            assert!(message.contains(path.to_string_lossy().as_ref()));
            assert!(message.contains("specify it with --language"));
        }
    }

    #[test]
    fn check_command_language_override_applies_markdown_to_every_file() {
        let directory = tempdir().unwrap();
        let json_path = directory.path().join("document.json");
        let markdown_path = directory.path().join("document.md");
        let unknown_path = directory.path().join("document.txt");
        fs::write(&json_path, "漢A\n").unwrap();
        fs::write(&markdown_path, "漢A\n").unwrap();
        fs::write(&unknown_path, "漢A\n").unwrap();

        let mut config = Config {
            max_width: 200,
            ..Config::default()
        };
        config.spacing.alphabets = SpacingRule::Require;
        let mut output = Vec::new();
        check_command(
            &mut output,
            &config,
            &[&json_path, &markdown_path, &unknown_path],
            Some(Language::Markdown),
        )
        .unwrap();

        let output = String::from_utf8(output).unwrap();
        assert_eq!(output.matches("W002").count(), 3);
        assert!(output.contains(json_path.to_string_lossy().as_ref()));
        assert!(output.contains(markdown_path.to_string_lossy().as_ref()));
        assert!(output.contains(unknown_path.to_string_lossy().as_ref()));
    }

    #[test]
    fn check_command_defaults_stdin_to_markdown() {
        let mut config = Config {
            max_width: 200,
            ..Config::default()
        };
        config.spacing.alphabets = SpacingRule::Require;
        let mut input = "漢A\n".as_bytes();
        let mut output = Vec::new();

        let outcome =
            check_command_with_reader(&mut output, &config, &[] as &[PathBuf], None, &mut input)
                .unwrap();

        assert_eq!(outcome, CheckOutcome::DiagnosticsFound);
        assert!(String::from_utf8(output).unwrap().contains("W002"));
    }

    #[test]
    fn check_command_reports_clean_when_stdin_has_no_diagnostics() {
        let mut input = "abc\n".as_bytes();
        let mut output = Vec::new();
        let config = Config {
            max_width: 3,
            ..Config::default()
        };

        let outcome =
            check_command_with_reader(&mut output, &config, &[] as &[PathBuf], None, &mut input)
                .unwrap();

        assert_eq!(outcome, CheckOutcome::Clean);
        assert!(output.is_empty());
    }

    #[test]
    fn check_command_language_json_overrides_markdown_path_without_spacing_diagnostic() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("document.md");
        fs::write(&path, "{\"value\":\"漢A\"}\n").unwrap();

        let mut config = Config {
            max_width: 200,
            ..Config::default()
        };
        config.spacing.alphabets = SpacingRule::Require;
        let mut output = Vec::new();
        check_command(&mut output, &config, &[&path], Some(Language::Json)).unwrap();

        assert!(!String::from_utf8(output).unwrap().contains("W002"));
    }

    #[test]
    fn check_command_language_json_suppresses_spacing_but_keeps_line_length() {
        let mut config = Config {
            max_width: 5,
            ..Config::default()
        };
        config.spacing.alphabets = SpacingRule::Require;
        let mut input = "{\"value\":\"漢A\"}\n".as_bytes();
        let mut output = Vec::new();
        check_command_with_reader(
            &mut output,
            &config,
            &[] as &[PathBuf],
            Some(Language::Json),
            &mut input,
        )
        .unwrap();

        let output = String::from_utf8(output).unwrap();
        assert!(output.contains("W001"));
        assert!(!output.contains("W002"));
    }
}