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