TheMovieDB 0.1.0

A robust and idiomatic Rust API wrapper for The Movie Database (TMDb) v3 API.
Documentation
use std::collections::HashMap;
use reqwest::{Client, Method, header::{HeaderMap, ACCEPT, CONNECTION, CONTENT_TYPE}};
use serde_json::Value;
use std::fmt;

const API_VERSION: &str = "3";

#[derive(Debug)]
pub enum APIKeyError {
    MissingAPIKey,
}

impl std::error::Error for APIKeyError {}

impl fmt::Display for APIKeyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            APIKeyError::MissingAPIKey => write!(f, "Missing API Key"),
        }
    }
}

pub struct TMDB {
    pub base_uri: String,
    pub session: Option<Client>,
    pub timeout: Option<u64>,
    pub headers: HeaderMap,
    pub base_path: String,
    pub urls: HashMap<String, String>,
}

impl TMDB {
    pub fn new() -> Self {
        let mut headers = HeaderMap::new();
        headers.insert(CONTENT_TYPE, "application/json".parse().unwrap());
        headers.insert(ACCEPT, "application/json".parse().unwrap());
        headers.insert(CONNECTION, "close".parse().unwrap());

        Self {
            base_uri: format!("https://api.themoviedb.org/{}", API_VERSION),
            session: None,
            timeout: None,
            headers,
            base_path: "".to_string(),
            urls: HashMap::new(),
        }
    }

    pub fn _get_path(&self, key: &str) -> String {
        format!("{}{}", self.base_path, self.urls.get(key).unwrap_or(&"".to_string()))
    }

    pub fn _get_complete_url(&self, path: &str) -> String {
        format!("{}/{}", self.base_uri, path)
    }

    pub fn _get_params(&self, params: &mut HashMap<String, String>) -> Result<(), APIKeyError> {
        let api_key = std::env::var("TMDB_API_KEY").map_err(|_| APIKeyError::MissingAPIKey)?;
        params.insert("api_key".to_string(), api_key);
        Ok(())
    }

    pub async fn _request(
        &self,
        method: Method,
        path: &str,
        params: Option<HashMap<String, String>>,
        payload: Option<Value>,
    ) -> Result<Value, Box<dyn std::error::Error + Send + Sync + 'static>> {
        let url = self._get_complete_url(path);
        let mut params = params.unwrap_or_else(HashMap::new);
        self._get_params(&mut params)?;

        let client = self.session.as_ref().unwrap_or(&Client::new()).clone();
        let mut request = client.request(method, &url).headers(self.headers.clone());

        if let Some(timeout) = self.timeout {
            request = request.timeout(std::time::Duration::from_secs(timeout));
        }

        if let Some(payload) = payload {
            request = request.json(&payload);
        }

        let response = request.query(&params).send().await?;
        response.error_for_status_ref()?;
        let json: Value = response.json().await?;
        Ok(json)
    }

    pub async fn _get(&self, path: &str, params: Option<HashMap<String, String>>) -> Result<Value, Box<dyn std::error::Error + Send + Sync + 'static>> {
        self._request(Method::GET, path, params, None).await
    }

    pub async fn _post(
        &self,
        path: &str,
        params: Option<HashMap<String, String>>,
        payload: Option<Value>,
    ) -> Result<Value, Box<dyn std::error::Error + Send + Sync + 'static>> {
        self._request(Method::POST, path, params, payload).await
    }

    pub async fn _delete(
        &self,
        path: &str,
        params: Option<HashMap<String, String>>,
        payload: Option<Value>,
    ) -> Result<Value, Box<dyn std::error::Error + Send + Sync + 'static>> {
        self._request(Method::DELETE, path, params, payload).await
    }
}