~baka/miyagi

d87fa6cd5d9c8d4a257c28de0639eb2c124203db — baka 18 days ago 62c439a
Wasn't meaning for canvaslms to be a submodule
D canvaslms => canvaslms +0 -1
@@ 1,1 0,0 @@
Subproject commit 8e9568b34e142b83ed843b2db37f5cab6b491c0d

A canvaslms/Cargo.lock => canvaslms/Cargo.lock +517 -0
@@ 0,0 1,517 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"

[[package]]
name = "android_system_properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
dependencies = [
 "libc",
]

[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"

[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"

[[package]]
name = "bumpalo"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"

[[package]]
name = "canvaslms"
version = "0.1.0"
dependencies = [
 "chrono",
 "serde",
 "serde_json",
 "ureq",
]

[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "chrono"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
 "iana-time-zone",
 "num-integer",
 "num-traits",
 "serde",
 "time",
 "winapi",
]

[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"

[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"

[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
 "cfg-if",
]

[[package]]
name = "flate2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [
 "crc32fast",
 "miniz_oxide",
]

[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
 "matches",
 "percent-encoding",
]

[[package]]
name = "iana-time-zone"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
dependencies = [
 "android_system_properties",
 "core-foundation-sys",
 "js-sys",
 "wasm-bindgen",
 "winapi",
]

[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
 "matches",
 "unicode-bidi",
 "unicode-normalization",
]

[[package]]
name = "itoa"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"

[[package]]
name = "js-sys"
version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
dependencies = [
 "wasm-bindgen",
]

[[package]]
name = "libc"
version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"

[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
 "cfg-if",
]

[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"

[[package]]
name = "miniz_oxide"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
dependencies = [
 "adler",
]

[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
 "autocfg",
 "num-traits",
]

[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
 "autocfg",
]

[[package]]
name = "once_cell"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"

[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"

[[package]]
name = "proc-macro2"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
 "cc",
 "libc",
 "once_cell",
 "spin",
 "untrusted",
 "web-sys",
 "winapi",
]

[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
 "log",
 "ring",
 "sct",
 "webpki",
]

[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"

[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
 "ring",
 "untrusted",
]

[[package]]
name = "serde"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "serde_json"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
dependencies = [
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"

[[package]]
name = "syn"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
 "libc",
 "wasi",
 "winapi",
]

[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
 "tinyvec_macros",
]

[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"

[[package]]
name = "unicode-bidi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"

[[package]]
name = "unicode-ident"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"

[[package]]
name = "unicode-normalization"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
dependencies = [
 "tinyvec",
]

[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"

[[package]]
name = "ureq"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f"
dependencies = [
 "base64",
 "chunked_transfer",
 "flate2",
 "log",
 "once_cell",
 "rustls",
 "serde",
 "serde_json",
 "url",
 "webpki",
 "webpki-roots",
]

[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
 "form_urlencoded",
 "idna",
 "matches",
 "percent-encoding",
]

[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"

[[package]]
name = "wasm-bindgen"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
dependencies = [
 "cfg-if",
 "wasm-bindgen-macro",
]

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
dependencies = [
 "bumpalo",
 "log",
 "once_cell",
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
dependencies = [
 "quote",
 "wasm-bindgen-macro-support",
]

[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"

[[package]]
name = "web-sys"
version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1"
dependencies = [
 "js-sys",
 "wasm-bindgen",
]

[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
 "ring",
 "untrusted",
]

[[package]]
name = "webpki-roots"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
dependencies = [
 "webpki",
]

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
 "winapi-i686-pc-windows-gnu",
 "winapi-x86_64-pc-windows-gnu",
]

[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

A canvaslms/Cargo.toml => canvaslms/Cargo.toml +10 -0
@@ 0,0 1,10 @@
[package]
name = "canvaslms"
version = "0.1.0"
edition = "2021"

[dependencies]
chrono = { version = "0.4.22", features = ["alloc", "serde", "time", "clock"], default_features = false }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = "1.0.83"
ureq = { version = "2.5.0", features = ["serde", "serde_json", "json"] }

A canvaslms/README.md => canvaslms/README.md +30 -0
@@ 0,0 1,30 @@

# canvaslms

An easy-to-use client for the Canvas LMS REST API written in Rust.

```rust
let client = Client {
    url: "https://yourschool.instructure.com",
    token: "12345~dmeeDJWJNWjdjwkdjk77483dmKD"
};

for c in courses(client).unwrap() {
    println!("{}", c.clone().name.unwrap());

    for a in c.overdue_assignments(client).unwrap() {
        if a.due_at.is_some() {
            println!(" ! {} ({})", a.name, a.due_in());
        }
    }
    for a in c.upcoming_assignments(client).unwrap() {
        if a.due_at.is_some() {
            println!(" - {} ({})", a.name, a.due_in());
        }
    }
}
```

## References

[Canvas LMS REST API Documentation](https://canvas.instructure.com/doc/api/)

A canvaslms/src/_models.rs => canvaslms/src/_models.rs +4 -0
@@ 0,0 1,4 @@

use crate::Client;
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};

A canvaslms/src/lib.rs => canvaslms/src/lib.rs +100 -0
@@ 0,0 1,100 @@
#![doc = include_str!("../README.md")]

extern crate ureq;
extern crate serde;
extern crate serde_json;

mod models;
pub use models::*;

use std::io::Read;
use serde_json::Result;
use chrono::prelude::*;

fn _get_req(c: Client, path: &str, params: &[(&str, &str)]) -> Box<dyn Read + Send + Sync + 'static> {
  let path = if path.starts_with("/") {
    format!("{}/api/v1{}", c.url, path)
  } else {
    path.to_string()
  };

  let mut r = ureq::get(path.as_str())
    .set("Authorization", format!("Bearer {}", c.token).as_str());

  for p in params {
    r = r.query(p.0, p.1);
  }

  r.call().unwrap().into_reader()
}

#[macro_export]
macro_rules! get_req {
    ( $client:expr, $path:expr, $params:expr ) => {{
        Ok(
            serde_json::from_reader(
                crate::_get_req($client, $path, $params)
            ).unwrap()
        )
    }}
}

pub fn courses(c: Client) -> Result<Vec<Course>> {
    get_req!{c, "/courses", &[
        ("state[]", "available"),
        ("enrollment_state", "active"),
        ("include[]", "total_students"),
        ("include[]", "syllabus_body"),
        ("include[]", "course_image"),
        //("include[]", "total_scores")
    ]}
}

#[derive(Clone)]
pub struct Client {
    pub url: &'static str,
    pub token: String,
}
impl Default for Client {
    fn default() -> Self {
        Self {
            url: "",
            token: String::new(),
        }
    }
}

impl Assignment {
    /// Returns a string indicating the number of days/weeks until an [Assignment] is due.
    /// 
    /// If the [Assignment] is overdue, this instead returns the number of days since the [Assignment] was due.
    pub fn due_in(&self) -> String {
        let due = self.due_at.unwrap();
        let now: DateTime<Utc> = Utc::now();

        let days: i64 = <i64 as From<u32>>::from(due.day()) - <i64 as From<u32>>::from(now.day());
        let weeks = due.iso_week().week() - now.iso_week().week();

        if days <= -1 {
            if days < -1 {
                format!("{} days late", days.abs())
            } else {
                "a day late".to_string()
            }
        } else if days < 7 {
            if days > 1 {
                format!("due in {} days", days)
            } else {
                "due tomorrow".to_string()
            }
        } else {
            if days > 7 {
                format!("{} weeks", weeks)
            } else {
                "a week".to_string()
            }
        }
    }
}

pub const ASSIGNMENTS_LIMIT: &'static str = "50";

A canvaslms/src/main.rs => canvaslms/src/main.rs +30 -0
@@ 0,0 1,30 @@
use canvaslms::*;

fn main() {
  let cl = Client {
    url: "https://ccsdschools.instructure.com",
    token: "11531~OiE5rvyDjyz6nhVVeGN3MbuFchIaLKG3aB4DQbf7MoDyZj2VUHCPbJQytZRCo4wS"
  };

  for c in courses(cl).unwrap() {
    println!("{}", c.clone().name.unwrap());

    for m in c.modules(cl).unwrap() {
      println!(" - {}", m.name);
      for i in m.items(cl).unwrap() {
        println!("   - {:?}: {}", i.kind, i.title);
      }
    }

    // for a in c.overdue_assignments(client).unwrap() {
    //     if a.due_at.is_some() {
    //         println!(" ! {} ({})", a.name, a.due_in());
    //     }
    // }
    // for a in c.upcoming_assignments(client).unwrap() {
    //     if a.due_at.is_some() {
    //         println!(" - {} ({})", a.name, a.due_in());
    //     }
    // }
  }
}
\ No newline at end of file

A canvaslms/src/models/assignment.rs => canvaslms/src/models/assignment.rs +11 -0
@@ 0,0 1,11 @@
use super::*;

#[derive(Serialize, Deserialize, Clone)]
pub struct Assignment {
    pub id:          i64,
    pub name:        String,
    pub description: Option<String>,
    pub due_at:      Option<DateTime<Utc>>,
    #[serde(rename = "html_url")]
    pub url:         Option<String>
}
\ No newline at end of file

A canvaslms/src/models/course.rs => canvaslms/src/models/course.rs +137 -0
@@ 0,0 1,137 @@
use super::*;

#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Course {
    pub id:             u64,
    pub name:           Option<String>,
    pub course_code:    Option<String>,
    pub original_name:  Option<String>,
    pub start_at:       Option<DateTime<Utc>>,
    pub end_at:         Option<DateTime<Utc>>,
    pub total_students: Option<i64>,
    pub calendar:       Option<Calendar>,
    #[serde(rename = "image_download_url")]
    pub image_url:      Option<String>,
    pub default_view:   Option<DefaultView>,
    pub syllabus_body:  Option<String>,
    #[serde(rename = "html_url")]
    pub url:            Option<String>
}
impl Course {
    pub fn new(id: u64) -> Self {
        Self {
            id,
            ..Default::default()
        }
    }
    pub fn peers(&self, c: Client) -> Result<Vec<User>> {
        let total_students = format!("{}", self.total_students.unwrap());
        get_req!{
            c,
            format!("/courses/{}/users", self.id).as_str(),
            &[
                ("include[]", "avatar_url"),
                ("include[]", "bio"),
                ("include[]", "custom_links"),
                ("enrollment_type[]", "student"),
                ("limit", total_students.as_str())
            ]
        }
    }
    pub fn modules(&self, c: Client) -> Result<Vec<Module>> {
      get_req!{
        c,
        format!("/courses/{}/modules", self.id).as_str(),
        &[]
      }
    }
    pub fn quizzes(&self, c: Client) -> Result<Vec<Quiz>> {
      get_req!{
        c,
        format!("/courses/{}/quizzes", self.id).as_str(),
        &[]
      }
    }
    /// Gets past [Assignment]s
    pub fn past_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "past")
            ]
        }
    }
    /// Gets unsubmitted [Assignment]s
    pub fn unsubmitted_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "unsubmitted")
            ]
        }
    }
    /// Gets overdue [Assignment]s
    pub fn overdue_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "overdue")
            ]
        }
    }
    /// Gets upcoming [Assignment]s
    pub fn upcoming_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "upcoming")
            ]
        }
    }
    /// Gets future [Assignment]s
    pub fn future_assignments(&self, c: Client) -> Result<Vec<Assignment>> {
        get_req!{
            c,
            format!("/courses/{}/assignments", self.id).as_str(),
            &[
                ("limit", ASSIGNMENTS_LIMIT),
                ("bucket", "future")
            ]
        }
    }
}

#[derive(Serialize, Deserialize, Clone)]
/// An `.ics` calendar
pub struct Calendar {
    /// URL to the `.ics` file
    pub ics: String
}

#[derive(Serialize, Deserialize, Clone)]
/// Teacher-selected `default_view` for a [Course]
pub enum DefaultView {
    #[serde(rename = "feed")]
    /// `feed`
    Feed,
    #[serde(rename = "wiki")]
    /// `wiki`
    Wiki,
    #[serde(rename = "modules")]
    /// `modules`
    Modules,
    #[serde(rename = "assignments")]
    /// `assignments`
    Assignments,
    #[serde(rename = "syllabus")]
    /// `syllabus`
    Syllabus
}
\ No newline at end of file

A canvaslms/src/models/mod.rs => canvaslms/src/models/mod.rs +17 -0
@@ 0,0 1,17 @@

pub use crate::{ Client, get_req, ASSIGNMENTS_LIMIT };
pub use serde::{ Deserialize, Serialize };
pub use serde_json::Result;
pub use chrono::{ DateTime, Utc };

mod course;
pub use course::*;

mod module;
pub use module::*;
mod quiz;
pub use quiz::*;
mod assignment;
pub use assignment::*;
mod user;
pub use user::*;

A canvaslms/src/models/module.rs => canvaslms/src/models/module.rs +62 -0
@@ 0,0 1,62 @@
use super::*;

#[derive(Serialize, Deserialize, Clone)]
pub struct Module {
  pub id: u64,
  pub name: String,
  pub items_url: String,
}
impl Module {
    pub fn items(&self, c: Client) -> Result<Vec<Item>> {
      get_req!{
        c,
        self.items_url.as_str(),
        &[]
      }
    }
  }

#[derive(Serialize, Deserialize, Clone)]
pub struct Item {
  pub id: u64,
  pub title: String,
  #[serde(rename = "type")]
  pub kind: ItemType,
  pub html_url: Option<String>,
  pub url: Option<String>,
  /// external url that the item points to
  /// 
  /// (only for [ItemType::ExternalUrl] and [ItemType::ExternalTool] types)
  pub external_url: Option<String>,
  /// whether the external tool opens in a new tab
  /// 
  /// (only for [ItemType::ExternalTool] type)
  pub new_tab: Option<bool>,
}

#[derive(Serialize, Deserialize, Clone)]
pub enum HideResults {
  #[serde(rename = "always")]
  Always,
  #[serde(rename = "until_after_last_attempt")]
  UntilLastAttempt,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ItemType {
  File,
  Page,
  Discussion,
  Assignment,
  Quiz,
  SubHeader,
  ExternalUrl,
  ExternalTool,
}

#[derive(Deserialize, Serialize, Clone)]
#[serde(rename = "snake_case")]
pub enum ScoringPolicy {
  KeepHighest,
  KeepLatest,
}
\ No newline at end of file

A canvaslms/src/models/quiz.rs => canvaslms/src/models/quiz.rs +48 -0
@@ 0,0 1,48 @@
use super::*;

#[derive(Serialize, Deserialize, Clone)]
pub struct Quiz {
  pub id: u64,

  pub title: String,
  pub description: Option<String>,

  /// whether the quiz has a published or unpublished draft state.
  pub published: bool,
  /// quiz time limit in minutes
  pub time_limit: Option<u64>,
  /// shuffle answers for students?
  pub shuffle_answers: bool,
  /// let students see their quiz responses?
  /// 
  /// possible values: null, `always`, `until_after_last_attempt`
  pub hide_results: Option<HideResults>,
  /// show which answers were correct when results are shown?
  /// 
  /// only valid if `hide_results` is null
  pub show_correct_answers: bool,
  /// restrict the `show_correct_answers` option above to apply only to the last submitted attempt of a quiz that allows multiple attempts.
  /// 
  /// only valid if `show_correct_answers` is true and `allowed_attempts` > 1
  pub show_correct_answers_last_attempt: bool,
  /// prevent the students from seeing their results more than once (right after they submit the quiz)
  pub one_time_results: bool,
  /// which quiz score to keep (only if `allowed_attempts` is not 1)
  /// 
  /// possible values: `keep_highest`, `keep_latest`
  pub scoring_policy: Option<ScoringPolicy>,
  /// how many times a student can take the quiz
  /// 
  /// -1 = unlimited attempts
  pub allowed_attempts: u32,
  /// show one question at a time?
  pub one_question_at_a_time: bool,
  /// the number of questions in the quiz
  pub question_count: u32,
  /// The total point value given to the quiz
  pub points_possible: u32,
  /// lock questions after answering?
  /// 
  /// only valid if `one_question_at_a_time` is true
  pub cant_go_back: bool,
}
\ No newline at end of file

A canvaslms/src/models/user.rs => canvaslms/src/models/user.rs +17 -0
@@ 0,0 1,17 @@
use super::*;

#[derive(Serialize, Deserialize, Clone)]
/// A [User] is a real person (or test student)
pub struct User {
    pub id:         i64,
    pub name:       String,
    pub short_name: String,
    pub avatar_url: String,
    /// Only appears if the [User] has their pronouns set
    // Why are there only 3 options?
    pub pronouns:   Option<String>,
    pub email:      Option<String>,
    /// Only appears if the [User] has their bio set
    // Why do I not have the option to set this?
    pub bio:        Option<String>
}
\ No newline at end of file