atcoder-kit 0.1.5

A command-line tool for AtCoder like acc and oj.
Documentation
pub(crate) mod sample;
mod submit;

use crate::application::sample::{TestResult, run_sample_tests};
use crate::application::submit::{prepare_solution, submit_prepared_solution};
use crate::client::AtCoderClient;
use crate::client::auth::{prompt_revel_session, verify_current_session};
use crate::client::cookie::Cookie;
use crate::validation::validate_atcoder_identifier;
use crate::workspace::command::SystemCommandRunner;
use crate::workspace::contest::save_contest_to;
use crate::workspace::problem::ProblemWorkspace;
use crate::workspace::template::{NewTemplate, TemplateRegistry, create_template};
use anyhow::{Context, Result};
use std::path::PathBuf;

pub(crate) enum AppEvent {
    FetchingContest(String),
    SavingContest(String),
    Testing,
    TestSuccessful,
    Submitting,
    SubmitSuccessful,
    LoggingIn,
    LoggingOut,
}

pub(crate) enum LoginOutcome {
    LoggedIn { username: String },
    AlreadyLoggedIn { username: String },
    ExistingSessionInvalid,
}

pub(crate) enum SessionStatus {
    LoggedIn { username: String },
    Invalid,
    LoggedOut,
}

pub(crate) struct DownloadOutcome {
    pub(crate) path: PathBuf,
}

pub(crate) struct SubmitOutcome {
    pub(crate) submission_url: String,
}

pub(crate) struct TemplateCreated {
    pub(crate) path: PathBuf,
}

pub(crate) struct Application {
    start_path: PathBuf,
}

impl Application {
    pub(crate) fn new(start_path: PathBuf) -> Self {
        Self { start_path }
    }

    pub(crate) async fn download<F>(
        &self,
        contest_id: &str,
        template_name: Option<&str>,
        no_template: bool,
        mut event: F,
    ) -> Result<DownloadOutcome>
    where
        F: FnMut(AppEvent),
    {
        validate_atcoder_identifier(contest_id, "contest ID")?;
        event(AppEvent::FetchingContest(contest_id.to_string()));
        let client = AtCoderClient::from_stored_session()?;
        let contest = client.fetch_contest(contest_id).await?;

        event(AppEvent::SavingContest(contest_id.to_string()));
        let registry = (!no_template).then(TemplateRegistry::load).transpose()?;
        let template = registry
            .as_ref()
            .map(|registry| registry.select(template_name))
            .transpose()?
            .flatten();
        let path = save_contest_to(&self.start_path, &contest, template)?;
        Ok(DownloadOutcome { path })
    }

    pub(crate) async fn test(&self) -> Result<Vec<TestResult>> {
        let workspace = ProblemWorkspace::discover_from(&self.start_path)?;
        run_sample_tests(&workspace, &SystemCommandRunner).await
    }

    pub(crate) async fn submit<F>(&self, no_test: bool, mut event: F) -> Result<SubmitOutcome>
    where
        F: FnMut(AppEvent),
    {
        event(AppEvent::Testing);
        let workspace = ProblemWorkspace::discover_from(&self.start_path)?;
        let runner = SystemCommandRunner;
        let source = prepare_solution(&workspace, &runner, no_test).await?;
        event(AppEvent::TestSuccessful);

        event(AppEvent::Submitting);
        let client = AtCoderClient::from_stored_session()?;
        let submission_url = submit_prepared_solution(&workspace, &client, source).await?;
        event(AppEvent::SubmitSuccessful);
        Ok(SubmitOutcome { submission_url })
    }

    pub(crate) async fn login<F>(&self, overwrite: bool, mut event: F) -> Result<LoginOutcome>
    where
        F: FnMut(AppEvent),
    {
        if !overwrite && let Some(result) = verify_current_session().await {
            return Ok(match result {
                Ok(username) => LoginOutcome::AlreadyLoggedIn { username },
                Err(_) => LoginOutcome::ExistingSessionInvalid,
            });
        }

        let revel_session = prompt_revel_session()?;
        event(AppEvent::LoggingIn);
        let client = AtCoderClient::new(Some(&revel_session))?;
        let username = client.validate_session().await?;
        Cookie { revel_session }
            .store()
            .context("Failed to store REVEL_SESSION.")?;
        Ok(LoginOutcome::LoggedIn { username })
    }

    pub(crate) fn logout<F>(&self, mut event: F) -> Result<SessionStatus>
    where
        F: FnMut(AppEvent),
    {
        event(AppEvent::LoggingOut);
        Cookie::set_default().context("Failed to clear REVEL_SESSION.")?;
        Ok(SessionStatus::LoggedOut)
    }

    pub(crate) async fn whoami(&self) -> Result<SessionStatus> {
        Ok(match verify_current_session().await {
            Some(Ok(username)) => SessionStatus::LoggedIn { username },
            Some(Err(_)) => SessionStatus::Invalid,
            None => SessionStatus::LoggedOut,
        })
    }

    pub(crate) fn create_template(&self, request: NewTemplate<'_>) -> Result<TemplateCreated> {
        Ok(TemplateCreated {
            path: create_template(request)?,
        })
    }
}