cses-cli 0.1.3

CSES CLI is a lightweight tool for using CSES from the command line.
use std::fmt;
use std::num::ParseIntError;
use std::str::FromStr;

use anyhow::anyhow;
use miniserde::Deserialize;

#[derive(Debug, Clone, PartialEq)]
pub enum Scope {
    Course(String),
    Contest(u64),
}

impl FromStr for Scope {
    type Err = ParseIntError;
    fn from_str(s: &str) -> Result<Self, ParseIntError> {
        let try_number = s.parse();
        Ok(match try_number {
            Ok(number) => Scope::Contest(number),
            Err(_) => Scope::Course(s.to_owned()),
        })
    }
}

impl fmt::Display for Scope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Scope::Course(id) => write!(f, "{}", id)?,
            Scope::Contest(id) => write!(f, "{}", id)?,
        }
        Ok(())
    }
}

#[derive(Debug, Deserialize)]
pub struct ScopeContent {
    pub sections: Vec<ScopeSection>,
}

#[derive(Debug, Deserialize)]
pub struct ScopeSection {
    pub header: String,
    pub text: Option<String>,
    pub list: Vec<ScopeItemRaw>,
}

#[derive(Debug)]
pub enum ScopeItem<'a> {
    Text {
        name: &'a str,
        id: &'a str,
        link: &'a str,
    },
    Link {
        name: &'a str,
        link: &'a str,
    },
    Task {
        name: &'a str,
        id: &'a str,
        link: &'a str,
        status: Option<TaskStatus>,
        score: Option<u64>,
    },
}

#[derive(Debug, Deserialize)]
pub struct ScopeItemRaw {
    item_type: ScopeItemType,
    name: String,
    id: Option<String>,
    link: String,
    status: Option<TaskStatus>,
    score: Option<u64>,
}

#[derive(Debug, Deserialize, Copy, Clone)]
pub enum TaskStatus {
    #[serde(rename = "pass")]
    Pass,
    #[serde(rename = "fail")]
    Fail,
    #[serde(rename = "none")]
    None,
}

#[derive(Debug, Deserialize)]
pub enum ScopeItemType {
    #[serde(rename = "text")]
    Text,
    #[serde(rename = "link")]
    Link,
    #[serde(rename = "task")]
    Task,
}

impl ScopeItemRaw {
    pub fn as_enum(&self) -> anyhow::Result<ScopeItem<'_>> {
        Ok(match &self.item_type {
            ScopeItemType::Text => ScopeItem::Text {
                name: &self.name,
                id: self
                    .id
                    .as_deref()
                    .ok_or_else(|| anyhow!("Text item does not have ID"))?,
                link: &self.link,
            },
            ScopeItemType::Link => ScopeItem::Link {
                name: &self.name,
                link: &self.link,
            },
            ScopeItemType::Task => ScopeItem::Task {
                name: &self.name,
                id: self
                    .id
                    .as_deref()
                    .ok_or_else(|| anyhow!("Task item does not have ID"))?,
                link: &self.link,
                status: self.status,
                score: self.score,
            },
        })
    }
}

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

    #[test]
    fn from_str_course() {
        let scope = "text123".parse().unwrap();
        assert!(matches!(
            scope,
            Scope::Course(id)
            if id == "text123"
        ));
    }

    #[test]
    fn from_str_contest() {
        let scope = "123".parse().unwrap();
        assert!(matches!(scope, Scope::Contest(123)));
    }

    #[test]
    fn to_string_course() {
        let string = Scope::Course("text123".to_owned()).to_string();
        assert_eq!(string, "text123");
    }

    #[test]
    fn to_string_contest() {
        let string = Scope::Contest(123).to_string();
        assert_eq!(string, "123");
    }
}