#![deny(missing_docs)]
use model::{
thread::{CourseThreads, ThreadResponse},
user::SelfUser,
};
use reqwest::RequestBuilder;
use serde::{Deserialize, Serialize};
pub mod model;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("error deserializing json: {0}")]
Json(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, self::Error>;
#[derive(Clone, Debug)]
pub struct Client {
http: reqwest::Client,
base_url: String,
token: String,
user_agent: String,
}
type EmptyParams = &'static [((), ())];
#[derive(Clone, Debug, Default)]
pub struct ClientOptions {
pub http: Option<reqwest::Client>,
pub base_url: Option<String>,
pub user_agent: Option<String>,
}
impl Client {
pub fn new(token: &str) -> Self {
Self::new_with_opts(token, ClientOptions::default())
}
pub fn new_with_opts(token: &str, options: ClientOptions) -> Self {
Self {
http: options.http.unwrap_or_else(|| reqwest::Client::new()),
base_url: options
.base_url
.unwrap_or(String::from("https://us.edstem.org")),
token: String::from(token),
user_agent: options.user_agent.unwrap_or(String::from("edstem-rust")),
}
}
async fn request<T>(&self, request: RequestBuilder) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
let built = request
.header("Authorization", format!("Bearer {}", self.token))
.header("User-Agent", &self.user_agent)
.build()?;
let response = self.http.execute(built).await?;
Ok(response.json().await?)
}
async fn get<T>(
&self,
endpoint: &str,
parameters: Option<&[(impl Serialize, impl Serialize)]>,
) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
let mut builder = self.http.get(format!("{}{}", self.base_url, endpoint));
if let Some(params) = parameters {
builder = builder.query(params);
};
Ok(self.request(builder).await?)
}
pub async fn get_self_user(&self) -> Result<SelfUser> {
Ok(self.get("/api/user", None::<EmptyParams>).await?)
}
pub async fn get_course_threads(&self, id: impl Into<u64>) -> Result<CourseThreads> {
let endpoint = format!("/api/courses/{}/threads", id.into());
Ok(self.get(&*endpoint, None::<EmptyParams>).await?)
}
pub async fn get_thread(&self, id: impl Into<u64>) -> Result<ThreadResponse> {
let endpoint = format!("/api/threads/{}", id.into());
Ok(self.get(&*endpoint, None::<EmptyParams>).await?)
}
pub async fn get_thread_by_number(
&self,
course_id: impl Into<u64>,
thread_number: u64,
) -> Result<ThreadResponse> {
let endpoint = format!(
"/api/courses/{}/threads/{}",
course_id.into(),
thread_number
);
Ok(self.get(&*endpoint, None::<EmptyParams>).await?)
}
}