Skip to main content

gor/cmd/
classroom.rs

1//! Implementation of the `gor classroom` subcommand.
2//!
3//! Provides classroom listing and assignment viewing for GitHub Classroom.
4
5#![allow(clippy::print_stdout)]
6
7use crate::cli::ClassroomCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use anyhow::Context;
11
12/// Run the `gor classroom` subcommand.
13///
14/// # Errors
15///
16/// Returns an error if the command execution fails.
17pub fn run(cmd: ClassroomCommand) -> anyhow::Result<()> {
18    match cmd {
19        ClassroomCommand::List { json, hostname } => list(json, hostname.as_deref()),
20        ClassroomCommand::Assignments { id, json, hostname } => {
21            assignments(id, json, hostname.as_deref())
22        }
23    }
24}
25
26/// List classrooms for the authenticated user.
27///
28/// # Errors
29///
30/// Returns an error if the API request fails.
31fn list(json: Option<Vec<String>>, hostname: Option<&str>) -> anyhow::Result<()> {
32    let host = hostname.unwrap_or("github.com");
33    let client = Client::new(host).context("failed to create HTTP client")?;
34
35    let response = client
36        .get("/classrooms")
37        .context("failed to fetch classrooms")?;
38
39    let status = response.status();
40    if !status.is_success() {
41        anyhow::bail!("failed to list classrooms: HTTP {status}");
42    }
43
44    let classrooms: Vec<serde_json::Value> = response.json().context("failed to parse response")?;
45
46    if let Some(fields) = json {
47        let fields_ref: Option<&[String]> = if fields.is_empty() {
48            None
49        } else {
50            Some(&fields)
51        };
52        print_json(&classrooms, fields_ref);
53        return Ok(());
54    }
55
56    if classrooms.is_empty() {
57        println!("No classrooms found.");
58        return Ok(());
59    }
60
61    println!("{:<8}  {:<30}  ORGANIZATION", "ID", "NAME");
62    for c in &classrooms {
63        let id = c["id"].as_u64().unwrap_or(0);
64        let name = c["name"].as_str().unwrap_or("—");
65        let org = c["organization"]["login"].as_str().unwrap_or("—");
66        let name_truncated = crate::cmd::util::truncate(name, 30);
67        println!("{id:<8}  {name_truncated:<30}  {org}");
68    }
69
70    Ok(())
71}
72
73/// List assignments for a classroom.
74///
75/// # Errors
76///
77/// Returns an error if the API request fails.
78fn assignments(id: u64, json: Option<Vec<String>>, hostname: Option<&str>) -> anyhow::Result<()> {
79    let host = hostname.unwrap_or("github.com");
80    let client = Client::new(host).context("failed to create HTTP client")?;
81
82    let path = format!("/classrooms/{id}/assignments");
83    let response = client.get(&path).context("failed to fetch assignments")?;
84
85    let status = response.status();
86    if !status.is_success() {
87        anyhow::bail!("failed to list assignments: HTTP {status}");
88    }
89
90    let assignments: Vec<serde_json::Value> =
91        response.json().context("failed to parse response")?;
92
93    if let Some(fields) = json {
94        let fields_ref: Option<&[String]> = if fields.is_empty() {
95            None
96        } else {
97            Some(&fields)
98        };
99        print_json(&assignments, fields_ref);
100        return Ok(());
101    }
102
103    if assignments.is_empty() {
104        println!("No assignments found for classroom {id}.");
105        return Ok(());
106    }
107
108    println!("{:<8}  {:<30}  {:<20}  INVITED", "ID", "TITLE", "DEADLINE");
109    for a in &assignments {
110        let aid = a["id"].as_u64().unwrap_or(0);
111        let title = a["title"].as_str().unwrap_or("—");
112        let deadline = a["deadline"].as_str().unwrap_or("—");
113        let invited = a["invitations_count"].as_u64().unwrap_or(0);
114        let title_truncated = crate::cmd::util::truncate(title, 30);
115        println!("{aid:<8}  {title_truncated:<30}  {deadline:<20}  {invited}");
116    }
117
118    Ok(())
119}