From 8738a2a6dfe75944268dbcb8786f32503e4add01 Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Thu, 12 Sep 2024 00:11:12 -0700 Subject: [PATCH] feat: add basic libcanvas functionality --- Cargo.lock | 1 + Cargo.toml | 1 + src/lib.rs | 2 + src/libcanvas.rs | 62 ++++++++++++++++++++++++++++++ src/libcanvas/courses.rs | 83 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 45 ++++++---------------- 6 files changed, 160 insertions(+), 34 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/libcanvas.rs create mode 100644 src/libcanvas/courses.rs diff --git a/Cargo.lock b/Cargo.lock index 04b97c7..7962e13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,7 @@ name = "cartographer" version = "0.1.0" dependencies = [ "reqwest", + "serde", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index c52938c..5a8188b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] reqwest = { version = "0.12.7", features = ["json"] } tokio = { version = "1.15", features = ["full"] } +serde = { version = "1.0.210", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..90f8901 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod libcanvas; +pub use reqwest::Url; diff --git a/src/libcanvas.rs b/src/libcanvas.rs new file mode 100644 index 0000000..6be48f7 --- /dev/null +++ b/src/libcanvas.rs @@ -0,0 +1,62 @@ +use reqwest::header; +use reqwest::Url; +use reqwest::{Client, Error}; +use std::env; +pub mod courses; + +static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +/// A high level client for interfacing with the Canvas API. Conveniently wraps common Canvas +/// operations. +pub struct CanvasClient { + client: Client, + api_url: Url, +} + +impl CanvasClient { + /// Create a Canvas client with a given API base URL (eg. + /// `https://ucsb.instructure.com/api/v1/`) and access token. You must include the trailing + /// slash or else API calls will be malformed. + pub fn create(token: String, api_url: Url) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::AUTHORIZATION, + format!("Bearer {}", token).parse().unwrap(), + ); + let client = reqwest::Client::builder() + .user_agent(APP_USER_AGENT) + .default_headers(headers) + .build()?; + + Ok(CanvasClient { client, api_url }) + } + + /// Call an API endpoint. Expects the relative path of the endpoint after the base API URL. + /// Returns the response as plaintext in a String regardless of its serialization format. + /// + /// # Example + /// Gives you the GET response from `https://ucsb.instructure.com/api/v1/courses`. + /// ``` + /// use std::env; + /// use cartographer::libcanvas::CanvasClient; + /// use cartographer::Url; + /// + /// let client = CanvasClient::create( + /// "SAMPLE_TOKEN".to_string(), + /// Url::parse("https://ucsb.instructure.com/api/v1/").unwrap() + /// ).unwrap(); + /// + /// client.get_from_endpoint("courses"); + /// ``` + pub async fn get_from_endpoint(&self, endpoint: &str) -> Result { + let response = + self.client + .get(self.api_url.join(endpoint).expect( + "API endpoint returned error. Perhaps it was malformed or doesn't exist.", + )) + .send() + .await?; + + response.text().await + } +} diff --git a/src/libcanvas/courses.rs b/src/libcanvas/courses.rs new file mode 100644 index 0000000..1de3374 --- /dev/null +++ b/src/libcanvas/courses.rs @@ -0,0 +1,83 @@ +use super::CanvasClient; +use serde::{Deserialize, Serialize}; + +impl CanvasClient { + pub async fn get_courses(&self) -> Result, reqwest::Error> { + let response = self + .client + .get(self.api_url.join("courses").unwrap()) + .send() + .await?; + + response.json::>().await + } +} + +// Some time options are ISO 8601 standard times but they are parsed as Strings for now for +// simplicity +/// Represents a response from the `/courses` API endpoint. Some strings are plaintext and some are +/// HTML. Some JSON objects which have not yet been typed are deserialized into plaintext instead. +#[derive(Serialize, Deserialize, Debug)] +pub struct Course { + id: u32, + name: String, + account_id: u32, + uuid: String, + start_at: Option, + grading_standard_id: Option, + is_public: bool, + created_at: String, + course_code: String, + default_view: Option, + root_account_id: u32, + enrollment_term_id: u32, + term: Option, + permissions: Option, + course_progress: Option, + license: String, + public_description: Option, + access_restricted_by_date: Option, + blueprint_restrictions: Option, + blueprint_restrictions_by_object_type: Option, + syllabus_body: Option, + needs_grading_count: Option, + grade_passback_setting: Option, + end_at: Option, + public_syllabus: bool, + public_syllabus_to_auth: bool, + storage_quota_mb: usize, + is_public_to_auth_users: bool, + homeroom_course: bool, + course_color: Option, + friendly_name: Option, + apply_assignment_group_weights: bool, + calendar: Calendar, + time_zone: String, + blueprint: bool, + template: bool, + enrollments: Option>, + hide_final_grades: bool, + workflow_state: String, + restrict_enrollments_to_course_dates: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Enrollment { + r#type: String, + role: String, + role_id: u32, + user_id: u32, + enrollment_state: String, + limit_privileges_to_course_section: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Calendar { + ics: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Permissions { + create_discussion_topic: bool, + create_announcement: bool, +} diff --git a/src/main.rs b/src/main.rs index 619c5c7..2320cb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,17 @@ -use reqwest::header; -use reqwest::{Client, Error}; +use cartographer::libcanvas::CanvasClient; +use reqwest::{Error, Url}; use std::env; -static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); - -fn create_client() -> Result { - let token = - env::var("CANVAS_SECRET").expect("Canvas API key is not defined in the environment."); - - let mut headers = header::HeaderMap::new(); - headers.insert( - header::AUTHORIZATION, - format!("Bearer {}", token).parse().unwrap(), - ); - reqwest::Client::builder() - .user_agent(APP_USER_AGENT) - .default_headers(headers) - .build() -} - -async fn get_request() -> Result<(), reqwest::Error> { - let response = create_client()? - .get("https://ucsb.instructure.com/api/v1/courses") - .send() - .await?; - - println!("Status: {}", response.status()); - - let body = response.text().await?; - println!("Body:\n{}", body); - - Ok(()) -} - #[tokio::main] async fn main() -> Result<(), Error> { - get_request().await?; + let token = + env::var("CANVAS_SECRET").expect("Canvas API key is not defined in the environment."); + + // Base URL must have trailing slash or URL `.join()` will not work + let client = CanvasClient::create( + token, + Url::parse("https://ucsb.instructure.com/api/v1/").expect("Could not parse API URL."), + )?; + println!("{:?}", client.get_courses().await?); Ok(()) }