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