open_library/
lib.rs

1use crate::clients::account::AccountClient;
2use crate::clients::author::AuthorClient;
3use crate::clients::works::WorksClient;
4use crate::models::account::Session;
5use clients::books::BooksClient;
6use reqwest::header::{HeaderMap, HeaderValue};
7use reqwest::{ClientBuilder, Error, StatusCode};
8use serde::{Deserialize, Serialize};
9use std::fmt::Debug;
10use thiserror::Error;
11use url::{ParseError, Url};
12
13mod clients;
14mod format;
15pub mod models;
16#[cfg(test)]
17mod tests;
18
19#[derive(Debug, Error)]
20pub enum OpenLibraryError {
21    #[error(
22        "Received an {:?} response from the Open Library API: {:?}",
23        status_code,
24        error
25    )]
26    ApiError {
27        status_code: StatusCode,
28        error: Option<OpenLibraryErrorResponse>,
29    },
30    #[error("Unable to build HTTP client: {}", source)]
31    ClientBuildingError { source: reqwest::Error },
32    #[error("An internal error occurred: {}", reason)]
33    InternalError { reason: String },
34    #[error("An error occurred while parsing json: {}", source)]
35    JsonParseError { source: reqwest::Error },
36    #[error("The operation ({}) requires authentication to be provided!", reason)]
37    NotAuthenticated { reason: String },
38    #[error("An error occurred while trying to parse a value: {}", reason)]
39    ParsingError { reason: String },
40    #[error("An error occurred while sending HTTP request: {}", source)]
41    RequestFailed { source: reqwest::Error },
42}
43
44impl From<reqwest::Error> for OpenLibraryError {
45    fn from(error: Error) -> Self {
46        OpenLibraryError::RequestFailed { source: error }
47    }
48}
49
50impl From<ParseError> for OpenLibraryError {
51    fn from(error: ParseError) -> Self {
52        OpenLibraryError::ParsingError {
53            reason: error.to_string(),
54        }
55    }
56}
57
58#[derive(Debug, Deserialize, Serialize)]
59pub struct OpenLibraryErrorResponse {
60    pub error: String,
61}
62
63pub struct OpenLibraryAuthClient {
64    account: AccountClient,
65}
66
67impl OpenLibraryAuthClient {
68    //TODO: this is odd to consume with having to know to pass in None to get production url
69    pub fn new(host: Option<Url>) -> Result<OpenLibraryAuthClient, OpenLibraryError> {
70        let client = ClientBuilder::new()
71            .build()
72            .map_err(|error| OpenLibraryError::ClientBuildingError { source: error })?;
73
74        let host_url = match host {
75            Some(value) => value,
76            None => Url::parse("https://openlibrary.org/").unwrap(),
77        };
78
79        Ok(Self {
80            account: AccountClient {
81                client,
82                host: host_url,
83            },
84        })
85    }
86
87    pub async fn login(
88        &self,
89        username: String,
90        password: String,
91    ) -> Result<Session, OpenLibraryError> {
92        self.account.login(username, password).await
93    }
94}
95
96#[derive(Clone)]
97pub struct OpenLibraryClient {
98    pub account: AccountClient,
99    pub author: AuthorClient,
100    pub books: BooksClient,
101    pub works: WorksClient,
102}
103
104impl OpenLibraryClient {
105    pub fn builder() -> OpenLibraryClientBuilder {
106        OpenLibraryClientBuilder::new()
107    }
108}
109
110pub struct OpenLibraryClientBuilder {
111    host: Url,
112    session: Option<Session>,
113}
114
115impl OpenLibraryClientBuilder {
116    fn new() -> OpenLibraryClientBuilder {
117        OpenLibraryClientBuilder {
118            host: Url::parse("https://openlibrary.org/").unwrap(),
119            session: None,
120        }
121    }
122
123    pub fn with_host(self, host: Url) -> OpenLibraryClientBuilder {
124        OpenLibraryClientBuilder {
125            host,
126            session: self.session,
127        }
128    }
129
130    pub fn with_session(self, session: &Session) -> OpenLibraryClientBuilder {
131        OpenLibraryClientBuilder {
132            host: self.host,
133            session: Some(session.clone()),
134        }
135    }
136
137    pub fn build(self) -> Result<OpenLibraryClient, OpenLibraryError> {
138        let default_headers = match self.session {
139            Some(session) => {
140                let mut headers = HeaderMap::new();
141                headers.insert(
142                    "Cookie",
143                    HeaderValue::from_str(session.cookie().as_str()).map_err(|_error| {
144                        OpenLibraryError::ParsingError {
145                            reason: "Unable to parse session cookie into header value".to_string(),
146                        }
147                    })?,
148                );
149                headers
150            }
151            None => HeaderMap::new(),
152        };
153
154        let client = ClientBuilder::new()
155            .default_headers(default_headers)
156            .build()
157            .map_err(|error| OpenLibraryError::ClientBuildingError { source: error })?;
158
159        Ok(OpenLibraryClient {
160            books: BooksClient::new(&client, &self.host),
161            account: AccountClient::new(&client, &self.host),
162            author: AuthorClient::new(&client, &self.host),
163            works: WorksClient::new(&client, &self.host),
164        })
165    }
166}