canvas_lms_sync/canvas_api/
mod.rs

1use once_cell::sync::Lazy;
2use regex::Regex;
3use reqwest::{header::HeaderValue, IntoUrl};
4use serde::{de::DeserializeOwned, Deserialize};
5
6pub mod files;
7pub mod modules;
8
9pub struct Client {
10    reqwest: reqwest::Client,
11    host: String,
12    auth_bearer: String,
13}
14
15#[derive(Debug, Deserialize)]
16pub struct ApiError {
17    pub errors: Vec<ApiErrorDetail>,
18}
19
20#[derive(Debug, Deserialize)]
21pub struct ApiErrorDetail {
22    pub message: String,
23}
24
25pub struct ResponsePagination {
26    current: String,
27    prev: Option<String>,
28    next: Option<String>,
29    first: String,
30    last: String,
31}
32
33impl From<&HeaderValue> for ResponsePagination {
34    fn from(value: &HeaderValue) -> Self {
35        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"<(.+?)>; rel="(.+?)""#).unwrap());
36
37        RE.captures_iter(value.to_str().unwrap())
38            .map(|cap| (cap[2].to_string(), cap[1].to_string()))
39            .fold(
40                ResponsePagination {
41                    current: String::new(),
42                    prev: None,
43                    next: None,
44                    first: String::new(),
45                    last: String::new(),
46                },
47                |mut acc, (rel, url)| {
48                    match rel.as_str() {
49                        "current" => acc.current = url,
50                        "prev" => acc.prev = Some(url),
51                        "next" => acc.next = Some(url),
52                        "first" => acc.first = url,
53                        "last" => acc.last = url,
54                        _ => (),
55                    }
56                    acc
57                },
58            )
59    }
60}
61
62#[derive(Debug)]
63pub enum Error {
64    ApiError(ApiError),
65    ReqwestError(reqwest::Error),
66}
67
68pub type ApiResult<T> = Result<T, Error>;
69
70impl Client {
71    pub fn new(host: String, auth_bearer: String) -> Self {
72        Self {
73            reqwest: reqwest::Client::new(),
74            host,
75            auth_bearer,
76        }
77    }
78    pub fn build_url(&self, path: &str) -> String {
79        format!(
80            "{}/{}",
81            self.host.strip_suffix("/").unwrap(),
82            path.strip_prefix("/").unwrap()
83        )
84    }
85    pub async fn make_json_request<T: DeserializeOwned, U: IntoUrl>(
86        &self,
87        url: U,
88    ) -> ApiResult<(T, Option<ResponsePagination>)> {
89        let response = self
90            .reqwest
91            .get(url)
92            .bearer_auth(self.auth_bearer.clone())
93            .send()
94            .await
95            .map_err(Error::ReqwestError)?;
96
97        if response.status() != 200 {
98            let json = response.json().await.map_err(Error::ReqwestError)?;
99            return Err(Error::ApiError(json));
100        }
101        let pagination = response.headers().get("Link").map(|v| v.into());
102        let json = response.json().await.map_err(Error::ReqwestError)?;
103        Ok((json, pagination))
104    }
105}