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", ¶ms);
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}"), ¶ms)
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", ¶ms);
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}