canvas-cli 0.1.0

Interact with Canvas LMS from the command line.
Documentation
use colored::Colorize;
use inquire::Select;
use reqwest::Client;
use serde_derive::Deserialize;
use std::{collections::HashMap, fmt::Display};

pub type DateTime = chrono::DateTime<chrono::Utc>;

#[derive(Debug, Hash, Clone, PartialEq, Eq)]
pub struct Course {
    pub name: String,
    pub id: u32,
    is_favorite: bool,
    css_color: Option<String>,
    created_at: DateTime,
}

#[derive(Deserialize, Debug)]
struct CourseResponse {
    id: u32,
    name: String,
    is_favorite: bool,
    created_at: DateTime,
    concluded: bool,
}

#[derive(Deserialize, Debug)]
struct ColorsResponse {
    custom_colors: HashMap<String, String>,
}

impl Display for Course {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let css_color = self.css_color.clone().unwrap_or("#000000".to_string());
        let color = csscolorparser::parse(&css_color)
            .unwrap()
            .to_linear_rgba_u8();
        write!(
            f,
            "{}{}{}",
            "".truecolor(color[0], color[1], color[2]),
            self.name,
            if self.is_favorite { "" } else { "" }.yellow()
        )
    }
}

impl Course {
    pub async fn fetch(
        course_id: Option<u32>,
        base_url: &str,
        client: &Client,
    ) -> Result<Course, anyhow::Error> {
        Ok(if let Some(course_id) = course_id {
            let course_response = client
                .get(format!(
                    "{}/api/v1/courses/{}?include[]=favorites&include[]=concluded",
                    base_url, course_id
                ))
                .send()
                .await?
                .json::<CourseResponse>()
                .await?;
            log::info!("Made REST request to get course information");

            let course_colors: HashMap<u32, String> = client
                .get(format!("{}/api/v1/users/self/colors", base_url))
                .send()
                .await?
                .json::<ColorsResponse>()
                .await?
                .custom_colors
                .into_iter()
                .filter(|(k, _)| k.starts_with("course_"))
                .map(|(k, v)| (k.trim_start_matches("course_").parse::<u32>().unwrap(), v))
                .collect();
            log::info!("Made REST request to get course colors");

            let course = Course {
                name: course_response.name,
                id: course_response.id,
                is_favorite: course_response.is_favorite,
                css_color: course_colors.get(&course_response.id).cloned(),
                created_at: course_response.created_at,
            };

            println!("✓ Found {course}");
            course
        } else {
            let courses_response = client
                .get(format!(
                    "{}/api/v1/courses?per_page=1000&include[]=favorites&include[]=concluded",
                    base_url
                ))
                .send()
                .await?
                .json::<Vec<serde_json::Value>>()
                .await?
                .into_iter()
                .filter_map(|v| serde_json::from_value(v).ok())
                .collect::<Vec<CourseResponse>>();

            log::info!("Made REST request to get favorite courses");

            let course_colors: HashMap<u32, String> = client
                .get(format!("{}/api/v1/users/self/colors", base_url))
                .send()
                .await?
                .json::<ColorsResponse>()
                .await?
                .custom_colors
                .into_iter()
                .filter(|(k, _)| k.starts_with("course_"))
                .map(|(k, v)| (k.trim_start_matches("course_").parse::<u32>().unwrap(), v))
                .collect();
            log::info!("Made REST request to get course colors");

            println!("✓ Queried course information");

            let mut courses: Vec<Course> = courses_response
                .into_iter()
                .filter(|course| !course.concluded)
                .map(|course| Course {
                    name: course.name.clone(),
                    id: course.id,
                    is_favorite: course.is_favorite,
                    css_color: course_colors.get(&course.id).cloned(),
                    created_at: course.created_at,
                })
                .collect();

            courses.sort_by(|a, b| {
                b.is_favorite
                    .cmp(&a.is_favorite)
                    .then(a.created_at.cmp(&b.created_at))
            });
            Select::new("Course?", courses).prompt()?
        })
    }
}