mod endpoint_type;
mod no_content;
pub use endpoint_type::{EndpointType, TokenType};
pub use no_content::NoContent;
use std::{any, fmt, marker::PhantomData};
use asknothingx2_util::api::{HeaderMap, HeaderMut, IntoRequestBuilder, Method, StatusCode};
use reqwest::{Client, RequestBuilder};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use url::Url;
use crate::{error, Error};
#[derive(Debug)]
pub struct TwitchAPIRequest<ResBody> {
kind: EndpointType,
url: Url,
method: Method,
headers: HeaderMap,
body: Option<String>,
client: Client,
_phantom: PhantomData<ResBody>,
}
impl<ResBody> TwitchAPIRequest<ResBody>
where
ResBody: DeserializeOwned,
{
pub fn new(
kind: EndpointType,
url: Url,
method: Method,
headers: HeaderMap,
body: Option<String>,
client: Client,
) -> Self {
Self {
kind,
headers,
method,
url,
body,
client,
_phantom: PhantomData,
}
}
pub fn kind(&self) -> &EndpointType {
&self.kind
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn headers_mut(&mut self) -> HeaderMut<'_> {
HeaderMut::new(&mut self.headers)
}
pub fn method(&self) -> &Method {
&self.method
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn body(&self) -> &Option<String> {
&self.body
}
pub async fn send(self) -> Result<reqwest::Response, Error> {
let Self {
kind: _,
url,
method,
headers,
body,
client,
_phantom,
} = self;
let mut client = client.request(method, url).headers(headers);
if let Some(body) = body {
client = client.body(body);
}
let resp = client.send().await.map_err(Error::from)?;
if !resp.status().is_success() {
let status = resp.status();
match resp.text().await {
Ok(body) => {
return Err(error::api_error(format!("HTTP {status}: {body}")));
}
Err(e) => {
return Err(error::api_error(format!(
"HTTP {status} - Failed to read error response: {e}"
)));
}
}
}
Ok(resp)
}
pub async fn text(self) -> Result<String, Error> {
self.send().await?.text().await.map_err(error::decode_error)
}
pub async fn json(self) -> Result<ResBody, crate::Error> {
let response = self.send().await?;
if response.status() == StatusCode::NO_CONTENT {
match serde_json::from_str("{}") {
Ok(result) => return Ok(result),
Err(_) => {
return Err(error::api_error(
format!("Received 204 No Content but type {} cannot be deserialized from empty response",
any::type_name::<ResBody>())
));
}
}
}
response.json().await.map_err(error::decode_error)
}
}
impl<ResBody> IntoRequestBuilder for TwitchAPIRequest<ResBody>
where
ResBody: DeserializeOwned,
{
type Error = crate::Error;
fn into_request_builder(self, client: &Client) -> Result<RequestBuilder, Self::Error> {
let mut client = client.request(self.method, self.url).headers(self.headers);
if let Some(body) = self.body {
client = client.body(body);
}
Ok(client)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct APIError {
#[serde(with = "http_serde::status_code")]
status: StatusCode,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
impl APIError {
pub fn status(&self) -> StatusCode {
self.status
}
pub fn message(&self) -> &str {
&self.message
}
pub fn error_details(&self) -> Option<&str> {
self.error.as_deref()
}
}
impl fmt::Display for APIError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"API error ({}): {}{}",
self.status,
self.message,
self.error
.as_ref()
.map(|e| format!(" - {}", e))
.unwrap_or_default()
)
}
}