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)]
75 pub fn sync(&self) -> ApiResult<Vec<String>> {
76 self.internal.sync()
77 }
78
79 #[handle_error(Error)]
87 pub fn update_config(&self, config: RemoteSettingsConfig2) -> ApiResult<()> {
88 self.internal.update_config(config)
89 }
90
91 pub fn client_url(&self) -> String {
92 self.internal.client_url().to_string()
93 }
94}
95
96#[derive(uniffi::Object)]
100pub struct RemoteSettingsClient {
101 internal: client::RemoteSettingsClient,
103}
104
105#[uniffi::export]
106impl RemoteSettingsClient {
107 pub fn collection_name(&self) -> String {
109 self.internal.collection_name().to_owned()
110 }
111
112 #[uniffi::method(default(sync_if_empty = false))]
129 pub fn get_records(&self, sync_if_empty: bool) -> Option<Vec<RemoteSettingsRecord>> {
130 match self.internal.get_records(sync_if_empty) {
131 Ok(records) => records,
132 Err(e) => {
133 trace!("get_records error: {e}");
135 convert_log_report_error(e);
136 None
139 }
140 }
141 }
142
143 #[uniffi::method(default(sync_if_empty = false))]
148 pub fn get_records_map(
149 &self,
150 sync_if_empty: bool,
151 ) -> Option<HashMap<String, RemoteSettingsRecord>> {
152 self.get_records(sync_if_empty)
153 .map(|records| records.into_iter().map(|r| (r.id.clone(), r)).collect())
154 }
155
156 #[handle_error(Error)]
166 pub fn get_attachment(&self, record: &RemoteSettingsRecord) -> ApiResult<Vec<u8>> {
167 self.internal.get_attachment(record)
168 }
169
170 #[handle_error(Error)]
171 pub fn sync(&self) -> ApiResult<()> {
172 self.internal.sync()
173 }
174
175 #[handle_error(Error)]
176 pub fn reset_storage(&self) -> ApiResult<()> {
177 self.internal.reset_storage()
178 }
179
180 pub fn shutdown(&self) {
182 self.internal.shutdown()
183 }
184}
185
186impl RemoteSettingsClient {
187 fn new(
190 base_url: BaseUrl,
191 bucket_name: String,
192 collection_name: String,
193 #[allow(unused)] context: Option<RemoteSettingsContext>,
194 storage: Storage,
195 ) -> Self {
196 Self {
197 internal: client::RemoteSettingsClient::new(
198 base_url,
199 bucket_name,
200 collection_name,
201 context,
202 storage,
203 ),
204 }
205 }
206}
207
208#[derive(uniffi::Object)]
209pub struct RemoteSettings {
210 pub config: RemoteSettingsConfig,
211 client: Client,
212}
213
214#[uniffi::export]
215impl RemoteSettings {
216 #[uniffi::constructor]
218 #[handle_error(Error)]
219 pub fn new(remote_settings_config: RemoteSettingsConfig) -> ApiResult<Self> {
220 Ok(RemoteSettings {
221 config: remote_settings_config.clone(),
222 client: Client::new(remote_settings_config)?,
223 })
224 }
225
226 #[handle_error(Error)]
228 pub fn get_records(&self) -> ApiResult<RemoteSettingsResponse> {
229 let resp = self.client.get_records()?;
230 Ok(resp)
231 }
232
233 #[handle_error(Error)]
236 pub fn get_records_since(&self, timestamp: u64) -> ApiResult<RemoteSettingsResponse> {
237 let resp = self.client.get_records_since(timestamp)?;
238 Ok(resp)
239 }
240
241 #[handle_error(Error)]
243 pub fn download_attachment_to_path(
244 &self,
245 attachment_id: String,
246 path: String,
247 ) -> ApiResult<()> {
248 let resp = self.client.get_attachment(&attachment_id)?;
249 let mut file = File::create(path).map_err(Error::AttachmentFileError)?;
250 file.write_all(&resp).map_err(Error::AttachmentFileError)?;
251 Ok(())
252 }
253}
254
255impl RemoteSettings {
260 #[handle_error(Error)]
264 pub fn get_records_raw(&self) -> ApiResult<viaduct::Response> {
265 self.client.get_records_raw()
266 }
267
268 #[handle_error(Error)]
272 pub fn get_attachment(&self, attachment_location: &str) -> ApiResult<Vec<u8>> {
273 self.client.get_attachment(attachment_location)
274 }
275}
276
277#[cfg(test)]
278mod test {
279 use super::*;
280 use crate::RemoteSettingsRecord;
281 use mockito::{mock, Matcher};
282
283 #[test]
284 fn test_get_records() {
285 viaduct_dev::init_backend_dev();
286 let m = mock(
287 "GET",
288 "/v1/buckets/the-bucket/collections/the-collection/records",
289 )
290 .with_body(response_body())
291 .with_status(200)
292 .with_header("content-type", "application/json")
293 .with_header("etag", "\"1000\"")
294 .create();
295
296 let config = RemoteSettingsConfig {
297 server: Some(RemoteSettingsServer::Custom {
298 url: mockito::server_url(),
299 }),
300 server_url: None,
301 bucket_name: Some(String::from("the-bucket")),
302 collection_name: String::from("the-collection"),
303 };
304 let remote_settings = RemoteSettings::new(config).unwrap();
305
306 let resp = remote_settings.get_records().unwrap();
307
308 assert!(are_equal_json(JPG_ATTACHMENT, &resp.records[0]));
309 assert_eq!(1000, resp.last_modified);
310 m.expect(1).assert();
311 }
312
313 #[test]
314 fn test_get_records_since() {
315 viaduct_dev::init_backend_dev();
316 let m = mock(
317 "GET",
318 "/v1/buckets/the-bucket/collections/the-collection/records",
319 )
320 .match_query(Matcher::UrlEncoded("gt_last_modified".into(), "500".into()))
321 .with_body(response_body())
322 .with_status(200)
323 .with_header("content-type", "application/json")
324 .with_header("etag", "\"1000\"")
325 .create();
326
327 let config = RemoteSettingsConfig {
328 server: Some(RemoteSettingsServer::Custom {
329 url: mockito::server_url(),
330 }),
331 server_url: None,
332 bucket_name: Some(String::from("the-bucket")),
333 collection_name: String::from("the-collection"),
334 };
335 let remote_settings = RemoteSettings::new(config).unwrap();
336
337 let resp = remote_settings.get_records_since(500).unwrap();
338 assert!(are_equal_json(JPG_ATTACHMENT, &resp.records[0]));
339 assert_eq!(1000, resp.last_modified);
340 m.expect(1).assert();
341 }
342
343 #[allow(dead_code)]
349 fn test_download() {
350 viaduct_dev::init_backend_dev();
351 let config = RemoteSettingsConfig {
352 server: Some(RemoteSettingsServer::Custom {
353 url: "http://localhost:8888".into(),
354 }),
355 server_url: None,
356 bucket_name: Some(String::from("the-bucket")),
357 collection_name: String::from("the-collection"),
358 };
359 let remote_settings = RemoteSettings::new(config).unwrap();
360
361 remote_settings
362 .download_attachment_to_path(
363 "d3a5eccc-f0ca-42c3-b0bb-c0d4408c21c9.jpg".to_string(),
364 "test.jpg".to_string(),
365 )
366 .unwrap();
367 }
368
369 fn are_equal_json(str: &str, rec: &RemoteSettingsRecord) -> bool {
370 let r1: RemoteSettingsRecord = serde_json::from_str(str).unwrap();
371 &r1 == rec
372 }
373
374 fn response_body() -> String {
375 format!(
376 r#"
377 {{
378 "data": [
379 {},
380 {},
381 {}
382 ]
383 }}"#,
384 JPG_ATTACHMENT, PDF_ATTACHMENT, NO_ATTACHMENT
385 )
386 }
387
388 const JPG_ATTACHMENT: &str = r#"
389 {
390 "title": "jpg-attachment",
391 "content": "content",
392 "attachment": {
393 "filename": "jgp-attachment.jpg",
394 "location": "the-bucket/the-collection/d3a5eccc-f0ca-42c3-b0bb-c0d4408c21c9.jpg",
395 "hash": "2cbd593f3fd5f1585f92265433a6696a863bc98726f03e7222135ff0d8e83543",
396 "mimetype": "image/jpeg",
397 "size": 1374325
398 },
399 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d7",
400 "schema": 1677694447771,
401 "last_modified": 1677694949407
402 }
403 "#;
404
405 const PDF_ATTACHMENT: &str = r#"
406 {
407 "title": "with-attachment",
408 "content": "content",
409 "attachment": {
410 "filename": "pdf-attachment.pdf",
411 "location": "the-bucket/the-collection/5f7347c2-af92-411d-a65b-f794f9b5084c.pdf",
412 "hash": "de1cde3571ef3faa77ea0493276de9231acaa6f6651602e93aa1036f51181e9b",
413 "mimetype": "application/pdf",
414 "size": 157
415 },
416 "id": "ff301910-6bf5-4cfe-bc4c-5c80308661a5",
417 "schema": 1677694447771,
418 "last_modified": 1677694470354
419 }
420 "#;
421
422 const NO_ATTACHMENT: &str = r#"
423 {
424 "title": "no-attachment",
425 "content": "content",
426 "schema": 1677694447771,
427 "id": "7403c6f9-79be-4e0c-a37a-8f2b5bd7ad58",
428 "last_modified": 1677694455368
429 }
430 "#;
431}