Compare commits
3 commits
7e691d07a6
...
8738a2a6df
Author | SHA1 | Date | |
---|---|---|---|
8738a2a6df | |||
20f3bf6ca6 | |||
69af77d0f0 |
8 changed files with 164 additions and 39 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -73,6 +73,7 @@ name = "cartographer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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
0
rustfmt.toml
Normal file
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod libcanvas;
|
||||||
|
pub use reqwest::Url;
|
62
src/libcanvas.rs
Normal file
62
src/libcanvas.rs
Normal 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
83
src/libcanvas/courses.rs
Normal 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,
|
||||||
|
}
|
45
src/main.rs
45
src/main.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue