Compare commits

...

3 commits

8 changed files with 164 additions and 39 deletions

1
Cargo.lock generated
View file

@ -73,6 +73,7 @@ name = "cartographer"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"serde",
"tokio", "tokio",
] ]

View file

@ -6,3 +6,4 @@ edition = "2021"
[dependencies] [dependencies]
reqwest = { version = "0.12.7", features = ["json"] } reqwest = { version = "0.12.7", features = ["json"] }
tokio = { version = "1.15", features = ["full"] } tokio = { version = "1.15", features = ["full"] }
serde = { version = "1.0.210", features = ["derive"] }

View file

@ -22,13 +22,12 @@
in in
{ {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
pkg-config
rust-bin.stable.latest.default
];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
rust-analyzer pkg-config
rust-bin.stable.latest.rust-analyzer
rust-bin.stable.latest.default
openssl openssl
rust-bin.stable.latest.rustfmt
]; ];
}; };
packages.default = pkgs.rustPlatform.buildRustPackage { packages.default = pkgs.rustPlatform.buildRustPackage {

0
rustfmt.toml Normal file
View file

2
src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod libcanvas;
pub use reqwest::Url;

62
src/libcanvas.rs Normal file
View file

@ -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<CanvasClient, Error> {
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<String, reqwest::Error> {
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
}
}

83
src/libcanvas/courses.rs Normal file
View file

@ -0,0 +1,83 @@
use super::CanvasClient;
use serde::{Deserialize, Serialize};
impl CanvasClient {
pub async fn get_courses(&self) -> Result<Vec<Course>, reqwest::Error> {
let response = self
.client
.get(self.api_url.join("courses").unwrap())
.send()
.await?;
response.json::<Vec<Course>>().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<String>,
grading_standard_id: Option<u32>,
is_public: bool,
created_at: String,
course_code: String,
default_view: Option<String>,
root_account_id: u32,
enrollment_term_id: u32,
term: Option<String>,
permissions: Option<Permissions>,
course_progress: Option<String>,
license: String,
public_description: Option<String>,
access_restricted_by_date: Option<bool>,
blueprint_restrictions: Option<String>,
blueprint_restrictions_by_object_type: Option<String>,
syllabus_body: Option<String>,
needs_grading_count: Option<u32>,
grade_passback_setting: Option<String>,
end_at: Option<String>,
public_syllabus: bool,
public_syllabus_to_auth: bool,
storage_quota_mb: usize,
is_public_to_auth_users: bool,
homeroom_course: bool,
course_color: Option<String>,
friendly_name: Option<String>,
apply_assignment_group_weights: bool,
calendar: Calendar,
time_zone: String,
blueprint: bool,
template: bool,
enrollments: Option<Vec<Enrollment>>,
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,
}

View file

@ -1,40 +1,17 @@
use reqwest::header; use cartographer::libcanvas::CanvasClient;
use reqwest::{Client, Error}; use reqwest::{Error, Url};
use std::env; use std::env;
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
fn create_client() -> Result<Client, Error> {
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] #[tokio::main]
async fn main() -> Result<(), Error> { 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(()) Ok(())
} }