acr-cli 0.6.0

A CLI tool for AtCoder competitive programming in Rust
use anyhow::Context;

use crate::atcoder;
use crate::browser;
use crate::workspace;
use crate::workspace::CurrentContext;

fn resolve_url(current: CurrentContext, args: &[String]) -> anyhow::Result<String> {
    match current {
        CurrentContext::ProblemDir(ctx) => {
            if !args.is_empty() {
                anyhow::bail!(
                    "Cannot specify arguments from a problem directory. Move to the contest directory."
                );
            }
            Ok(ctx.problem_url)
        }
        CurrentContext::ContestDir(ctx) => match args.first().map(|s| s.as_str()) {
            Some(p) => {
                let problem_ctx =
                    workspace::detect_problem_dir_from(&ctx.contest_dir.join(p.to_lowercase()))
                        .with_context(|| format!("Problem '{}' not found", p))?;
                Ok(problem_ctx.problem_url)
            }
            None => Ok(format!(
                "{}/contests/{}/tasks",
                atcoder::BASE_URL,
                ctx.contest_id
            )),
        },
        CurrentContext::Outside => {
            if args.is_empty() {
                anyhow::bail!("Specify a contest ID, or run from a contest directory.");
            }
            let contest_id = &args[0];
            match args.get(1) {
                Some(problem) => Ok(format!(
                    "{}/contests/{}/tasks/{}_{}",
                    atcoder::BASE_URL,
                    contest_id,
                    contest_id,
                    problem.to_lowercase()
                )),
                None => Ok(format!(
                    "{}/contests/{}/tasks",
                    atcoder::BASE_URL,
                    contest_id
                )),
            }
        }
    }
}

pub fn execute(args: Vec<String>) -> anyhow::Result<()> {
    let current = workspace::detect_current_context();
    let url = resolve_url(current, &args)?;
    browser::open(&url);
    println!("{}", url);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::workspace::{ContestContext, ProblemContext};
    use std::path::PathBuf;

    fn make_problem_context(contest_id: &str, problem: &str) -> ProblemContext {
        ProblemContext {
            contest_id: contest_id.to_string(),
            problem_alphabet: problem.to_string(),
            task_screen_name: format!("{}_{}", contest_id, problem),
            problem_dir: PathBuf::from(format!("/tmp/{}/{}", contest_id, problem)),
            problem_url: format!(
                "https://atcoder.jp/contests/{}/tasks/{}_{}",
                contest_id, contest_id, problem
            ),
        }
    }

    #[test]
    fn test_resolve_url_problem_dir_no_args() {
        let ctx = CurrentContext::ProblemDir(make_problem_context("abc001", "a"));
        let url = resolve_url(ctx, &[]).unwrap();
        assert_eq!(url, "https://atcoder.jp/contests/abc001/tasks/abc001_a");
    }

    #[test]
    fn test_resolve_url_problem_dir_with_args_error() {
        let ctx = CurrentContext::ProblemDir(make_problem_context("abc001", "a"));
        assert!(resolve_url(ctx, &["b".to_string()]).is_err());
    }

    #[test]
    fn test_resolve_url_contest_dir_no_args() {
        let dir = tempfile::tempdir().unwrap();
        let ws = dir.path().join("abc001");
        std::fs::create_dir_all(&ws).unwrap();
        std::fs::write(
            ws.join("Cargo.toml"),
            "[workspace]\nmembers = [\"a\"]\nresolver = \"2\"\n",
        )
        .unwrap();
        let ctx = CurrentContext::ContestDir(ContestContext {
            contest_id: "abc001".to_string(),
            contest_dir: ws,
        });
        let url = resolve_url(ctx, &[]).unwrap();
        assert_eq!(url, "https://atcoder.jp/contests/abc001/tasks");
    }

    #[test]
    fn test_resolve_url_contest_dir_with_problem() {
        let dir = tempfile::tempdir().unwrap();
        let ws = dir.path().join("abc001");
        let problem_dir = ws.join("a");
        std::fs::create_dir_all(&problem_dir).unwrap();
        std::fs::write(
            ws.join("Cargo.toml"),
            "[workspace]\nmembers = [\"a\"]\nresolver = \"2\"\n",
        )
        .unwrap();
        std::fs::write(
            problem_dir.join("Cargo.toml"),
            r#"[package]
name = "abc001-a"
version = "0.1.0"
edition = "2021"

[package.metadata.acr]
problem_url = "https://atcoder.jp/contests/abc001/tasks/abc001_a"
"#,
        )
        .unwrap();
        let ctx = CurrentContext::ContestDir(ContestContext {
            contest_id: "abc001".to_string(),
            contest_dir: ws,
        });
        let url = resolve_url(ctx, &["a".to_string()]).unwrap();
        assert_eq!(url, "https://atcoder.jp/contests/abc001/tasks/abc001_a");
    }

    #[test]
    fn test_resolve_url_outside_contest_only() {
        let ctx = CurrentContext::Outside;
        let url = resolve_url(ctx, &["abc001".to_string()]).unwrap();
        assert_eq!(url, "https://atcoder.jp/contests/abc001/tasks");
    }

    #[test]
    fn test_resolve_url_outside_contest_and_problem() {
        let ctx = CurrentContext::Outside;
        let url = resolve_url(ctx, &["abc001".to_string(), "a".to_string()]).unwrap();
        assert_eq!(url, "https://atcoder.jp/contests/abc001/tasks/abc001_a");
    }

    #[test]
    fn test_resolve_url_outside_no_args_error() {
        let ctx = CurrentContext::Outside;
        assert!(resolve_url(ctx, &[]).is_err());
    }
}