Skip to main content

canvas_lms_api/
client.rs

1use crate::{
2    error::Result,
3    http::Requester,
4    pagination::PageStream,
5    params::wrap_params,
6    resources::{
7        account::Account,
8        course::Course,
9        params::{course_params::CreateCourseParams, user_params::CreateUserParams},
10        user::{CurrentUser, User, UserId},
11    },
12};
13use reqwest::Client;
14use std::sync::Arc;
15use url::Url;
16
17/// The Canvas LMS API client. All interaction starts here.
18///
19/// # Example
20/// ```no_run
21/// # #[tokio::main] async fn main() -> canvas_lms_api::Result<()> {
22/// let canvas = canvas_lms_api::Canvas::new("https://canvas.example.edu", "my-token")?;
23/// let course = canvas.get_course(1).await?;
24/// println!("{}", course.name.unwrap_or_default());
25/// # Ok(()) }
26/// ```
27pub struct Canvas {
28    pub(crate) requester: Arc<Requester>,
29}
30
31impl Canvas {
32    /// Create a new Canvas client.
33    ///
34    /// `base_url` should be the institution root (e.g. `https://canvas.example.edu`),
35    /// not including `/api/v1`.
36    pub fn new(base_url: &str, access_token: &str) -> Result<Self> {
37        Self::with_client(base_url, access_token, Client::new())
38    }
39
40    /// Create a Canvas client with a custom [`reqwest::Client`] (for proxy, TLS config, etc.).
41    pub fn with_client(base_url: &str, access_token: &str, client: Client) -> Result<Self> {
42        let base_url = validate_base_url(base_url)?;
43        let api_url = base_url.join("api/v1/")?;
44        Ok(Self {
45            requester: Arc::new(Requester::new(
46                api_url,
47                access_token.trim().to_string(),
48                client,
49            )),
50        })
51    }
52
53    // -------------------------------------------------------------------------
54    // Courses
55    // -------------------------------------------------------------------------
56
57    /// Fetch a single course by ID.
58    ///
59    /// # Canvas API
60    /// `GET /api/v1/courses/:id`
61    pub async fn get_course(&self, course_id: u64) -> Result<Course> {
62        let mut course: Course = self
63            .requester
64            .get(&format!("courses/{course_id}"), &[])
65            .await?;
66        course.requester = Some(Arc::clone(&self.requester));
67        Ok(course)
68    }
69
70    /// Stream all courses visible to the authenticated user.
71    ///
72    /// # Canvas API
73    /// `GET /api/v1/courses`
74    pub fn get_courses(&self) -> PageStream<Course> {
75        PageStream::new_with_injector(
76            Arc::clone(&self.requester),
77            "courses",
78            vec![],
79            |mut c: Course, req| {
80                c.requester = Some(Arc::clone(&req));
81                c
82            },
83        )
84    }
85
86    /// Create a new course under an account.
87    ///
88    /// # Canvas API
89    /// `POST /api/v1/accounts/:account_id/courses`
90    pub async fn create_course(
91        &self,
92        account_id: u64,
93        params: CreateCourseParams,
94    ) -> Result<Course> {
95        let form = wrap_params("course", &params);
96        let mut course: Course = self
97            .requester
98            .post(&format!("accounts/{account_id}/courses"), &form)
99            .await?;
100        course.requester = Some(Arc::clone(&self.requester));
101        Ok(course)
102    }
103
104    /// Delete a course by ID. Canvas returns the deleted course object.
105    ///
106    /// # Canvas API
107    /// `DELETE /api/v1/courses/:id`
108    pub async fn delete_course(&self, course_id: u64) -> Result<Course> {
109        let params = vec![("event".to_string(), "delete".to_string())];
110        let mut course: Course = self
111            .requester
112            .delete(&format!("courses/{course_id}"), &params)
113            .await?;
114        course.requester = Some(Arc::clone(&self.requester));
115        Ok(course)
116    }
117
118    // -------------------------------------------------------------------------
119    // Users
120    // -------------------------------------------------------------------------
121
122    /// Fetch a single user by ID or `UserId::Current` for the authenticated user.
123    ///
124    /// # Canvas API
125    /// `GET /api/v1/users/:id`
126    pub async fn get_user(&self, user_id: UserId) -> Result<User> {
127        let id = user_id.to_path_segment();
128        let mut user: User = self.requester.get(&format!("users/{id}"), &[]).await?;
129        user.requester = Some(Arc::clone(&self.requester));
130        Ok(user)
131    }
132
133    /// Fetch the currently authenticated user.
134    ///
135    /// # Canvas API
136    /// `GET /api/v1/users/self`
137    pub async fn get_current_user(&self) -> Result<CurrentUser> {
138        self.requester.get("users/self", &[]).await
139    }
140
141    /// Create a new user under an account.
142    ///
143    /// # Canvas API
144    /// `POST /api/v1/accounts/:account_id/users`
145    pub async fn create_user(&self, account_id: u64, params: CreateUserParams) -> Result<User> {
146        let form = wrap_params("user", &params);
147        let mut user: User = self
148            .requester
149            .post(&format!("accounts/{account_id}/users"), &form)
150            .await?;
151        user.requester = Some(Arc::clone(&self.requester));
152        Ok(user)
153    }
154
155    // -------------------------------------------------------------------------
156    // Accounts
157    // -------------------------------------------------------------------------
158
159    /// Fetch a single account by ID.
160    ///
161    /// # Canvas API
162    /// `GET /api/v1/accounts/:id`
163    pub async fn get_account(&self, account_id: u64) -> Result<Account> {
164        self.requester
165            .get(&format!("accounts/{account_id}"), &[])
166            .await
167    }
168
169    /// Stream all accounts accessible to the authenticated user.
170    ///
171    /// # Canvas API
172    /// `GET /api/v1/accounts`
173    pub fn get_accounts(&self) -> PageStream<Account> {
174        PageStream::new(Arc::clone(&self.requester), "accounts", vec![])
175    }
176}
177
178fn validate_base_url(raw: &str) -> Result<Url> {
179    let trimmed = raw.trim().trim_end_matches('/');
180    if trimmed.contains("/api/v1") {
181        return Err(crate::error::CanvasError::ApiError {
182            status: 0,
183            message: "base_url should not include /api/v1".into(),
184        });
185    }
186    Ok(Url::parse(&format!("{trimmed}/"))?)
187}