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        progress::Progress,
21        section::Section,
22        user::{CurrentUser, User, UserId},
23    },
24};
25use reqwest::Client;
26use std::sync::Arc;
27use url::Url;
28
29/// The Canvas LMS API client. All interaction starts here.
30///
31/// # Example
32/// ```no_run
33/// # #[tokio::main] async fn main() -> canvas_lms_api::Result<()> {
34/// let canvas = canvas_lms_api::Canvas::new("https://canvas.example.edu", "my-token")?;
35/// let course = canvas.get_course(1).await?;
36/// println!("{}", course.name.unwrap_or_default());
37/// # Ok(()) }
38/// ```
39pub struct Canvas {
40    pub(crate) requester: Arc<Requester>,
41}
42
43impl Canvas {
44    /// Create a new Canvas client.
45    ///
46    /// `base_url` should be the institution root (e.g. `https://canvas.example.edu`),
47    /// not including `/api/v1`.
48    pub fn new(base_url: &str, access_token: &str) -> Result<Self> {
49        Self::with_client(base_url, access_token, Client::new())
50    }
51
52    /// Create a Canvas client with a custom [`reqwest::Client`] (for proxy, TLS config, etc.).
53    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    // -------------------------------------------------------------------------
66    // Courses
67    // -------------------------------------------------------------------------
68
69    /// Fetch a single course by ID.
70    ///
71    /// # Canvas API
72    /// `GET /api/v1/courses/:id`
73    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    /// Stream all courses visible to the authenticated user.
83    ///
84    /// # Canvas API
85    /// `GET /api/v1/courses`
86    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    /// Create a new course under an account.
99    ///
100    /// # Canvas API
101    /// `POST /api/v1/accounts/:account_id/courses`
102    pub async fn create_course(
103        &self,
104        account_id: u64,
105        params: CreateCourseParams,
106    ) -> Result<Course> {
107        let form = wrap_params("course", &params);
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    /// Delete a course by ID. Canvas returns the deleted course object.
117    ///
118    /// # Canvas API
119    /// `DELETE /api/v1/courses/:id`
120    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}"), &params)
125            .await?;
126        course.requester = Some(Arc::clone(&self.requester));
127        Ok(course)
128    }
129
130    // -------------------------------------------------------------------------
131    // Users
132    // -------------------------------------------------------------------------
133
134    /// Fetch a single user by ID or `UserId::Current` for the authenticated user.
135    ///
136    /// # Canvas API
137    /// `GET /api/v1/users/:id`
138    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    /// Fetch the currently authenticated user.
146    ///
147    /// # Canvas API
148    /// `GET /api/v1/users/self`
149    pub async fn get_current_user(&self) -> Result<CurrentUser> {
150        self.requester.get("users/self", &[]).await
151    }
152
153    /// Create a new user under an account.
154    ///
155    /// # Canvas API
156    /// `POST /api/v1/accounts/:account_id/users`
157    pub async fn create_user(&self, account_id: u64, params: CreateUserParams) -> Result<User> {
158        let form = wrap_params("user", &params);
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    // -------------------------------------------------------------------------
168    // Accounts
169    // -------------------------------------------------------------------------
170
171    /// Fetch a single account by ID.
172    ///
173    /// # Canvas API
174    /// `GET /api/v1/accounts/:id`
175    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    /// Fetch a single outcome by ID.
185    ///
186    /// # Canvas API
187    /// `GET /api/v1/outcomes/:id`
188    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    /// Stream all accounts accessible to the authenticated user.
198    ///
199    /// # Canvas API
200    /// `GET /api/v1/accounts`
201    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    // -------------------------------------------------------------------------
214    // Convenience accessors for existing resources
215    // -------------------------------------------------------------------------
216
217    /// Fetch a single section by ID.
218    ///
219    /// # Canvas API
220    /// `GET /api/v1/sections/:id`
221    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    /// Fetch a single group by ID.
228    ///
229    /// # Canvas API
230    /// `GET /api/v1/groups/:id`
231    pub async fn get_group(&self, group_id: u64) -> Result<Group> {
232        self.requester.get(&format!("groups/{group_id}"), &[]).await
233    }
234
235    /// Fetch a single file by ID.
236    ///
237    /// # Canvas API
238    /// `GET /api/v1/files/:id`
239    pub async fn get_file(&self, file_id: u64) -> Result<File> {
240        self.requester.get(&format!("files/{file_id}"), &[]).await
241    }
242
243    /// Fetch a single folder by ID.
244    ///
245    /// # Canvas API
246    /// `GET /api/v1/folders/:id`
247    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    /// Fetch a single progress object by ID.
254    ///
255    /// # Canvas API
256    /// `GET /api/v1/progress/:id`
257    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    // -------------------------------------------------------------------------
264    // Conversations
265    // -------------------------------------------------------------------------
266
267    /// Fetch a single conversation by ID.
268    ///
269    /// # Canvas API
270    /// `GET /api/v1/conversations/:id`
271    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    /// Stream all conversations for the authenticated user.
281    ///
282    /// # Canvas API
283    /// `GET /api/v1/conversations`
284    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    /// Create a new conversation.
297    ///
298    /// # Canvas API
299    /// `POST /api/v1/conversations`
300    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        // Canvas returns an array; take the first element
324        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    // -------------------------------------------------------------------------
336    // Calendar Events
337    // -------------------------------------------------------------------------
338
339    /// Fetch a single calendar event by ID.
340    ///
341    /// # Canvas API
342    /// `GET /api/v1/calendar_events/:id`
343    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    /// Stream all calendar events visible to the authenticated user.
353    ///
354    /// # Canvas API
355    /// `GET /api/v1/calendar_events`
356    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    /// Create a new calendar event.
369    ///
370    /// # Canvas API
371    /// `POST /api/v1/calendar_events`
372    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(&params).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    // -------------------------------------------------------------------------
389    // Planner
390    // -------------------------------------------------------------------------
391
392    /// Fetch a single planner note by ID.
393    ///
394    /// # Canvas API
395    /// `GET /api/v1/planner_notes/:id`
396    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    /// Stream all planner notes for the authenticated user.
406    ///
407    /// # Canvas API
408    /// `GET /api/v1/planner_notes`
409    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    /// Create a planner note for the authenticated user.
422    ///
423    /// # Canvas API
424    /// `POST /api/v1/planner_notes`
425    pub async fn create_planner_note(&self, params: PlannerNoteParams) -> Result<PlannerNote> {
426        let flat: Vec<(String, String)> = serde_json::to_value(&params)
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    /// Fetch a single planner override by ID.
443    ///
444    /// # Canvas API
445    /// `GET /api/v1/planner/overrides/:id`
446    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    /// Stream all planner overrides for the authenticated user.
456    ///
457    /// # Canvas API
458    /// `GET /api/v1/planner/overrides`
459    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    /// Create a planner override for a specific plannable item.
472    ///
473    /// # Canvas API
474    /// `POST /api/v1/planner/overrides`
475    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    // -------------------------------------------------------------------------
490    // ePortfolios
491    // -------------------------------------------------------------------------
492
493    /// Fetch a single ePortfolio by ID.
494    ///
495    /// # Canvas API
496    /// `GET /api/v1/eportfolios/:id`
497    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    // -------------------------------------------------------------------------
507    // Appointment Groups
508    // -------------------------------------------------------------------------
509
510    /// Fetch a single appointment group by ID.
511    ///
512    /// # Canvas API
513    /// `GET /api/v1/appointment_groups/:id`
514    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    /// Stream all appointment groups visible to the authenticated user.
524    ///
525    /// # Canvas API
526    /// `GET /api/v1/appointment_groups`
527    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    /// Create a new appointment group.
540    ///
541    /// # Canvas API
542    /// `POST /api/v1/appointment_groups`
543    pub async fn create_appointment_group(
544        &self,
545        params: AppointmentGroupParams,
546    ) -> Result<AppointmentGroup> {
547        let body = serde_json::to_value(&params).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    // -------------------------------------------------------------------------
555    // GraphQL (feature = "graphql")
556    // -------------------------------------------------------------------------
557
558    /// Return a [`GraphQL`][crate::graphql::GraphQL] client for this Canvas instance.
559    ///
560    /// # Example
561    /// ```no_run
562    /// # #[tokio::main] async fn main() -> canvas_lms_api::Result<()> {
563    /// let canvas = canvas_lms_api::Canvas::new("https://canvas.example.edu", "token")?;
564    /// let gql = canvas.graphql();
565    /// let res = gql.query("{ allCourses { id name } }", None).await?;
566    /// # Ok(()) }
567    /// ```
568    #[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    // -------------------------------------------------------------------------
576    // JWT
577    // -------------------------------------------------------------------------
578
579    /// Create a short-lived JWT for use with other Canvas services.
580    ///
581    /// # Canvas API
582    /// `POST /api/v1/jwts`
583    pub async fn create_jwt(&self) -> Result<CanvasJwt> {
584        self.requester.post("jwts", &[]).await
585    }
586
587    /// Refresh an existing JWT, returning a new one.
588    ///
589    /// # Canvas API
590    /// `POST /api/v1/jwts/refresh`
591    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", &params).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}