Skip to main content

googlebooks_rs/
lib.rs

1use crate::{
2    errors::{AppError, DeserializeJsonSnafu, HttpSnafu},
3    models::{GoogleApiError, VolumeResponse},
4    queries::VolumeQuery,
5};
6use snafu::prelude::*;
7
8pub mod errors;
9pub mod models;
10pub mod queries;
11
12/// Base URL for Google Books API
13const GOOGLE_BOOKS_BASE_URL: &str = "https://www.googleapis.com";
14
15/// Main client for interacting with Google Books API
16#[derive(Clone)]
17pub struct GoogleBooks {
18    pub client: reqwest::Client,
19    pub api_key: Option<String>,
20}
21
22impl Default for GoogleBooks {
23    fn default() -> Self {
24        Self::new(None)
25    }
26}
27
28impl GoogleBooks {
29    /// Creates a new GoogleBooks client instance
30    pub fn new(api_key: Option<String>) -> Self {
31        Self {
32            client: reqwest::Client::new(),
33            api_key,
34        }
35    }
36
37    /// Searches for books using a query builder
38    ///
39    /// # Example
40    /// ```no_run
41    /// use googlebooks_rs::{GoogleBooks, queries::VolumeQuery};
42    ///
43    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
44    /// let client = GoogleBooks::new(Some("api_key".to_string()));
45    /// let query = VolumeQuery::new("Rust programming");
46    /// let response = client.search(query).await?;
47    /// # Ok(())
48    /// # }
49    /// ```
50    pub async fn search(&self, query: VolumeQuery) -> Result<VolumeResponse, AppError> {
51        let response = reqwest::get(query.build_url(GOOGLE_BOOKS_BASE_URL, self.api_key.clone()))
52            .await
53            .context(HttpSnafu)?;
54
55        if !response.status().is_success() {
56            let error_body: GoogleApiError = response.json().await.context(DeserializeJsonSnafu)?;
57
58            if error_body.error.code == 429 {
59                return Err(AppError::RateLimitExceeded {
60                    message: error_body.error.message,
61                });
62            }
63
64            return Err(AppError::GoogleApi {
65                code: error_body.error.code,
66                message: error_body.error.message,
67                reason: error_body
68                    .error
69                    .errors
70                    .and_then(|e| e.first().map(|i| i.reason.clone())),
71            });
72        }
73
74        let result = response
75            .json::<VolumeResponse>()
76            .await
77            .context(DeserializeJsonSnafu)?;
78        Ok(result)
79    }
80
81    /// Fetches a specific book by its volume ID
82    ///
83    /// # Example
84    /// ```no_run
85    /// use googlebooks_rs::GoogleBooks;
86    ///
87    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
88    ///   let response = GoogleBooks::search_by_id("zyTCAlFPjgYC").await?;
89    /// # Ok(())
90    /// # }
91    /// ```
92    pub async fn search_by_id(id: impl Into<String>) -> Result<VolumeResponse, AppError> {
93        let response = reqwest::get(&format!(
94            "{}/books/v1/volumes/{}",
95            GOOGLE_BOOKS_BASE_URL,
96            id.into()
97        ))
98        .await
99        .context(HttpSnafu)?;
100
101        if !response.status().is_success() {
102            let error_body: GoogleApiError = response.json().await.context(DeserializeJsonSnafu)?;
103
104            if error_body.error.code == 429 {
105                return Err(AppError::RateLimitExceeded {
106                    message: error_body.error.message,
107                });
108            }
109
110            return Err(AppError::GoogleApi {
111                code: error_body.error.code,
112                message: error_body.error.message,
113                reason: error_body
114                    .error
115                    .errors
116                    .and_then(|e| e.first().map(|i| i.reason.clone())),
117            });
118        }
119
120        let result = response
121            .json::<VolumeResponse>()
122            .await
123            .context(DeserializeJsonSnafu)?;
124
125        Ok(result)
126    }
127}