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        self.requester.get(&format!("files/{file_id}"), &[]).await
247    }
248
249    /// Fetch a single folder by ID.
250    ///
251    /// # Canvas API
252    /// `GET /api/v1/folders/:id`
253    pub async fn get_folder(&self, folder_id: u64) -> Result<Folder> {
254        self.requester
255            .get(&format!("folders/{folder_id}"), &[])
256            .await
257    }
258
259    /// Fetch a single progress object by ID.
260    ///
261    /// # Canvas API
262    /// `GET /api/v1/progress/:id`
263    pub async fn get_progress(&self, progress_id: u64) -> Result<Progress> {
264        self.requester
265            .get(&format!("progress/{progress_id}"), &[])
266            .await
267    }
268
269    // -------------------------------------------------------------------------
270    // Conversations
271    // -------------------------------------------------------------------------
272
273    /// Fetch a single conversation by ID.
274    ///
275    /// # Canvas API
276    /// `GET /api/v1/conversations/:id`
277    pub async fn get_conversation(&self, conversation_id: u64) -> Result<Conversation> {
278        let mut c: Conversation = self
279            .requester
280            .get(&format!("conversations/{conversation_id}"), &[])
281            .await?;
282        c.requester = Some(Arc::clone(&self.requester));
283        Ok(c)
284    }
285
286    /// Stream all conversations for the authenticated user.
287    ///
288    /// # Canvas API
289    /// `GET /api/v1/conversations`
290    pub fn get_conversations(&self) -> PageStream<Conversation> {
291        PageStream::new_with_injector(
292            Arc::clone(&self.requester),
293            "conversations",
294            vec![],
295            |mut c: Conversation, req| {
296                c.requester = Some(Arc::clone(&req));
297                c
298            },
299        )
300    }
301
302    /// Create a new conversation.
303    ///
304    /// # Canvas API
305    /// `POST /api/v1/conversations`
306    pub async fn create_conversation(
307        &self,
308        recipients: &[&str],
309        body: &str,
310        params: ConversationParams,
311    ) -> Result<Conversation> {
312        let mut form: Vec<(String, String)> = recipients
313            .iter()
314            .map(|r| ("recipients[]".into(), r.to_string()))
315            .collect();
316        form.push(("body".into(), body.to_string()));
317        if let Some(subject) = params.subject {
318            form.push(("subject".into(), subject));
319        }
320        if let Some(fg) = params.force_new {
321            form.push(("force_new".into(), fg.to_string()));
322        }
323        if let Some(gc) = params.group_conversation {
324            form.push(("group_conversation".into(), gc.to_string()));
325        }
326        if let Some(ctx) = params.context_code {
327            form.push(("context_code".into(), ctx));
328        }
329        // Canvas returns an array; take the first element
330        let result: serde_json::Value = self.requester.post("conversations", &form).await?;
331        let first = result
332            .as_array()
333            .and_then(|a| a.first())
334            .cloned()
335            .unwrap_or(result);
336        let mut c: Conversation = serde_json::from_value(first)?;
337        c.requester = Some(Arc::clone(&self.requester));
338        Ok(c)
339    }
340
341    // -------------------------------------------------------------------------
342    // Calendar Events
343    // -------------------------------------------------------------------------
344
345    /// Fetch a single calendar event by ID.
346    ///
347    /// # Canvas API
348    /// `GET /api/v1/calendar_events/:id`
349    pub async fn get_calendar_event(&self, event_id: u64) -> Result<CalendarEvent> {
350        let mut e: CalendarEvent = self
351            .requester
352            .get(&format!("calendar_events/{event_id}"), &[])
353            .await?;
354        e.requester = Some(Arc::clone(&self.requester));
355        Ok(e)
356    }
357
358    /// Stream all calendar events visible to the authenticated user.
359    ///
360    /// # Canvas API
361    /// `GET /api/v1/calendar_events`
362    pub fn get_calendar_events(&self) -> PageStream<CalendarEvent> {
363        PageStream::new_with_injector(
364            Arc::clone(&self.requester),
365            "calendar_events",
366            vec![],
367            |mut e: CalendarEvent, req| {
368                e.requester = Some(Arc::clone(&req));
369                e
370            },
371        )
372    }
373
374    /// Create a new calendar event.
375    ///
376    /// # Canvas API
377    /// `POST /api/v1/calendar_events`
378    pub async fn create_calendar_event(
379        &self,
380        context_code: &str,
381        params: CalendarEventParams,
382    ) -> Result<CalendarEvent> {
383        let body = serde_json::to_value(&params).unwrap_or_default();
384        let mut form = wrap_params("calendar_event", &body);
385        form.push((
386            "calendar_event[context_code]".into(),
387            context_code.to_string(),
388        ));
389        let mut e: CalendarEvent = self.requester.post("calendar_events", &form).await?;
390        e.requester = Some(Arc::clone(&self.requester));
391        Ok(e)
392    }
393
394    // -------------------------------------------------------------------------
395    // Planner
396    // -------------------------------------------------------------------------
397
398    /// Fetch a single planner note by ID.
399    ///
400    /// # Canvas API
401    /// `GET /api/v1/planner_notes/:id`
402    pub async fn get_planner_note(&self, note_id: u64) -> Result<PlannerNote> {
403        let mut n: PlannerNote = self
404            .requester
405            .get(&format!("planner_notes/{note_id}"), &[])
406            .await?;
407        n.requester = Some(Arc::clone(&self.requester));
408        Ok(n)
409    }
410
411    /// Stream all planner notes for the authenticated user.
412    ///
413    /// # Canvas API
414    /// `GET /api/v1/planner_notes`
415    pub fn get_planner_notes(&self) -> PageStream<PlannerNote> {
416        PageStream::new_with_injector(
417            Arc::clone(&self.requester),
418            "planner_notes",
419            vec![],
420            |mut n: PlannerNote, req| {
421                n.requester = Some(Arc::clone(&req));
422                n
423            },
424        )
425    }
426
427    /// Create a planner note for the authenticated user.
428    ///
429    /// # Canvas API
430    /// `POST /api/v1/planner_notes`
431    pub async fn create_planner_note(&self, params: PlannerNoteParams) -> Result<PlannerNote> {
432        let flat: Vec<(String, String)> = serde_json::to_value(&params)
433            .unwrap_or_default()
434            .as_object()
435            .into_iter()
436            .flatten()
437            .filter_map(|(k, v)| {
438                v.as_str()
439                    .map(|s| (k.clone(), s.to_string()))
440                    .or_else(|| v.as_u64().map(|n| (k.clone(), n.to_string())))
441            })
442            .collect();
443        let mut n: PlannerNote = self.requester.post("planner_notes", &flat).await?;
444        n.requester = Some(Arc::clone(&self.requester));
445        Ok(n)
446    }
447
448    /// Fetch a single planner override by ID.
449    ///
450    /// # Canvas API
451    /// `GET /api/v1/planner/overrides/:id`
452    pub async fn get_planner_override(&self, override_id: u64) -> Result<PlannerOverride> {
453        let mut o: PlannerOverride = self
454            .requester
455            .get(&format!("planner/overrides/{override_id}"), &[])
456            .await?;
457        o.requester = Some(Arc::clone(&self.requester));
458        Ok(o)
459    }
460
461    /// Stream all planner overrides for the authenticated user.
462    ///
463    /// # Canvas API
464    /// `GET /api/v1/planner/overrides`
465    pub fn get_planner_overrides(&self) -> PageStream<PlannerOverride> {
466        PageStream::new_with_injector(
467            Arc::clone(&self.requester),
468            "planner/overrides",
469            vec![],
470            |mut o: PlannerOverride, req| {
471                o.requester = Some(Arc::clone(&req));
472                o
473            },
474        )
475    }
476
477    /// Create a planner override for a specific plannable item.
478    ///
479    /// # Canvas API
480    /// `POST /api/v1/planner/overrides`
481    pub async fn create_planner_override(
482        &self,
483        plannable_type: &str,
484        plannable_id: u64,
485    ) -> Result<PlannerOverride> {
486        let form = vec![
487            ("plannable_type".into(), plannable_type.to_string()),
488            ("plannable_id".into(), plannable_id.to_string()),
489        ];
490        let mut o: PlannerOverride = self.requester.post("planner/overrides", &form).await?;
491        o.requester = Some(Arc::clone(&self.requester));
492        Ok(o)
493    }
494
495    // -------------------------------------------------------------------------
496    // ePortfolios
497    // -------------------------------------------------------------------------
498
499    /// Fetch a single ePortfolio by ID.
500    ///
501    /// # Canvas API
502    /// `GET /api/v1/eportfolios/:id`
503    pub async fn get_eportfolio(&self, eportfolio_id: u64) -> Result<EPortfolio> {
504        let mut p: EPortfolio = self
505            .requester
506            .get(&format!("eportfolios/{eportfolio_id}"), &[])
507            .await?;
508        p.requester = Some(Arc::clone(&self.requester));
509        Ok(p)
510    }
511
512    // -------------------------------------------------------------------------
513    // Appointment Groups
514    // -------------------------------------------------------------------------
515
516    /// Fetch a single appointment group by ID.
517    ///
518    /// # Canvas API
519    /// `GET /api/v1/appointment_groups/:id`
520    pub async fn get_appointment_group(&self, group_id: u64) -> Result<AppointmentGroup> {
521        let mut a: AppointmentGroup = self
522            .requester
523            .get(&format!("appointment_groups/{group_id}"), &[])
524            .await?;
525        a.requester = Some(Arc::clone(&self.requester));
526        Ok(a)
527    }
528
529    /// Stream all appointment groups visible to the authenticated user.
530    ///
531    /// # Canvas API
532    /// `GET /api/v1/appointment_groups`
533    pub fn get_appointment_groups(&self) -> PageStream<AppointmentGroup> {
534        PageStream::new_with_injector(
535            Arc::clone(&self.requester),
536            "appointment_groups",
537            vec![],
538            |mut a: AppointmentGroup, req| {
539                a.requester = Some(Arc::clone(&req));
540                a
541            },
542        )
543    }
544
545    /// Create a new appointment group.
546    ///
547    /// # Canvas API
548    /// `POST /api/v1/appointment_groups`
549    pub async fn create_appointment_group(
550        &self,
551        params: AppointmentGroupParams,
552    ) -> Result<AppointmentGroup> {
553        let body = serde_json::to_value(&params).unwrap_or_default();
554        let form = wrap_params("appointment_group", &body);
555        let mut a: AppointmentGroup = self.requester.post("appointment_groups", &form).await?;
556        a.requester = Some(Arc::clone(&self.requester));
557        Ok(a)
558    }
559
560    // -------------------------------------------------------------------------
561    // GraphQL (feature = "graphql")
562    // -------------------------------------------------------------------------
563
564    /// Return a [`GraphQL`][crate::graphql::GraphQL] client for this Canvas instance.
565    ///
566    /// # Example
567    /// ```no_run
568    /// # #[tokio::main] async fn main() -> canvas_lms_api::Result<()> {
569    /// let canvas = canvas_lms_api::Canvas::new("https://canvas.example.edu", "token")?;
570    /// let gql = canvas.graphql();
571    /// let res = gql.query("{ allCourses { id name } }", None).await?;
572    /// # Ok(()) }
573    /// ```
574    #[cfg(feature = "graphql")]
575    pub fn graphql(&self) -> crate::graphql::GraphQL {
576        crate::graphql::GraphQL {
577            requester: Arc::clone(&self.requester),
578        }
579    }
580
581    // -------------------------------------------------------------------------
582    // JWT
583    // -------------------------------------------------------------------------
584
585    // -------------------------------------------------------------------------
586    // Polls
587    // -------------------------------------------------------------------------
588
589    /// Fetch a single poll by ID.
590    ///
591    /// # Canvas API
592    /// `GET /api/v1/polls/:id`
593    pub async fn get_poll(&self, poll_id: u64) -> Result<Poll> {
594        let val: serde_json::Value = self.requester.get(&format!("polls/{poll_id}"), &[]).await?;
595        let mut poll: Poll = serde_json::from_value(val["polls"][0].clone())?;
596        poll.requester = Some(Arc::clone(&self.requester));
597        Ok(poll)
598    }
599
600    /// Stream all polls for the current user.
601    ///
602    /// # Canvas API
603    /// `GET /api/v1/polls`
604    pub fn get_polls(&self) -> PageStream<Poll> {
605        PageStream::new_with_injector(
606            Arc::clone(&self.requester),
607            "polls",
608            vec![],
609            |mut p: Poll, req| {
610                p.requester = Some(Arc::clone(&req));
611                p
612            },
613        )
614    }
615
616    /// Create a new poll for the current user.
617    ///
618    /// # Canvas API
619    /// `POST /api/v1/polls`
620    pub async fn create_poll(&self, params: CreatePollParams) -> Result<Poll> {
621        let form = wrap_params("polls[]", &params);
622        let val: serde_json::Value = self.requester.post("polls", &form).await?;
623        let mut poll: Poll = serde_json::from_value(val["polls"][0].clone())?;
624        poll.requester = Some(Arc::clone(&self.requester));
625        Ok(poll)
626    }
627
628    // -------------------------------------------------------------------------
629
630    /// Create a short-lived JWT for use with other Canvas services.
631    ///
632    /// # Canvas API
633    /// `POST /api/v1/jwts`
634    pub async fn create_jwt(&self) -> Result<CanvasJwt> {
635        self.requester.post("jwts", &[]).await
636    }
637
638    /// Refresh an existing JWT, returning a new one.
639    ///
640    /// # Canvas API
641    /// `POST /api/v1/jwts/refresh`
642    pub async fn refresh_jwt(&self, token: &str) -> Result<CanvasJwt> {
643        let params = vec![("jwt".into(), token.to_string())];
644        self.requester.post("jwts/refresh", &params).await
645    }
646}
647
648fn validate_base_url(raw: &str) -> Result<Url> {
649    let trimmed = raw.trim().trim_end_matches('/');
650    if trimmed.contains("/api/v1") {
651        return Err(crate::error::CanvasError::ApiError {
652            status: 0,
653            message: "base_url should not include /api/v1".into(),
654        });
655    }
656    Ok(Url::parse(&format!("{trimmed}/"))?)
657}