firebase_admin_sdk/remote_config/
mod.rs1pub mod models;
11#[cfg(test)]
12mod tests;
13
14use crate::core::middleware::AuthMiddleware;
15use crate::remote_config::models::RemoteConfig;
16use reqwest::Client;
17use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
18use reqwest_retry::policies::ExponentialBackoff;
19use reqwest_retry::RetryTransientMiddleware;
20use url::Url;
21
22pub struct FirebaseRemoteConfig {
24 client: ClientWithMiddleware,
25 base_url: String,
26}
27
28const REMOTE_CONFIG_V1_API: &str =
29 "https://firebaseremoteconfig.googleapis.com/v1/projects/{project_id}/remoteConfig";
30
31#[derive(Debug, serde::Deserialize)]
32struct ApiError {
33 code: u16,
34 message: String,
35 status: String,
36}
37
38#[derive(Debug, serde::Deserialize)]
39struct ErrorWrapper {
40 error: ApiError,
41}
42
43#[derive(Debug, thiserror::Error)]
45pub enum Error {
46 #[error("the service account key is missing the project_id")]
48 ProjectIdMissing,
49 #[error("an error occurred while sending the request: {0}")]
51 Request(#[from] reqwest_middleware::Error),
52 #[error("an error occurred while sending the request: {0}")]
54 Reqwest(#[from] reqwest::Error),
55 #[error("an error occurred while serializing/deserializing JSON: {0}")]
57 Json(#[from] serde_json::Error),
58 #[error("the firebase API returned an error: {code} {status}: {message}")]
60 Api {
61 code: u16,
62 message: String,
63 status: String,
64 },
65}
66
67impl FirebaseRemoteConfig {
68 pub fn new(middleware: AuthMiddleware) -> Self {
72 let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
73
74 let client = ClientBuilder::new(Client::new())
75 .with(RetryTransientMiddleware::new_with_policy(retry_policy))
76 .with(middleware.clone())
77 .build();
78
79 let project_id = middleware.key.project_id.clone().unwrap_or_default();
80 let base_url = REMOTE_CONFIG_V1_API.replace("{project_id}", &project_id);
81
82 Self { client, base_url }
83 }
84
85 #[allow(dead_code)]
88 pub(crate) fn new_with_client(client: ClientWithMiddleware, base_url: String) -> Self {
89 Self { client, base_url }
90 }
91
92 async fn process_response<T: serde::de::DeserializeOwned>(
93 &self,
94 response: reqwest::Response,
95 ) -> Result<T, Error> {
96 if response.status().is_success() {
97 Ok(response.json().await?)
98 } else {
99 let error: ErrorWrapper = response.json().await?;
100 Err(Error::Api {
101 code: error.error.code,
102 message: error.error.message,
103 status: error.error.status,
104 })
105 }
106 }
107
108 async fn request<T: serde::de::DeserializeOwned>(
109 &self,
110 req: reqwest_middleware::RequestBuilder,
111 ) -> Result<(T, Option<String>), Error> {
112 let response = req.send().await?;
113 if !response.status().is_success() {
114 let error: ErrorWrapper = response.json().await?;
115 return Err(Error::Api {
116 code: error.error.code,
117 message: error.error.message,
118 status: error.error.status,
119 });
120 }
121 let etag = response
122 .headers()
123 .get("ETag")
124 .and_then(|v| v.to_str().ok())
125 .map(|s| s.to_string());
126 let body: T = response.json().await?;
127 Ok((body, etag))
128 }
129
130 pub async fn get(&self) -> Result<RemoteConfig, Error> {
134 let req = self.client.get(&self.base_url);
135 let (mut config, etag) = self.request::<RemoteConfig>(req).await?;
136 if let Some(e) = etag {
137 config.etag = e;
138 }
139 Ok(config)
140 }
141
142 pub async fn publish(&self, config: RemoteConfig) -> Result<RemoteConfig, Error> {
151 let req = self
152 .client
153 .put(&self.base_url)
154 .header("If-Match", config.etag.clone())
155 .json(&config);
156 let (mut config, etag) = self.request::<RemoteConfig>(req).await?;
157 if let Some(e) = etag {
158 config.etag = e;
159 }
160 Ok(config)
161 }
162
163 pub async fn list_versions(
169 &self,
170 options: Option<models::ListVersionsOptions>,
171 ) -> Result<models::ListVersionsResult, Error> {
172 let url = format!("{}/versions", self.base_url);
173 let mut url_obj = Url::parse(&url).map_err(|e| Error::Api {
174 code: 0,
175 message: e.to_string(),
176 status: "INTERNAL".to_string(),
177 })?;
178
179 if let Some(opts) = options {
180 let mut query_pairs = url_obj.query_pairs_mut();
181 if let Some(size) = opts.page_size {
182 query_pairs.append_pair("pageSize", &size.to_string());
183 }
184 if let Some(token) = opts.page_token {
185 query_pairs.append_pair("pageToken", &token);
186 }
187 }
188
189 let response = self.client.get(url_obj).send().await?;
190 self.process_response(response).await
191 }
192
193 pub async fn rollback(&self, version_number: String) -> Result<RemoteConfig, Error> {
199 let url = format!("{}:rollback", self.base_url);
200 let body = models::RollbackRequest { version_number };
201
202 let req = self.client.post(url).json(&body);
203 let (mut config, etag) = self.request::<RemoteConfig>(req).await?;
204 if let Some(e) = etag {
205 config.etag = e;
206 }
207 Ok(config)
208 }
209}