fars/
client.rs

1//! Provides an internal API client for the Firebase Auth.
2//!
3//! See also [API reference](https://firebase.google.com/docs/reference/rest/auth).
4//!
5//! ## Examples
6//! ```rust
7//! use fars::Client;
8//!
9//! // Create a client.
10//! let client = Client::new();
11//! ```
12//!
13//! ## Custom HTTP client
14//! You can use a custom HTTP client by enabling the `custom_client` feature with the re-exported `reqwest` crate.
15//!
16//! ```rust
17//! use fars::Client;
18//! use std::time::Duration;
19//!
20//! // Create a custom reqwest client with timeout.
21//! let client = fars::reqwest::ClientBuilder::new()
22//!     .timeout(Duration::from_secs(60))
23//!     .connect_timeout(Duration::from_secs(10))
24//!     .build()?;
25//!
26//! // Customize HTTP client.
27//! let client = Client::custom(client);
28//! ```
29
30use serde::{de::DeserializeOwned, Serialize};
31
32use crate::error::{ApiErrorResponse, CommonErrorCode};
33use crate::ApiKey;
34use crate::Endpoint;
35use crate::Error;
36use crate::LanguageCode;
37use crate::Result;
38
39/// HTTP client.
40#[derive(Clone, Debug)]
41pub struct Client {
42    inner: reqwest::Client,
43}
44
45impl Default for Client {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl Client {
52    /// Creates a new HTTP client.
53    pub fn new() -> Self {
54        Self {
55            inner: reqwest::Client::new(),
56        }
57    }
58
59    /// Creates a new HTTP client with a custom instance.
60    ///
61    /// ## NOTE
62    /// This method requires the `custom_client` feature.
63    ///
64    /// ## Arguments
65    /// - `client` - A custom HTTP client instance.
66    ///
67    /// ## Example
68    /// ```
69    /// use fars::Client;
70    /// use std::time::Duration;
71    ///
72    /// // Create a custom reqwest client with timeout.
73    /// let client = fars::reqwest::ClientBuilder::new()
74    ///     .timeout(Duration::from_secs(60))
75    ///     .connect_timeout(Duration::from_secs(10))
76    ///     .build()?;
77    ///
78    /// // Customize HTTP client.
79    /// let client = Client::custom(client);
80    /// ```
81    #[cfg(feature = "custom_client")]
82    pub fn custom(client: crate::reqwest::Client) -> Self {
83        Self {
84            inner: client,
85        }
86    }
87
88    /// Returns a reference to the inner HTTP client.
89    #[allow(dead_code)]
90    pub(crate) fn inner(&self) -> &reqwest::Client {
91        &self.inner
92    }
93
94    /// Sends a POST request to the Firebase Auth API.
95    ///
96    /// See also [API reference](https://firebase.google.com/docs/reference/rest/auth).
97    ///
98    /// ## Arguments
99    /// - `endpoint` - The endpoint to send the request to.
100    /// - `api_key` - The Firebase project's API key.
101    /// - `request_payload` - The request body payload.
102    /// - `locale` - The BCP 47 language code, eg: en-US.
103    ///
104    /// ## Returns
105    /// The result with the response payload of the API.
106    ///
107    /// ## Errors
108    /// - `Error::HttpRequestError` - Failed to send a request.
109    /// - `Error::ReadResponseTextFailed` - Failed to read the response body as text.
110    /// - `Error::DeserializeResponseJsonFailed` - Failed to deserialize the response body as JSON.
111    /// - `Error::DeserializeErrorResponseJsonFailed` - Failed to deserialize the error response body as JSON.
112    /// - `Error::InvalidIdToken` - Invalid ID token.
113    /// - `Error::ApiError` - API error on the Firebase Auth.
114    pub(crate) async fn send_post<T, U>(
115        &self,
116        endpoint: Endpoint,
117        api_key: &ApiKey,
118        request_payload: T,
119        locale: Option<LanguageCode>,
120    ) -> Result<U>
121    where
122        T: Serialize,
123        U: DeserializeOwned,
124    {
125        // Build a request URL.
126        let url = format!(
127            "https://identitytoolkit.googleapis.com/v1/{}?key={}",
128            endpoint.format(),
129            api_key.inner()
130        );
131
132        // Create request builder and set method and payload.
133        let mut builder = self
134            .inner
135            .post(url)
136            .json(&request_payload);
137
138        // Set optional headers if some are provided.
139        if let Some(locale) = locale {
140            builder = builder.headers(optional_locale_header(locale)?);
141        }
142
143        // Send a request.
144        let response = builder
145            .send()
146            .await
147            .map_err(Error::HttpRequestError)?;
148
149        // Check the response status code.
150        let status_code = response.status();
151
152        // Read the response body as text.
153        let response_text = response
154            .text()
155            .await
156            .map_err(|error| Error::ReadResponseTextFailed {
157                error,
158            })?;
159
160        // Successful response.
161        if status_code.is_success() {
162            // Deserialize the response text to a payload.
163            serde_json::from_str::<U>(&response_text).map_err(|error| {
164                Error::DeserializeResponseJsonFailed {
165                    error,
166                    json: response_text,
167                }
168            })
169        }
170        // Error response.
171        else {
172            // Deserialize the response text to the error payload.
173            let error_response =
174                serde_json::from_str::<ApiErrorResponse>(&response_text)
175                    .map_err(|error| {
176                        Error::DeserializeErrorResponseJsonFailed {
177                            error,
178                            json: response_text,
179                        }
180                    })?;
181
182            // Check error message and create error code.
183            let error_code: CommonErrorCode = error_response
184                .error
185                .message
186                .clone()
187                .into();
188
189            match error_code {
190                // Take invalid ID token error as special case.
191                | CommonErrorCode::InvalidIdToken => Err(Error::InvalidIdToken),
192                | _ => Err(Error::ApiError {
193                    status_code,
194                    error_code,
195                    response: error_response,
196                }),
197            }
198        }
199    }
200}
201
202/// Creates optional headers for the locale.
203///
204/// ## Arguments
205/// - `locale` - The BCP 47 language code, eg: en-US.
206///
207/// ## Returns
208/// Optional headers for the locale if some locale is provided.
209///
210/// ## Errors
211/// - `Error::InvalidHeaderValue` - Invalid header value.
212fn optional_locale_header(
213    locale: LanguageCode
214) -> Result<reqwest::header::HeaderMap> {
215    let mut headers = reqwest::header::HeaderMap::new();
216
217    headers.insert(
218        "X-Firebase-Locale",
219        reqwest::header::HeaderValue::from_str(locale.format()).map_err(
220            |error| Error::InvalidHeaderValue {
221                key: "X-Firebase-Locale",
222                error,
223            },
224        )?,
225    );
226
227    Ok(headers)
228}