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        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        poll::{CreatePollParams, Poll},
21        progress::Progress,
22        section::Section,
23        user::{CurrentUser, User, UserId},
24    },
25};
26use reqwest::Client;
27use std::sync::Arc;
28use url::Url;
29
30/// The Canvas LMS API client. All interaction starts here.
31///
32/// # Example
33/// ```no_run
34/// # #[tokio::main] async fn main() -> canvas_lms_api::Result<()> {
35/// let canvas = canvas_lms_api::Canvas::new("https://canvas.example.edu", "my-token")?;
36/// let course = canvas.get_course(1).await?;
37/// println!("{}", course.name.unwrap_or_default());
38/// # Ok(()) }
39/// ```
40pub struct Canvas {
41    pub(crate) requester: Arc<Requester>,
42}
43
44impl Canvas {
45    /// Create a new Canvas client.
46    ///
47    /// `base_url` should be the institution root (e.g. `https://canvas.example.edu`),
48    /// not including `/api/v1`.
49    pub fn new(base_url: &str, access_token: &str) -> Result<Self> {
50        Self::with_client(base_url, access_token, Client::new())
51    }
52
53    /// Create a Canvas client with a custom [`reqwest::Client`] (for proxy, TLS config, etc.).
54    pub fn with_client(base_url: &str, access_token: &str, client: Client) -> Result<Self> {
55        let base_url = validate_base_url(base_url)?;
56        let api_url = base_url.join("api/v1/")?;
57        Ok(Self {
58            requester: Arc::new(Requester::new(
59                api_url,
60                access_token.trim().to_string(),
61                client,
62            )),
63        })
64    }
65
66    // -------------------------------------------------------------------------
67    // Courses
68    // -------------------------------------------------------------------------
69
70    /// Fetch a single course by ID.
71    ///
72    /// # Canvas API
73    /// `GET /api/v1/courses/:id`
74    pub async fn get_course(&self, course_id: u64) -> Result<Course> {
75        let mut course: Course = self
76            .requester
77            .get(&format!("courses/{course_id}"), &[])
78            .await?;
79        course.requester = Some(Arc::clone(&self.requester));
80        Ok(course)
81    }
82
83    /// Stream all courses visible to the authenticated user.
84    ///
85    /// # Canvas API
86    /// `GET /api/v1/courses`
87    pub fn get_courses(&self) -> PageStream<Course> {
88        PageStream::new_with_injector(
89            Arc::clone(&self.requester),
90            "courses",
91            vec![],
92            |mut c: Course, req| {
93                c.requester = Some(Arc::clone(&req));
94                c
95            },
96        )
97    }
98
99    /// Create a new course under an account.
100    ///
101    /// # Canvas API
102    /// `POST /api/v1/accounts/:account_id/courses`
103    pub async fn create_course(
104        &self,
105        account_id: u64,
106        params: CreateCourseParams,
107    ) -> Result<Course> {
108        let form = wrap_params("course", &params);
109        let mut course: Course = self
110            .requester
111            .post(&format!("accounts/{account_id}/courses"), &form)
112            .await?;
113        course.requester = Some(Arc::clone(&self.requester));
114        Ok(course)
115    }
116
117    /// Delete a course by ID. Canvas returns the deleted course object.
118    ///
119    /// # Canvas API
120    /// `DELETE /api/v1/courses/:id`
121    pub async fn delete_course(&self, course_id: u64) -> Result<Course> {
122        let params = vec![("event".to_string(), "delete".to_string())];
123        let mut course: Course = self
124            .requester
125            .delete(&format!("courses/{course_id}"), &params)
126            .await?;
127        course.requester = Some(Arc::clone(&self.requester));
128        Ok(course)
129    }
130
131    // -------------------------------------------------------------------------
132    // Users
133    // -------------------------------------------------------------------------
134
135    /// Fetch a single user by ID or `UserId::Current` for the authenticated user.
136    ///
137    /// # Canvas API
138    /// `GET /api/v1/users/:id`
139    pub async fn get_user(&self, user_id: UserId) -> Result<User> {
140        let id = user_id.to_path_segment();
141        let mut user: User = self.requester.get(&format!("users/{id}"), &[]).await?;
142        user.requester = Some(Arc::clone(&self.requester));
143        Ok(user)
144    }
145
146    /// Fetch the currently authenticated user.
147    ///
148    /// # Canvas API
149    /// `GET /api/v1/users/self`
150    pub async fn get_current_user(&self) -> Result<CurrentUser> {
151        self.requester.get("users/self", &[]).await
152    }
153
154    /// Create a new user under an account.
155    ///
156    /// # Canvas API
157    /// `POST /api/v1/accounts/:account_id/users`
158    pub async fn create_user(&self, account_id: u64, params: CreateUserParams) -> Result<User> {
159        let form = wrap_params("user", &params);
160        let mut user: User = self
161            .requester
162            .post(&format!("accounts/{account_id}/users"), &form)
163            .await?;
164        user.requester = Some(Arc::clone(&self.requester));
165        Ok(user)
166    }
167
168    // -------------------------------------------------------------------------
169    // Accounts
170    // -------------------------------------------------------------------------
171
172    /// Fetch a single account by ID.
173    ///
174    /// # Canvas API
175    /// `GET /api/v1/accounts/:id`
176    pub async fn get_account(&self, account_id: u64) -> Result<Account> {
177        let mut account: Account = self
178            .requester
179            .get(&format!("accounts/{account_id}"), &[])
180            .await?;
181        account.requester = Some(Arc::clone(&self.requester));
182        Ok(account)
183    }
184
185    /// Fetch a single outcome by ID.
186    ///
187    /// # Canvas API
188    /// `GET /api/v1/outcomes/:id`
189    pub async fn get_outcome(&self, outcome_id: u64) -> Result<Outcome> {
190        let mut outcome: Outcome = self
191            .requester
192            .get(&format!("outcomes/{outcome_id}"), &[])
193            .await?;
194        outcome.requester = Some(Arc::clone(&self.requester));
195        Ok(outcome)
196    }
197
198    /// Stream all accounts accessible to the authenticated user.
199    ///
200    /// # Canvas API
201    /// `GET /api/v1/accounts`
202    pub fn get_accounts(&self) -> PageStream<Account> {
203        PageStream::new_with_injector(
204            Arc::clone(&self.requester),
205            "accounts",
206            vec![],
207            |mut a: Account, req| {
208                a.requester = Some(Arc::clone(&req));
209                a
210            },
211        )
212    }
213
214    // -------------------------------------------------------------------------
215    // Convenience accessors for existing resources
216    // -------------------------------------------------------------------------
217
218    /// Fetch a single section by ID.
219    ///
220    /// # Canvas API
221    /// `GET /api/v1/sections/:id`
222    pub async fn get_section(&self, section_id: u64) -> Result<Section> {
223        self.requester
224            .get(&format!("sections/{section_id}"), &[])
225            .await
226    }
227
228    /// Fetch a single group by ID.
229    ///
230    /// # Canvas API
231    /// `GET /api/v1/groups/:id`
232    pub async fn get_group(&self, group_id: u64) -> Result<Group> {
233        let mut g: Group = self
234            .requester
235            .get(&format!("groups/{group_id}"), &[])
236            .await?;
237        g.requester = Some(Arc::clone(&self.requester));
238        Ok(g)
239    }
240
241    /// Fetch a single file by ID.
242    ///
243    /// # Canvas API
244    /// `GET /api/v1/files/:id`
245    pub async fn get_file(&self, file_id: u64) -> Result<File> {
246        let mut f: File = self.requester.get(&format!("files/{file_id}"), &[]).await?;
247        f.requester = Some(Arc::clone(&self.requester));
248        Ok(f)
249    }
250
251    /// Fetch a single folder by ID.
252    ///
253    /// # Canvas API
254    /// `GET /api/v1/folders/:id`
255    pub async fn get_folder(&self, folder_id: u64) -> Result<Folder> {
256        let mut folder: Folder = self
257            .requester
258            .get(&format!("folders/{folder_id}"), &[])
259            .await?;
260        folder.requester = Some(Arc::clone(&self.requester));
261        Ok(folder)
262    }
263
264    /// Fetch a single progress object by ID.
265    ///
266    /// # Canvas API
267    /// `GET /api/v1/progress/:id`
268    pub async fn get_progress(&self, progress_id: u64) -> Result<Progress> {
269        let mut p: Progress = self
270            .requester
271            .get(&format!("progress/{progress_id}"), &[])
272            .await?;
273        p.requester = Some(Arc::clone(&self.requester));
274        Ok(p)
275    }
276
277    // -------------------------------------------------------------------------
278    // Conversations
279    // -------------------------------------------------------------------------
280
281    /// Fetch a single conversation by ID.
282    ///
283    /// # Canvas API
284    /// `GET /api/v1/conversations/:id`
285    pub async fn get_conversation(&self, conversation_id: u64) -> Result<Conversation> {
286        let mut c: Conversation = self
287            .requester
288            .get(&format!("conversations/{conversation_id}"), &[])
289            .await?;
290        c.requester = Some(Arc::clone(&self.requester));
291        Ok(c)
292    }
293
294    /// Stream all conversations for the authenticated user.
295    ///
296    /// # Canvas API
297    /// `GET /api/v1/conversations`
298    pub fn get_conversations(&self) -> PageStream<Conversation> {
299        PageStream::new_with_injector(
300            Arc::clone(&self.requester),
301            "conversations",
302            vec![],
303            |mut c: Conversation, req| {
304                c.requester = Some(Arc::clone(&req));
305                c
306            },
307        )
308    }
309
310    /// Create a new conversation.
311    ///
312    /// # Canvas API
313    /// `POST /api/v1/conversations`
314    pub async fn create_conversation(
315        &self,
316        recipients: &[&str],
317        body: &str,
318        params: ConversationParams,
319    ) -> Result<Conversation> {
320        let mut form: Vec<(String, String)> = recipients
321            .iter()
322            .map(|r| ("recipients[]".into(), r.to_string()))
323            .collect();
324        form.push(("body".into(), body.to_string()));
325        if let Some(subject) = params.subject {
326            form.push(("subject".into(), subject));
327        }
328        if let Some(fg) = params.force_new {
329            form.push(("force_new".into(), fg.to_string()));
330        }
331        if let Some(gc) = params.group_conversation {
332            form.push(("group_conversation".into(), gc.to_string()));
333        }
334        if let Some(ctx) = params.context_code {
335            form.push(("context_code".into(), ctx));
336        }
337        // Canvas returns an array; take the first element
338        let result: serde_json::Value = self.requester.post("conversations", &form).await?;
339        let first = result
340            .as_array()
341            .and_then(|a| a.first())
342            .cloned()
343            .unwrap_or(result);
344        let mut c: Conversation = serde_json::from_value(first)?;
345        c.requester = Some(Arc::clone(&self.requester));
346        Ok(c)
347    }
348
349    // -------------------------------------------------------------------------
350    // Calendar Events
351    // -------------------------------------------------------------------------
352
353    /// Fetch a single calendar event by ID.
354    ///
355    /// # Canvas API
356    /// `GET /api/v1/calendar_events/:id`
357    pub async fn get_calendar_event(&self, event_id: u64) -> Result<CalendarEvent> {
358        let mut e: CalendarEvent = self
359            .requester
360            .get(&format!("calendar_events/{event_id}"), &[])
361            .await?;
362        e.requester = Some(Arc::clone(&self.requester));
363        Ok(e)
364    }
365
366    /// Stream all calendar events visible to the authenticated user.
367    ///
368    /// # Canvas API
369    /// `GET /api/v1/calendar_events`
370    pub fn get_calendar_events(&self) -> PageStream<CalendarEvent> {
371        PageStream::new_with_injector(
372            Arc::clone(&self.requester),
373            "calendar_events",
374            vec![],
375            |mut e: CalendarEvent, req| {
376                e.requester = Some(Arc::clone(&req));
377                e
378            },
379        )
380    }
381
382    /// Create a new calendar event.
383    ///
384    /// # Canvas API
385    /// `POST /api/v1/calendar_events`
386    pub async fn create_calendar_event(
387        &self,
388        context_code: &str,
389        params: CalendarEventParams,
390    ) -> Result<CalendarEvent> {
391        let body = serde_json::to_value(&params).unwrap_or_default();
392        let mut form = wrap_params("calendar_event", &body);
393        form.push((
394            "calendar_event[context_code]".into(),
395            context_code.to_string(),
396        ));
397        let mut e: CalendarEvent = self.requester.post("calendar_events", &form).await?;
398        e.requester = Some(Arc::clone(&self.requester));
399        Ok(e)
400    }
401
402    // -------------------------------------------------------------------------
403    // Planner
404    // -------------------------------------------------------------------------
405
406    /// Fetch a single planner note by ID.
407    ///
408    /// # Canvas API
409    /// `GET /api/v1/planner_notes/:id`
410    pub async fn get_planner_note(&self, note_id: u64) -> Result<PlannerNote> {
411        let mut n: PlannerNote = self
412            .requester
413            .get(&format!("planner_notes/{note_id}"), &[])
414            .await?;
415        n.requester = Some(Arc::clone(&self.requester));
416        Ok(n)
417    }
418
419    /// Stream all planner notes for the authenticated user.
420    ///
421    /// # Canvas API
422    /// `GET /api/v1/planner_notes`
423    pub fn get_planner_notes(&self) -> PageStream<PlannerNote> {
424        PageStream::new_with_injector(
425            Arc::clone(&self.requester),
426            "planner_notes",
427            vec![],
428            |mut n: PlannerNote, req| {
429                n.requester = Some(Arc::clone(&req));
430                n
431            },
432        )
433    }
434
435    /// Create a planner note for the authenticated user.
436    ///
437    /// # Canvas API
438    /// `POST /api/v1/planner_notes`
439    pub async fn create_planner_note(&self, params: PlannerNoteParams) -> Result<PlannerNote> {
440        let flat: Vec<(String, String)> = serde_json::to_value(&params)
441            .unwrap_or_default()
442            .as_object()
443            .into_iter()
444            .flatten()
445            .filter_map(|(k, v)| {
446                v.as_str()
447                    .map(|s| (k.clone(), s.to_string()))
448                    .or_else(|| v.as_u64().map(|n| (k.clone(), n.to_string())))
449            })
450            .collect();
451        let mut n: PlannerNote = self.requester.post("planner_notes", &flat).await?;
452        n.requester = Some(Arc::clone(&self.requester));
453        Ok(n)
454    }
455
456    /// Fetch a single planner override by ID.
457    ///
458    /// # Canvas API
459    /// `GET /api/v1/planner/overrides/:id`
460    pub async fn get_planner_override(&self, override_id: u64) -> Result<PlannerOverride> {
461        let mut o: PlannerOverride = self
462            .requester
463            .get(&format!("planner/overrides/{override_id}"), &[])
464            .await?;
465        o.requester = Some(Arc::clone(&self.requester));
466        Ok(o)
467    }
468
469    /// Stream all planner overrides for the authenticated user.
470    ///
471    /// # Canvas API
472    /// `GET /api/v1/planner/overrides`
473    pub fn get_planner_overrides(&self) -> PageStream<PlannerOverride> {
474        PageStream::new_with_injector(
475            Arc::clone(&self.requester),
476            "planner/overrides",
477            vec![],
478            |mut o: PlannerOverride, req| {
479                o.requester = Some(Arc::clone(&req));
480                o
481            },
482        )
483    }
484
485    /// Create a planner override for a specific plannable item.
486    ///
487    /// # Canvas API
488    /// `POST /api/v1/planner/overrides`
489    pub async fn create_planner_override(
490        &self,
491        plannable_type: &str,
492        plannable_id: u64,
493    ) -> Result<PlannerOverride> {
494        let form = vec![
495            ("plannable_type".into(), plannable_type.to_string()),
496            ("plannable_id".into(), plannable_id.to_string()),
497        ];
498        let mut o: PlannerOverride = self.requester.post("planner/overrides", &form).await?;
499        o.requester = Some(Arc::clone(&self.requester));
500        Ok(o)
501    }
502
503    // -------------------------------------------------------------------------
504    // ePortfolios
505    // -------------------------------------------------------------------------
506
507    /// Fetch a single ePortfolio by ID.
508    ///
509    /// # Canvas API
510    /// `GET /api/v1/eportfolios/:id`
511    pub async fn get_eportfolio(&self, eportfolio_id: u64) -> Result<EPortfolio> {
512        let mut p: EPortfolio = self
513            .requester
514            .get(&format!("eportfolios/{eportfolio_id}"), &[])
515            .await?;
516        p.requester = Some(Arc::clone(&self.requester));
517        Ok(p)
518    }
519
520    // -------------------------------------------------------------------------
521    // Appointment Groups
522    // -------------------------------------------------------------------------
523
524    /// Fetch a single appointment group by ID.
525    ///
526    /// # Canvas API
527    /// `GET /api/v1/appointment_groups/:id`
528    pub async fn get_appointment_group(&self, group_id: u64) -> Result<AppointmentGroup> {
529        let mut a: AppointmentGroup = self
530            .requester
531            .get(&format!("appointment_groups/{group_id}"), &[])
532            .await?;
533        a.requester = Some(Arc::clone(&self.requester));
534        Ok(a)
535    }
536
537    /// Stream all appointment groups visible to the authenticated user.
538    ///
539    /// # Canvas API
540    /// `GET /api/v1/appointment_groups`
541    pub fn get_appointment_groups(&self) -> PageStream<AppointmentGroup> {
542        PageStream::new_with_injector(
543            Arc::clone(&self.requester),
544            "appointment_groups",
545            vec![],
546            |mut a: AppointmentGroup, req| {
547                a.requester = Some(Arc::clone(&req));
548                a
549            },
550        )
551    }
552
553    /// Create a new appointment group.
554    ///
555    /// # Canvas API
556    /// `POST /api/v1/appointment_groups`
557    pub async fn create_appointment_group(
558        &self,
559        params: AppointmentGroupParams,
560    ) -> Result<AppointmentGroup> {
561        let body = serde_json::to_value(&params).unwrap_or_default();
562        let form = wrap_params("appointment_group", &body);
563        let mut a: AppointmentGroup = self.requester.post("appointment_groups", &form).await?;
564        a.requester = Some(Arc::clone(&self.requester));
565        Ok(a)
566    }
567
568    // -------------------------------------------------------------------------
569    // GraphQL (feature = "graphql")
570    // -------------------------------------------------------------------------
571
572    /// Return a [`GraphQL`][crate::graphql::GraphQL] client for this Canvas instance.
573    ///
574    /// # Example
575    /// ```no_run
576    /// # #[tokio::main] async fn main() -> canvas_lms_api::Result<()> {
577    /// let canvas = canvas_lms_api::Canvas::new("https://canvas.example.edu", "token")?;
578    /// let gql = canvas.graphql();
579    /// let res = gql.query("{ allCourses { id name } }", None).await?;
580    /// # Ok(()) }
581    /// ```
582    #[cfg(feature = "graphql")]
583    pub fn graphql(&self) -> crate::graphql::GraphQL {
584        crate::graphql::GraphQL {
585            requester: Arc::clone(&self.requester),
586        }
587    }
588
589    // -------------------------------------------------------------------------
590    // JWT
591    // -------------------------------------------------------------------------
592
593    // -------------------------------------------------------------------------
594    // Polls
595    // -------------------------------------------------------------------------
596
597    /// Fetch a single poll by ID.
598    ///
599    /// # Canvas API
600    /// `GET /api/v1/polls/:id`
601    pub async fn get_poll(&self, poll_id: u64) -> Result<Poll> {
602        let val: serde_json::Value = self.requester.get(&format!("polls/{poll_id}"), &[]).await?;
603        let mut poll: Poll = serde_json::from_value(val["polls"][0].clone())?;
604        poll.requester = Some(Arc::clone(&self.requester));
605        Ok(poll)
606    }
607
608    /// Stream all polls for the current user.
609    ///
610    /// # Canvas API
611    /// `GET /api/v1/polls`
612    pub fn get_polls(&self) -> PageStream<Poll> {
613        PageStream::new_with_injector(
614            Arc::clone(&self.requester),
615            "polls",
616            vec![],
617            |mut p: Poll, req| {
618                p.requester = Some(Arc::clone(&req));
619                p
620            },
621        )
622    }
623
624    /// Create a new poll for the current user.
625    ///
626    /// # Canvas API
627    /// `POST /api/v1/polls`
628    pub async fn create_poll(&self, params: CreatePollParams) -> Result<Poll> {
629        let form = wrap_params("polls[]", &params);
630        let val: serde_json::Value = self.requester.post("polls", &form).await?;
631        let mut poll: Poll = serde_json::from_value(val["polls"][0].clone())?;
632        poll.requester = Some(Arc::clone(&self.requester));
633        Ok(poll)
634    }
635
636    // -------------------------------------------------------------------------
637
638    /// Create a short-lived JWT for use with other Canvas services.
639    ///
640    /// # Canvas API
641    /// `POST /api/v1/jwts`
642    pub async fn create_jwt(&self) -> Result<CanvasJwt> {
643        self.requester.post("jwts", &[]).await
644    }
645
646    /// Refresh an existing JWT, returning a new one.
647    ///
648    /// # Canvas API
649    /// `POST /api/v1/jwts/refresh`
650    pub async fn refresh_jwt(&self, token: &str) -> Result<CanvasJwt> {
651        let params = vec![("jwt".into(), token.to_string())];
652        self.requester.post("jwts/refresh", &params).await
653    }
654}
655
656fn validate_base_url(raw: &str) -> Result<Url> {
657    let trimmed = raw.trim().trim_end_matches('/');
658    if trimmed.contains("/api/v1") {
659        return Err(crate::error::CanvasError::ApiError {
660            status: 0,
661            message: "base_url should not include /api/v1".into(),
662        });
663    }
664    Ok(Url::parse(&format!("{trimmed}/"))?)
665}