1use crate::{
2 error::Result,
3 http::Requester,
4 pagination::PageStream,
5 params::wrap_params,
6 resources::{
7 account::Account,
8 appointment_group::{AppointmentGroup, AppointmentGroupParams},
9 calendar_event::{CalendarEvent, CalendarEventParams},
10 conversation::{Conversation, ConversationParams},
11 course::Course,
12 eportfolio::EPortfolio,
13 file::File,
14 folder::Folder,
15 group::Group,
16 jwt::CanvasJwt,
17 outcome::Outcome,
18 params::{course_params::CreateCourseParams, user_params::CreateUserParams},
19 planner::{PlannerNote, PlannerNoteParams, PlannerOverride},
20 progress::Progress,
21 section::Section,
22 user::{CurrentUser, User, UserId},
23 },
24};
25use reqwest::Client;
26use std::sync::Arc;
27use url::Url;
28
29pub struct Canvas {
40 pub(crate) requester: Arc<Requester>,
41}
42
43impl Canvas {
44 pub fn new(base_url: &str, access_token: &str) -> Result<Self> {
49 Self::with_client(base_url, access_token, Client::new())
50 }
51
52 pub fn with_client(base_url: &str, access_token: &str, client: Client) -> Result<Self> {
54 let base_url = validate_base_url(base_url)?;
55 let api_url = base_url.join("api/v1/")?;
56 Ok(Self {
57 requester: Arc::new(Requester::new(
58 api_url,
59 access_token.trim().to_string(),
60 client,
61 )),
62 })
63 }
64
65 pub async fn get_course(&self, course_id: u64) -> Result<Course> {
74 let mut course: Course = self
75 .requester
76 .get(&format!("courses/{course_id}"), &[])
77 .await?;
78 course.requester = Some(Arc::clone(&self.requester));
79 Ok(course)
80 }
81
82 pub fn get_courses(&self) -> PageStream<Course> {
87 PageStream::new_with_injector(
88 Arc::clone(&self.requester),
89 "courses",
90 vec![],
91 |mut c: Course, req| {
92 c.requester = Some(Arc::clone(&req));
93 c
94 },
95 )
96 }
97
98 pub async fn create_course(
103 &self,
104 account_id: u64,
105 params: CreateCourseParams,
106 ) -> Result<Course> {
107 let form = wrap_params("course", ¶ms);
108 let mut course: Course = self
109 .requester
110 .post(&format!("accounts/{account_id}/courses"), &form)
111 .await?;
112 course.requester = Some(Arc::clone(&self.requester));
113 Ok(course)
114 }
115
116 pub async fn delete_course(&self, course_id: u64) -> Result<Course> {
121 let params = vec![("event".to_string(), "delete".to_string())];
122 let mut course: Course = self
123 .requester
124 .delete(&format!("courses/{course_id}"), ¶ms)
125 .await?;
126 course.requester = Some(Arc::clone(&self.requester));
127 Ok(course)
128 }
129
130 pub async fn get_user(&self, user_id: UserId) -> Result<User> {
139 let id = user_id.to_path_segment();
140 let mut user: User = self.requester.get(&format!("users/{id}"), &[]).await?;
141 user.requester = Some(Arc::clone(&self.requester));
142 Ok(user)
143 }
144
145 pub async fn get_current_user(&self) -> Result<CurrentUser> {
150 self.requester.get("users/self", &[]).await
151 }
152
153 pub async fn create_user(&self, account_id: u64, params: CreateUserParams) -> Result<User> {
158 let form = wrap_params("user", ¶ms);
159 let mut user: User = self
160 .requester
161 .post(&format!("accounts/{account_id}/users"), &form)
162 .await?;
163 user.requester = Some(Arc::clone(&self.requester));
164 Ok(user)
165 }
166
167 pub async fn get_account(&self, account_id: u64) -> Result<Account> {
176 let mut account: Account = self
177 .requester
178 .get(&format!("accounts/{account_id}"), &[])
179 .await?;
180 account.requester = Some(Arc::clone(&self.requester));
181 Ok(account)
182 }
183
184 pub async fn get_outcome(&self, outcome_id: u64) -> Result<Outcome> {
189 let mut outcome: Outcome = self
190 .requester
191 .get(&format!("outcomes/{outcome_id}"), &[])
192 .await?;
193 outcome.requester = Some(Arc::clone(&self.requester));
194 Ok(outcome)
195 }
196
197 pub fn get_accounts(&self) -> PageStream<Account> {
202 PageStream::new_with_injector(
203 Arc::clone(&self.requester),
204 "accounts",
205 vec![],
206 |mut a: Account, req| {
207 a.requester = Some(Arc::clone(&req));
208 a
209 },
210 )
211 }
212
213 pub async fn get_section(&self, section_id: u64) -> Result<Section> {
222 self.requester
223 .get(&format!("sections/{section_id}"), &[])
224 .await
225 }
226
227 pub async fn get_group(&self, group_id: u64) -> Result<Group> {
232 self.requester.get(&format!("groups/{group_id}"), &[]).await
233 }
234
235 pub async fn get_file(&self, file_id: u64) -> Result<File> {
240 self.requester.get(&format!("files/{file_id}"), &[]).await
241 }
242
243 pub async fn get_folder(&self, folder_id: u64) -> Result<Folder> {
248 self.requester
249 .get(&format!("folders/{folder_id}"), &[])
250 .await
251 }
252
253 pub async fn get_progress(&self, progress_id: u64) -> Result<Progress> {
258 self.requester
259 .get(&format!("progress/{progress_id}"), &[])
260 .await
261 }
262
263 pub async fn get_conversation(&self, conversation_id: u64) -> Result<Conversation> {
272 let mut c: Conversation = self
273 .requester
274 .get(&format!("conversations/{conversation_id}"), &[])
275 .await?;
276 c.requester = Some(Arc::clone(&self.requester));
277 Ok(c)
278 }
279
280 pub fn get_conversations(&self) -> PageStream<Conversation> {
285 PageStream::new_with_injector(
286 Arc::clone(&self.requester),
287 "conversations",
288 vec![],
289 |mut c: Conversation, req| {
290 c.requester = Some(Arc::clone(&req));
291 c
292 },
293 )
294 }
295
296 pub async fn create_conversation(
301 &self,
302 recipients: &[&str],
303 body: &str,
304 params: ConversationParams,
305 ) -> Result<Conversation> {
306 let mut form: Vec<(String, String)> = recipients
307 .iter()
308 .map(|r| ("recipients[]".into(), r.to_string()))
309 .collect();
310 form.push(("body".into(), body.to_string()));
311 if let Some(subject) = params.subject {
312 form.push(("subject".into(), subject));
313 }
314 if let Some(fg) = params.force_new {
315 form.push(("force_new".into(), fg.to_string()));
316 }
317 if let Some(gc) = params.group_conversation {
318 form.push(("group_conversation".into(), gc.to_string()));
319 }
320 if let Some(ctx) = params.context_code {
321 form.push(("context_code".into(), ctx));
322 }
323 let result: serde_json::Value = self.requester.post("conversations", &form).await?;
325 let first = result
326 .as_array()
327 .and_then(|a| a.first())
328 .cloned()
329 .unwrap_or(result);
330 let mut c: Conversation = serde_json::from_value(first)?;
331 c.requester = Some(Arc::clone(&self.requester));
332 Ok(c)
333 }
334
335 pub async fn get_calendar_event(&self, event_id: u64) -> Result<CalendarEvent> {
344 let mut e: CalendarEvent = self
345 .requester
346 .get(&format!("calendar_events/{event_id}"), &[])
347 .await?;
348 e.requester = Some(Arc::clone(&self.requester));
349 Ok(e)
350 }
351
352 pub fn get_calendar_events(&self) -> PageStream<CalendarEvent> {
357 PageStream::new_with_injector(
358 Arc::clone(&self.requester),
359 "calendar_events",
360 vec![],
361 |mut e: CalendarEvent, req| {
362 e.requester = Some(Arc::clone(&req));
363 e
364 },
365 )
366 }
367
368 pub async fn create_calendar_event(
373 &self,
374 context_code: &str,
375 params: CalendarEventParams,
376 ) -> Result<CalendarEvent> {
377 let body = serde_json::to_value(¶ms).unwrap_or_default();
378 let mut form = wrap_params("calendar_event", &body);
379 form.push((
380 "calendar_event[context_code]".into(),
381 context_code.to_string(),
382 ));
383 let mut e: CalendarEvent = self.requester.post("calendar_events", &form).await?;
384 e.requester = Some(Arc::clone(&self.requester));
385 Ok(e)
386 }
387
388 pub async fn get_planner_note(&self, note_id: u64) -> Result<PlannerNote> {
397 let mut n: PlannerNote = self
398 .requester
399 .get(&format!("planner_notes/{note_id}"), &[])
400 .await?;
401 n.requester = Some(Arc::clone(&self.requester));
402 Ok(n)
403 }
404
405 pub fn get_planner_notes(&self) -> PageStream<PlannerNote> {
410 PageStream::new_with_injector(
411 Arc::clone(&self.requester),
412 "planner_notes",
413 vec![],
414 |mut n: PlannerNote, req| {
415 n.requester = Some(Arc::clone(&req));
416 n
417 },
418 )
419 }
420
421 pub async fn create_planner_note(&self, params: PlannerNoteParams) -> Result<PlannerNote> {
426 let flat: Vec<(String, String)> = serde_json::to_value(¶ms)
427 .unwrap_or_default()
428 .as_object()
429 .into_iter()
430 .flatten()
431 .filter_map(|(k, v)| {
432 v.as_str()
433 .map(|s| (k.clone(), s.to_string()))
434 .or_else(|| v.as_u64().map(|n| (k.clone(), n.to_string())))
435 })
436 .collect();
437 let mut n: PlannerNote = self.requester.post("planner_notes", &flat).await?;
438 n.requester = Some(Arc::clone(&self.requester));
439 Ok(n)
440 }
441
442 pub async fn get_planner_override(&self, override_id: u64) -> Result<PlannerOverride> {
447 let mut o: PlannerOverride = self
448 .requester
449 .get(&format!("planner/overrides/{override_id}"), &[])
450 .await?;
451 o.requester = Some(Arc::clone(&self.requester));
452 Ok(o)
453 }
454
455 pub fn get_planner_overrides(&self) -> PageStream<PlannerOverride> {
460 PageStream::new_with_injector(
461 Arc::clone(&self.requester),
462 "planner/overrides",
463 vec![],
464 |mut o: PlannerOverride, req| {
465 o.requester = Some(Arc::clone(&req));
466 o
467 },
468 )
469 }
470
471 pub async fn create_planner_override(
476 &self,
477 plannable_type: &str,
478 plannable_id: u64,
479 ) -> Result<PlannerOverride> {
480 let form = vec![
481 ("plannable_type".into(), plannable_type.to_string()),
482 ("plannable_id".into(), plannable_id.to_string()),
483 ];
484 let mut o: PlannerOverride = self.requester.post("planner/overrides", &form).await?;
485 o.requester = Some(Arc::clone(&self.requester));
486 Ok(o)
487 }
488
489 pub async fn get_eportfolio(&self, eportfolio_id: u64) -> Result<EPortfolio> {
498 let mut p: EPortfolio = self
499 .requester
500 .get(&format!("eportfolios/{eportfolio_id}"), &[])
501 .await?;
502 p.requester = Some(Arc::clone(&self.requester));
503 Ok(p)
504 }
505
506 pub async fn get_appointment_group(&self, group_id: u64) -> Result<AppointmentGroup> {
515 let mut a: AppointmentGroup = self
516 .requester
517 .get(&format!("appointment_groups/{group_id}"), &[])
518 .await?;
519 a.requester = Some(Arc::clone(&self.requester));
520 Ok(a)
521 }
522
523 pub fn get_appointment_groups(&self) -> PageStream<AppointmentGroup> {
528 PageStream::new_with_injector(
529 Arc::clone(&self.requester),
530 "appointment_groups",
531 vec![],
532 |mut a: AppointmentGroup, req| {
533 a.requester = Some(Arc::clone(&req));
534 a
535 },
536 )
537 }
538
539 pub async fn create_appointment_group(
544 &self,
545 params: AppointmentGroupParams,
546 ) -> Result<AppointmentGroup> {
547 let body = serde_json::to_value(¶ms).unwrap_or_default();
548 let form = wrap_params("appointment_group", &body);
549 let mut a: AppointmentGroup = self.requester.post("appointment_groups", &form).await?;
550 a.requester = Some(Arc::clone(&self.requester));
551 Ok(a)
552 }
553
554 #[cfg(feature = "graphql")]
569 pub fn graphql(&self) -> crate::graphql::GraphQL {
570 crate::graphql::GraphQL {
571 requester: Arc::clone(&self.requester),
572 }
573 }
574
575 pub async fn create_jwt(&self) -> Result<CanvasJwt> {
584 self.requester.post("jwts", &[]).await
585 }
586
587 pub async fn refresh_jwt(&self, token: &str) -> Result<CanvasJwt> {
592 let params = vec![("jwt".into(), token.to_string())];
593 self.requester.post("jwts/refresh", ¶ms).await
594 }
595}
596
597fn validate_base_url(raw: &str) -> Result<Url> {
598 let trimmed = raw.trim().trim_end_matches('/');
599 if trimmed.contains("/api/v1") {
600 return Err(crate::error::CanvasError::ApiError {
601 status: 0,
602 message: "base_url should not include /api/v1".into(),
603 });
604 }
605 Ok(Url::parse(&format!("{trimmed}/"))?)
606}