open-library 0.7.1

A client to interact with the Open Library API
Documentation
use crate::clients::account::AccountClient;
use crate::clients::author::AuthorClient;
use crate::clients::works::WorksClient;
use crate::models::account::Session;
use clients::books::BooksClient;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{ClientBuilder, Error, StatusCode};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use thiserror::Error;
use url::{ParseError, Url};

mod clients;
mod format;
pub mod models;
#[cfg(test)]
mod tests;

#[derive(Debug, Error)]
pub enum OpenLibraryError {
    #[error(
        "Received an {:?} response from the Open Library API: {:?}",
        status_code,
        error
    )]
    ApiError {
        status_code: StatusCode,
        error: Option<OpenLibraryErrorResponse>,
    },
    #[error("Unable to build HTTP client: {}", source)]
    ClientBuildingError { source: reqwest::Error },
    #[error("An internal error occurred: {}", reason)]
    InternalError { reason: String },
    #[error("An error occurred while parsing json: {}", source)]
    JsonParseError { source: reqwest::Error },
    #[error("The operation ({}) requires authentication to be provided!", reason)]
    NotAuthenticated { reason: String },
    #[error("An error occurred while trying to parse a value: {}", reason)]
    ParsingError { reason: String },
    #[error("An error occurred while sending HTTP request: {}", source)]
    RequestFailed { source: reqwest::Error },
}

impl From<reqwest::Error> for OpenLibraryError {
    fn from(error: Error) -> Self {
        OpenLibraryError::RequestFailed { source: error }
    }
}

impl From<ParseError> for OpenLibraryError {
    fn from(error: ParseError) -> Self {
        OpenLibraryError::ParsingError {
            reason: error.to_string(),
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct OpenLibraryErrorResponse {
    pub error: String,
}

pub struct OpenLibraryAuthClient {
    account: AccountClient,
}

impl OpenLibraryAuthClient {
    //TODO: this is odd to consume with having to know to pass in None to get production url
    pub fn new(host: Option<Url>) -> Result<OpenLibraryAuthClient, OpenLibraryError> {
        let client = ClientBuilder::new()
            .build()
            .map_err(|error| OpenLibraryError::ClientBuildingError { source: error })?;

        let host_url = match host {
            Some(value) => value,
            None => Url::parse("https://openlibrary.org/").unwrap(),
        };

        Ok(Self {
            account: AccountClient {
                client,
                host: host_url,
            },
        })
    }

    pub async fn login(
        &self,
        username: String,
        password: String,
    ) -> Result<Session, OpenLibraryError> {
        self.account.login(username, password).await
    }
}

#[derive(Clone)]
pub struct OpenLibraryClient {
    pub account: AccountClient,
    pub author: AuthorClient,
    pub books: BooksClient,
    pub works: WorksClient,
}

impl OpenLibraryClient {
    pub fn builder() -> OpenLibraryClientBuilder {
        OpenLibraryClientBuilder::new()
    }
}

pub struct OpenLibraryClientBuilder {
    host: Url,
    session: Option<Session>,
}

impl OpenLibraryClientBuilder {
    fn new() -> OpenLibraryClientBuilder {
        OpenLibraryClientBuilder {
            host: Url::parse("https://openlibrary.org/").unwrap(),
            session: None,
        }
    }

    pub fn with_host(self, host: Url) -> OpenLibraryClientBuilder {
        OpenLibraryClientBuilder {
            host,
            session: self.session,
        }
    }

    pub fn with_session(self, session: &Session) -> OpenLibraryClientBuilder {
        OpenLibraryClientBuilder {
            host: self.host,
            session: Some(session.clone()),
        }
    }

    pub fn build(self) -> Result<OpenLibraryClient, OpenLibraryError> {
        let default_headers = match self.session {
            Some(session) => {
                let mut headers = HeaderMap::new();
                headers.insert(
                    "Cookie",
                    HeaderValue::from_str(session.cookie().as_str()).map_err(|_error| {
                        OpenLibraryError::ParsingError {
                            reason: "Unable to parse session cookie into header value".to_string(),
                        }
                    })?,
                );
                headers
            }
            None => HeaderMap::new(),
        };

        let client = ClientBuilder::new()
            .default_headers(default_headers)
            .build()
            .map_err(|error| OpenLibraryError::ClientBuildingError { source: error })?;

        Ok(OpenLibraryClient {
            books: BooksClient::new(&client, &self.host),
            account: AccountClient::new(&client, &self.host),
            author: AuthorClient::new(&client, &self.host),
            works: WorksClient::new(&client, &self.host),
        })
    }
}