1use std::str::FromStr;
2
3use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5use clap_complete::Shell;
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8use tracing::info;
9
10mod config;
11mod file;
12mod submission;
13
14pub use config::Config;
15pub use file::FileSubmission;
16pub use submission::Submission;
17
18#[derive(Serialize, Deserialize, Clone)]
20pub struct AccessToken(String);
21
22impl std::fmt::Debug for AccessToken {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "AccessToken")
25 }
26}
27
28impl AccessToken {
29 pub fn secret(&self) -> &str {
30 &self.0
31 }
32}
33
34#[derive(Parser, Clone, Debug)]
35#[command(version, about, long_about = None)]
36pub struct CLI {
37 #[arg(long)]
40 pub access_token: Option<String>,
41
42 #[arg(long, short)]
45 pub course_id: Option<u64>,
46
47 #[arg(long, short)]
50 pub base_url: Option<String>,
51
52 #[arg(long)]
54 generate: Option<Shell>,
55
56 #[command(subcommand)]
57 pub command: Command,
58
59 pub assignment_id: u64,
61}
62
63#[derive(Subcommand, Clone, Debug)]
64pub enum Command {
65 Debug,
67 #[command(subcommand)]
69 Submissions(SubmissionState),
70 Grade,
72 #[command(subcommand)]
74 Count(SubmissionState),
75}
76
77#[derive(Subcommand, Clone, Debug)]
78pub enum SubmissionState {
79 Unsubmitted,
80 Submitted,
81 Ungraded,
82 Graded,
83}
84
85impl SubmissionState {
86 pub fn predicate(&self) -> fn(&Submission) -> bool {
87 match self {
88 SubmissionState::Unsubmitted => Submission::unsubmitted,
89 SubmissionState::Submitted => Submission::submitted,
90 SubmissionState::Ungraded => Submission::ungraded,
91 SubmissionState::Graded => Submission::graded,
92 }
93 }
94}
95
96impl Default for SubmissionState {
97 fn default() -> Self {
98 Self::Ungraded
99 }
100}
101
102#[derive(Debug)]
103pub struct Grade {
104 pub user_id: u64,
105 pub grade: f32,
106}
107
108impl FromStr for Grade {
109 type Err = anyhow::Error;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 let mut parts = s.split(": ");
113 let user_id = parts
114 .next()
115 .context("Unable to parse user id from stdin.")?;
116 let grade = parts.next().context("Unable to parse grade from stdin.")?;
117
118 Ok(Self {
119 user_id: user_id.parse().context("Unable to parse user id to u64")?,
120 grade: grade.parse().context("Unable to parse grade to f32")?,
121 })
122 }
123}
124
125#[derive(Debug)]
126pub struct Comment {
127 pub user_id: u64,
128 pub comment: String,
129}
130
131impl FromStr for Comment {
132 type Err = anyhow::Error;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
135 let (user_id, comment) = s
136 .split_once(": ")
137 .context("Unable to parse comment line.")?;
138
139 Ok(Self {
140 user_id: user_id.parse().context("Unable to parse user id to u64")?,
141 comment: comment.to_string(),
142 })
143 }
144}
145
146pub fn create_client(auth_token: AccessToken) -> Result<Client> {
147 info!("Building application reqwest client...");
148 info!("Setting auth header...");
149 let mut auth_bearer: reqwest::header::HeaderValue = ("Bearer ".to_owned()
150 + auth_token.secret())
151 .try_into()
152 .unwrap();
153 auth_bearer.set_sensitive(true);
154 info!("Auth header set!");
155
156 let mut headers = reqwest::header::HeaderMap::new();
157 headers.insert(reqwest::header::AUTHORIZATION, auth_bearer);
158 headers.insert("per_page", 100.into());
159
160 Ok(reqwest::ClientBuilder::new()
161 .default_headers(headers)
162 .build()?)
163}