twitch_highway 0.4.1

Twitch API
Documentation
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()
        )
    }
}