1use std::{collections::HashMap, fs::File, io::prelude::Write, sync::Arc};
6
7use error_support::{convert_log_report_error, handle_error};
8
9pub mod cache;
10pub mod client;
11pub mod config;
12pub mod context;
13pub mod error;
14pub mod schema;
15pub mod service;
16#[cfg(feature = "signatures")]
17pub(crate) mod signatures;
18pub mod storage;
19
20pub(crate) mod jexl_filter;
21mod macros;
22
23pub use client::{Attachment, RemoteSettingsRecord, RemoteSettingsResponse, RsJsonObject};
24pub use config::{BaseUrl, RemoteSettingsConfig, RemoteSettingsConfig2, RemoteSettingsServer};
25pub use context::RemoteSettingsContext;
26pub use error::{trace, ApiResult, RemoteSettingsError, Result};
27
28use client::Client;
29use error::Error;
30use storage::Storage;
31
32uniffi::setup_scaffolding!("remote_settings");
33
34#[derive(uniffi::Object)]
39pub struct RemoteSettingsService {
40 internal: service::RemoteSettingsService,
42}
43
44#[uniffi::export]
45impl RemoteSettingsService {
46 #[uniffi::constructor]
57 pub fn new(storage_dir: String, config: RemoteSettingsConfig2) -> Self {
58 Self {
59 internal: service::RemoteSettingsService::new(storage_dir, config),
60 }
61 }
62
63 pub fn make_client(&self, collection_name: String) -> Arc<RemoteSettingsClient> {
67 self.internal.make_client(collection_name)
68 }
69
70 #[handle_error(Error)]
72 pub fn sync(&self) -> ApiResult<Vec<String>> {
73 self.internal.sync()
74 }
75
76 #[handle_error(Error)]
84 pub fn update_config(&self, config: RemoteSettingsConfig2) -> ApiResult<()> {
85 self.internal.update_config(config)
86 }
87
88 pub fn client_url(&self) -> String {
89 self.internal.client_url().to_string()
90 }
91}
92
93#[derive(uniffi::Object)]
97pub struct RemoteSettingsClient {
98 internal: client::RemoteSettingsClient,
100}
101
102#[uniffi::export]
103impl RemoteSettingsClient {
104 pub fn collection_name(&self) -> String {
106 self.internal.collection_name().to_owned()
107 }
108
109 #[uniffi::method(default(sync_if_empty = false))]
126 pub fn get_records(&self, sync_if_empty: bool) -> Option<Vec<RemoteSettingsRecord>> {
127 match self.internal.get_records(sync_if_empty) {
128 Ok(records) => records,
129 Err(e) => {
130 trace!("get_records error: {e}");
132 convert_log_report_error(e);
133 None
136 }
137 }
138 }
139
140 #[uniffi::method(default(sync_if_empty = false))]
145 pub fn get_records_map(
146 &self,
147 sync_if_empty: bool,
148 ) -> Option<HashMap<String, RemoteSettingsRecord>> {
149 self.get_records(sync_if_empty)
150 .map(|records| records.into_iter().map(|r| (r.id.clone(), r)).collect())
151 }
152
153 #[handle_error(Error)]
163 pub fn get_attachment(&self, record: &RemoteSettingsRecord) -> ApiResult<Vec<u8>> {
164 self.internal.get_attachment(record)
165 }
166
167 #[handle_error(Error)]
168 pub fn sync(&self) -> ApiResult<()> {
169 self.internal.sync()
170 }
171
172 pub fn shutdown(&self) {
174 self.internal.shutdown()
175 }
176}
177
178impl RemoteSettingsClient {
179 fn new(
182 base_url: BaseUrl,
183 bucket_name: String,
184 collection_name: String,
185 #[allow(unused)] context: Option<RemoteSettingsContext>,
186 storage: Storage,
187 ) -> Self {
188 Self {
189 internal: client::RemoteSettingsClient::new(
190 base_url,
191 bucket_name,
192 collection_name,
193 context,
194 storage,
195 ),
196 }
197 }
198}
199
200#[derive(uniffi::Object)]
201pub struct RemoteSettings {
202 pub config: RemoteSettingsConfig,
203 client: Client,
204}
205
206#[uniffi::export]
207impl RemoteSettings {
208 #[uniffi::constructor]
210 #[handle_error(Error)]
211 pub fn new(remote_settings_config: RemoteSettingsConfig) -> ApiResult<Self> {
212 Ok(RemoteSettings {
213 config: remote_settings_config.clone(),
214 client: Client::new(remote_settings_config)?,
215 })
216 }
217
218 #[handle_error(Error)]
220 pub fn get_records(&self) -> ApiResult<RemoteSettingsResponse> {
221 let resp = self.client.get_records()?;
222 Ok(resp)
223 }
224
225 #[handle_error(Error)]
228 pub fn get_records_since(&self, timestamp: u64) -> ApiResult<RemoteSettingsResponse> {
229 let resp = self.client.get_records_since(timestamp)?;
230 Ok(resp)
231 }
232
233 #[handle_error(Error)]
235 pub fn download_attachment_to_path(
236 &self,
237 attachment_id: String,
238 path: String,
239 ) -> ApiResult<()> {
240 let resp = self.client.get_attachment(&attachment_id)?;
241 let mut file = File::create(path).map_err(Error::AttachmentFileError)?;
242 file.write_all(&resp).map_err(Error::AttachmentFileError)?;
243 Ok(())
244 }
245}
246
247impl RemoteSettings {
252 #[handle_error(Error)]
256 pub fn get_records_raw(&self) -> ApiResult<viaduct::Response> {
257 self.client.get_records_raw()
258 }
259
260 #[handle_error(Error)]
264 pub fn get_attachment(&self, attachment_location: &str) -> ApiResult<Vec<u8>> {
265 self.client.get_attachment(attachment_location)
266 }
267}
268
269#[cfg(test)]
270mod test {
271 use super::*;
272 use crate::RemoteSettingsRecord;
273 use mockito::{mock, Matcher};
274
275 #[test]
276 fn test_get_records() {
277 viaduct_dev::init_backend_dev();
278 let m = mock(
279 "GET",
280 "/v1/buckets/the-bucket/collections/the-collection/records",
281 )
282 .with_body(response_body())
283 .with_status(200)
284 .with_header("content-type", "application/json")
285 .with_header("etag", "\"1000\"")
286 .create();
287
288 let config = RemoteSettingsConfig {
289 server: Some(RemoteSettingsServer::Custom {
290 url: mockito::server_url(),
291 }),
292 server_url: None,
293 bucket_name: Some(String::from("the-bucket")),
294 collection_name: String::from("the-collection"),
295 };
296 let remote_settings = RemoteSettings::new(config).unwrap();
297
298 let resp = remote_settings.get_records().unwrap();
299
300 assert!(are_equal_json(JPG_ATTACHMENT, &resp.records[0]));
301 assert_eq!(1000, resp.last_modified);
302 m.expect(1).assert();
303 }
304
305 #[test]
306 fn test_get_records_since() {
307 viaduct_dev::init_backend_dev();
308 let m = mock(
309 "GET",
310 "/v1/buckets/the-bucket/collections/the-collection/records",
311 )
312 .match_query(Matcher::UrlEncoded("gt_last_modified".into(), "500".into()))
313 .with_body(response_body())
314 .with_status(200)
315 .with_header("content-type", "application/json")
316 .with_header("etag", "\"1000\"")
317 .create();
318
319 let config = RemoteSettingsConfig {
320 server: Some(RemoteSettingsServer::Custom {
321 url: mockito::server_url(),
322 }),
323 server_url: None,
324 bucket_name: Some(String::from("the-bucket")),
325 collection_name: String::from("the-collection"),
326 };
327 let remote_settings = RemoteSettings::new(config).unwrap();
328
329 let resp = remote_settings.get_records_since(500).unwrap();
330 assert!(are_equal_json(JPG_ATTACHMENT, &resp.records[0]));
331 assert_eq!(1000, resp.last_modified);
332 m.expect(1).assert();
333 }
334
335 #[allow(dead_code)]
341 fn test_download() {
342 viaduct_dev::init_backend_dev();
343 let config = RemoteSettingsConfig {
344 server: Some(RemoteSettingsServer::Custom {
345 url: "http://localhost:8888".into(),
346 }),
347 server_url: None,
348 bucket_name: Some(String::from("the-bucket")),
349 collection_name: String::from("the-collection"),
350 };
351 let remote_settings = RemoteSettings::new(config).unwrap();
352
353 remote_settings
354 .download_attachment_to_path(
355 "d3a5eccc-f0ca-42c3-b0bb-c0d4408c21c9.jpg".to_string(),
356 "test.jpg".to_string(),
357 )
358 .unwrap();
359 }
360
361 fn are_equal_json(str: &str, rec: &RemoteSettingsRecord) -> bool {
362 let r1: RemoteSettingsRecord = serde_json::from_str(str).unwrap();
363 &r1 == rec
364 }
365
366 fn response_body() -> String {
367 format!(
368 r#"
369 {{
370 "data": [
371 {},
372 {},
373 {}
374 ]
375 }}"#,
376 JPG_ATTACHMENT, PDF_ATTACHMENT, NO_ATTACHMENT
377 )
378 }
379
380 const JPG_ATTACHMENT: &str = r#"
381 {
382 "title": "jpg-attachment",
383 "content": "content",
384 "attachment": {
385 "filename": "jgp-attachment.jpg",
386 "location": "the-bucket/the-collection/d3a5eccc-f0ca-42c3-b0bb-c0d4408c21c9.jpg",
387 "hash": "2cbd593f3fd5f1585f92265433a6696a863bc98726f03e7222135ff0d8e83543",
388 "mimetype": "image/jpeg",
389 "size": 1374325
390 },
391 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d7",
392 "schema": 1677694447771,
393 "last_modified": 1677694949407
394 }
395 "#;
396
397 const PDF_ATTACHMENT: &str = r#"
398 {
399 "title": "with-attachment",
400 "content": "content",
401 "attachment": {
402 "filename": "pdf-attachment.pdf",
403 "location": "the-bucket/the-collection/5f7347c2-af92-411d-a65b-f794f9b5084c.pdf",
404 "hash": "de1cde3571ef3faa77ea0493276de9231acaa6f6651602e93aa1036f51181e9b",
405 "mimetype": "application/pdf",
406 "size": 157
407 },
408 "id": "ff301910-6bf5-4cfe-bc4c-5c80308661a5",
409 "schema": 1677694447771,
410 "last_modified": 1677694470354
411 }
412 "#;
413
414 const NO_ATTACHMENT: &str = r#"
415 {
416 "title": "no-attachment",
417 "content": "content",
418 "schema": 1677694447771,
419 "id": "7403c6f9-79be-4e0c-a37a-8f2b5bd7ad58",
420 "last_modified": 1677694455368
421 }
422 "#;
423}