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}