1use crate::{
13 protocol::{self, Cohort, request::InstallSource},
14 storage::Storage,
15 time::PartialComplexTime,
16 version::Version,
17};
18use log::error;
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::fmt;
22use std::time::Duration;
23use typed_builder::TypedBuilder;
24
25#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30pub enum UserCounting {
31 ClientRegulatedByDate(
32 Option<u32>,
34 ),
35}
36
37impl From<Option<protocol::response::DayStart>> for UserCounting {
40 fn from(opt_day_start: Option<protocol::response::DayStart>) -> Self {
41 match opt_day_start {
42 Some(day_start) => UserCounting::ClientRegulatedByDate(day_start.elapsed_days),
43 None => UserCounting::ClientRegulatedByDate(None),
44 }
45 }
46}
47
48#[derive(Clone, Debug, Eq, PartialEq, TypedBuilder)]
50pub struct App {
51 #[builder(setter(into))]
53 pub id: String,
54
55 #[builder(setter(into))]
57 pub version: Version,
58
59 #[builder(default)]
63 #[builder(setter(into, strip_option))]
64 pub fingerprint: Option<String>,
65
66 #[builder(default)]
69 pub cohort: Cohort,
70
71 #[builder(default=UserCounting::ClientRegulatedByDate(None))]
74 pub user_counting: UserCounting,
75
76 #[builder(default)]
80 #[builder(setter(into))]
81 pub extra_fields: HashMap<String, String>,
82}
83
84#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
87pub struct PersistedApp {
88 pub cohort: Cohort,
89 pub user_counting: UserCounting,
90}
91
92impl From<&App> for PersistedApp {
93 fn from(app: &App) -> Self {
94 PersistedApp { cohort: app.cohort.clone(), user_counting: app.user_counting.clone() }
95 }
96}
97
98impl App {
99 pub async fn load<'a>(&'a mut self, storage: &'a impl Storage) {
101 if let Some(app_json) = storage.get_string(&self.id).await {
102 match serde_json::from_str::<PersistedApp>(&app_json) {
103 Ok(persisted_app) => {
104 if self.cohort.id.is_none() {
106 self.cohort.id = persisted_app.cohort.id;
107 }
108 if self.cohort.hint.is_none() {
109 self.cohort.hint = persisted_app.cohort.hint;
110 }
111 if self.cohort.name.is_none() {
112 self.cohort.name = persisted_app.cohort.name;
113 }
114 if self.user_counting == UserCounting::ClientRegulatedByDate(None) {
115 self.user_counting = persisted_app.user_counting;
116 }
117 }
118 Err(e) => {
119 error!("Unable to deserialize PersistedApp from json {}: {}", app_json, e);
120 }
121 }
122 }
123 }
124
125 pub async fn persist<'a>(&'a self, storage: &'a mut impl Storage) {
129 let persisted_app = PersistedApp::from(self);
130 match serde_json::to_string(&persisted_app) {
131 Ok(json) => {
132 if let Err(e) = storage.set_string(&self.id, &json).await {
133 error!("Unable to persist cohort id: {}", e);
134 }
135 }
136 Err(e) => {
137 error!("Unable to serialize PersistedApp {:?}: {}", persisted_app, e);
138 }
139 }
140 }
141
142 pub fn get_current_channel(&self) -> &str {
145 self.cohort.name.as_deref().unwrap_or("")
146 }
147
148 pub fn get_target_channel(&self) -> &str {
150 self.cohort.hint.as_deref().unwrap_or_else(|| self.get_current_channel())
151 }
152
153 pub fn set_target_channel(&mut self, channel: Option<String>, id: Option<String>) {
155 self.cohort.hint = channel;
156 if let Some(id) = id {
157 self.id = id;
158 }
159 }
160
161 pub fn valid(&self) -> bool {
162 !self.id.is_empty() && self.version != Version::from([0])
163 }
164}
165
166#[derive(Clone, Debug, Default, PartialEq, Eq)]
168pub struct CheckOptions {
169 pub source: InstallSource,
173}
174
175#[derive(Clone, Copy, Default, PartialEq, Eq, TypedBuilder)]
177pub struct UpdateCheckSchedule {
178 #[builder(default, setter(into))]
184 pub last_update_time: Option<PartialComplexTime>,
185
186 #[builder(default, setter(into))]
188 pub last_update_check_time: Option<PartialComplexTime>,
189
190 #[builder(default, setter(into))]
192 pub next_update_time: Option<CheckTiming>,
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq, TypedBuilder)]
200pub struct CheckTiming {
201 #[builder(setter(into))]
204 pub time: PartialComplexTime,
205
206 #[builder(default, setter(strip_option))]
210 pub minimum_wait: Option<Duration>,
211}
212
213pub struct PrettyOptionDisplay<T>(pub Option<T>)
222where
223 T: fmt::Display;
224impl<T> fmt::Display for PrettyOptionDisplay<T>
225where
226 T: fmt::Display,
227{
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 match &self.0 {
230 None => write!(f, "None"),
231 Some(value) => fmt::Display::fmt(value, f),
232 }
233 }
234}
235impl<T> fmt::Debug for PrettyOptionDisplay<T>
236where
237 T: fmt::Display,
238{
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 fmt::Display::fmt(self, f)
241 }
242}
243
244impl fmt::Debug for UpdateCheckSchedule {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.debug_struct("UpdateCheckSchedule")
253 .field("last_update_time", &PrettyOptionDisplay(self.last_update_time))
254 .field("next_update_time", &PrettyOptionDisplay(self.next_update_time))
255 .finish()
256 }
257}
258
259impl fmt::Display for CheckTiming {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match self.minimum_wait {
262 None => fmt::Display::fmt(&self.time, f),
263 Some(wait) => write!(f, "{} wait: {:?}", self.time, wait),
264 }
265 }
266}
267
268#[derive(Clone, Debug, Default, Eq, PartialEq)]
272pub struct ProtocolState {
273 pub server_dictated_poll_interval: Option<std::time::Duration>,
276
277 pub consecutive_failed_update_checks: u32,
279
280 pub consecutive_proxied_requests: u32,
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::{
289 storage::MemStorage,
290 time::{MockTimeSource, TimeSource},
291 };
292 use futures::executor::block_on;
293 use pretty_assertions::assert_eq;
294 use std::str::FromStr;
295 use std::time::SystemTime;
296
297 #[test]
298 fn test_app_new_version() {
299 let app = App::builder()
300 .id("some_id")
301 .version([1, 2])
302 .cohort(Cohort::from_hint("some-channel"))
303 .build();
304 assert_eq!(app.id, "some_id");
305 assert_eq!(app.version, [1, 2].into());
306 assert_eq!(app.fingerprint, None);
307 assert_eq!(app.cohort.hint, Some("some-channel".to_string()));
308 assert_eq!(app.cohort.name, None);
309 assert_eq!(app.cohort.id, None);
310 assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
311 assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
312 }
313
314 #[test]
315 fn test_app_with_fingerprint() {
316 let app = App::builder()
317 .id("some_id_2")
318 .version([4, 6])
319 .cohort(Cohort::from_hint("test-channel"))
320 .fingerprint("some_fp")
321 .build();
322 assert_eq!(app.id, "some_id_2");
323 assert_eq!(app.version, [4, 6].into());
324 assert_eq!(app.fingerprint, Some("some_fp".to_string()));
325 assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
326 assert_eq!(app.cohort.name, None);
327 assert_eq!(app.cohort.id, None);
328 assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
329 assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
330 }
331
332 #[test]
333 fn test_app_with_user_counting() {
334 let app = App::builder()
335 .id("some_id_2")
336 .version([4, 6])
337 .cohort(Cohort::from_hint("test-channel"))
338 .user_counting(UserCounting::ClientRegulatedByDate(Some(42)))
339 .build();
340 assert_eq!(app.id, "some_id_2");
341 assert_eq!(app.version, [4, 6].into());
342 assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
343 assert_eq!(app.cohort.name, None);
344 assert_eq!(app.cohort.id, None);
345 assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(Some(42)));
346 assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
347 }
348
349 #[test]
350 fn test_app_with_extras() {
351 let app = App::builder()
352 .id("some_id_2")
353 .version([4, 6])
354 .cohort(Cohort::from_hint("test-channel"))
355 .extra_fields([
356 ("key1".to_string(), "value1".to_string()),
357 ("key2".to_string(), "value2".to_string()),
358 ])
359 .build();
360 assert_eq!(app.id, "some_id_2");
361 assert_eq!(app.version, [4, 6].into());
362 assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
363 assert_eq!(app.cohort.name, None);
364 assert_eq!(app.cohort.id, None);
365 assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
366 assert_eq!(app.extra_fields.len(), 2);
367 assert_eq!(app.extra_fields["key1"], "value1");
368 assert_eq!(app.extra_fields["key2"], "value2");
369 }
370
371 #[test]
372 fn test_app_load() {
373 block_on(async {
374 let mut storage = MemStorage::new();
375 let json = serde_json::json!({
376 "cohort": {
377 "cohort": "some_id",
378 "cohorthint":"some_hint",
379 "cohortname": "some_name"
380 },
381 "user_counting": {
382 "ClientRegulatedByDate":123
383 }});
384 let json = serde_json::to_string(&json).unwrap();
385 let mut app = App::builder().id("some_id").version([1, 2]).build();
386 storage.set_string(&app.id, &json).await.unwrap();
387 app.load(&storage).await;
388
389 let cohort = Cohort {
390 id: Some("some_id".to_string()),
391 hint: Some("some_hint".to_string()),
392 name: Some("some_name".to_string()),
393 };
394 assert_eq!(cohort, app.cohort);
395 assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
396 });
397 }
398
399 #[test]
400 fn test_app_load_empty_storage() {
401 block_on(async {
402 let storage = MemStorage::new();
403 let cohort = Cohort {
404 id: Some("some_id".to_string()),
405 hint: Some("some_hint".to_string()),
406 name: Some("some_name".to_string()),
407 };
408 let mut app = App::builder()
409 .id("some_id")
410 .version([1, 2])
411 .cohort(cohort)
412 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
413 .build();
414 app.load(&storage).await;
415
416 let cohort = Cohort {
418 id: Some("some_id".to_string()),
419 hint: Some("some_hint".to_string()),
420 name: Some("some_name".to_string()),
421 };
422 assert_eq!(cohort, app.cohort);
423 assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
424 });
425 }
426
427 #[test]
428 fn test_app_load_malformed() {
429 block_on(async {
430 let mut storage = MemStorage::new();
431 let cohort = Cohort {
432 id: Some("some_id".to_string()),
433 hint: Some("some_hint".to_string()),
434 name: Some("some_name".to_string()),
435 };
436 let mut app = App::builder()
437 .id("some_id")
438 .version([1, 2])
439 .cohort(cohort)
440 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
441 .build();
442 storage.set_string(&app.id, "not a json").await.unwrap();
443 app.load(&storage).await;
444
445 let cohort = Cohort {
447 id: Some("some_id".to_string()),
448 hint: Some("some_hint".to_string()),
449 name: Some("some_name".to_string()),
450 };
451 assert_eq!(cohort, app.cohort);
452 assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
453 });
454 }
455
456 #[test]
457 fn test_app_load_partial() {
458 block_on(async {
459 let mut storage = MemStorage::new();
460 let json = serde_json::json!({
461 "cohort": {
462 "cohorthint":"some_hint_2",
463 "cohortname": "some_name_2"
464 },
465 "user_counting": {
466 "ClientRegulatedByDate":null
467 }});
468 let json = serde_json::to_string(&json).unwrap();
469 let cohort = Cohort {
470 id: Some("some_id".to_string()),
471 hint: Some("some_hint".to_string()),
472 name: Some("some_name".to_string()),
473 };
474 let mut app = App::builder()
475 .id("some_id")
476 .version([1, 2])
477 .cohort(cohort)
478 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
479 .build();
480 storage.set_string(&app.id, &json).await.unwrap();
481 app.load(&storage).await;
482
483 let cohort = Cohort {
485 id: Some("some_id".to_string()),
486 hint: Some("some_hint".to_string()),
487 name: Some("some_name".to_string()),
488 };
489 assert_eq!(cohort, app.cohort);
490 assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
491 });
492 }
493
494 #[test]
495 fn test_app_load_override() {
496 block_on(async {
497 let mut storage = MemStorage::new();
498 let json = serde_json::json!({
499 "cohort": {
500 "cohort": "some_id_2",
501 "cohorthint":"some_hint_2",
502 "cohortname": "some_name_2"
503 },
504 "user_counting": {
505 "ClientRegulatedByDate":123
506 }});
507 let json = serde_json::to_string(&json).unwrap();
508 let cohort = Cohort {
509 id: Some("some_id".to_string()),
510 hint: Some("some_hint".to_string()),
511 name: None,
512 };
513 let mut app = App::builder()
514 .id("some_id")
515 .version([1, 2])
516 .cohort(cohort)
517 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
518 .build();
519 storage.set_string(&app.id, &json).await.unwrap();
520 app.load(&storage).await;
521
522 let cohort = Cohort {
524 id: Some("some_id".to_string()),
525 hint: Some("some_hint".to_string()),
526 name: Some("some_name_2".to_string()),
527 };
528 assert_eq!(cohort, app.cohort);
529 assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting);
530 });
531 }
532
533 #[test]
534 fn test_app_persist() {
535 block_on(async {
536 let mut storage = MemStorage::new();
537 let cohort = Cohort {
538 id: Some("some_id".to_string()),
539 hint: Some("some_hint".to_string()),
540 name: Some("some_name".to_string()),
541 };
542 let app = App::builder()
543 .id("some_id")
544 .version([1, 2])
545 .cohort(cohort)
546 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
547 .build();
548 app.persist(&mut storage).await;
549
550 let expected = serde_json::json!({
551 "cohort": {
552 "cohort": "some_id",
553 "cohorthint":"some_hint",
554 "cohortname": "some_name"
555 },
556 "user_counting": {
557 "ClientRegulatedByDate":123
558 }});
559 let json = storage.get_string(&app.id).await.unwrap();
560 assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
561 assert!(!storage.committed());
562 });
563 }
564
565 #[test]
566 fn test_app_persist_empty() {
567 block_on(async {
568 let mut storage = MemStorage::new();
569 let cohort = Cohort { id: None, hint: None, name: None };
570 let app = App::builder().id("some_id").version([1, 2]).cohort(cohort).build();
571 app.persist(&mut storage).await;
572
573 let expected = serde_json::json!({
574 "cohort": {},
575 "user_counting": {
576 "ClientRegulatedByDate":null
577 }});
578 let json = storage.get_string(&app.id).await.unwrap();
579 assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
580 assert!(!storage.committed());
581 });
582 }
583
584 #[test]
585 fn test_app_get_current_channel() {
586 let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() };
587 let app = App::builder().id("some_id").version([0, 1]).cohort(cohort).build();
588 assert_eq!("current-channel-123", app.get_current_channel());
589 }
590
591 #[test]
592 fn test_app_get_current_channel_default() {
593 let app = App::builder().id("some_id").version([0, 1]).build();
594 assert_eq!("", app.get_current_channel());
595 }
596
597 #[test]
598 fn test_app_get_target_channel() {
599 let cohort = Cohort::from_hint("target-channel-456");
600 let app = App::builder().id("some_id").version([0, 1]).cohort(cohort).build();
601 assert_eq!("target-channel-456", app.get_target_channel());
602 }
603
604 #[test]
605 fn test_app_get_target_channel_fallback() {
606 let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() };
607 let app = App::builder().id("some_id").version([0, 1]).cohort(cohort).build();
608 assert_eq!("current-channel-123", app.get_target_channel());
609 }
610
611 #[test]
612 fn test_app_get_target_channel_default() {
613 let app = App::builder().id("some_id").version([0, 1]).build();
614 assert_eq!("", app.get_target_channel());
615 }
616
617 #[test]
618 fn test_app_set_target_channel() {
619 let mut app = App::builder().id("some_id").version([0, 1]).build();
620 assert_eq!("", app.get_target_channel());
621 app.set_target_channel(Some("new-target-channel".to_string()), None);
622 assert_eq!("new-target-channel", app.get_target_channel());
623 app.set_target_channel(None, None);
624 assert_eq!("", app.get_target_channel());
625 }
626
627 #[test]
628 fn test_app_set_target_channel_and_id() {
629 let mut app = App::builder().id("some_id").version([0, 1]).build();
630 assert_eq!("", app.get_target_channel());
631 app.set_target_channel(Some("new-target-channel".to_string()), Some("new-id".to_string()));
632 assert_eq!("new-target-channel", app.get_target_channel());
633 assert_eq!("new-id", app.id);
634 app.set_target_channel(None, None);
635 assert_eq!("", app.get_target_channel());
636 assert_eq!("new-id", app.id);
637 }
638
639 #[test]
640 fn test_app_valid() {
641 let app = App::builder().id("some_id").version([0, 1]).build();
642 assert!(app.valid());
643 }
644
645 #[test]
646 fn test_app_not_valid() {
647 let app = App::builder().id("").version([0, 1]).build();
648 assert!(!app.valid());
649 let app = App::builder().id("some_id").version([0]).build();
650 assert!(!app.valid());
651 }
652
653 #[test]
654 fn test_pretty_option_display_with_none() {
655 assert_eq!("None", format!("{:?}", PrettyOptionDisplay(Option::<String>::None)));
656 }
657
658 #[test]
659 fn test_pretty_option_display_with_some() {
660 assert_eq!("this is a test", format!("{:?}", PrettyOptionDisplay(Some("this is a test"))));
661 }
662
663 #[test]
664 fn test_update_check_schedule_debug_with_defaults() {
665 assert_eq!(
666 "UpdateCheckSchedule { \
667 last_update_time: None, \
668 next_update_time: None \
669 }",
670 format!("{:?}", UpdateCheckSchedule::default())
671 );
672 }
673
674 #[test]
675 fn test_update_check_schedule_debug_with_values() {
676 let mock_time = MockTimeSource::new_from_now();
677 let last = mock_time.now();
678 let next = last + Duration::from_secs(1000);
679 assert_eq!(
680 format!(
681 "UpdateCheckSchedule {{ last_update_time: {}, next_update_time: {} }}",
682 PartialComplexTime::from(last),
683 next
684 ),
685 format!(
686 "{:?}",
687 UpdateCheckSchedule::builder()
688 .last_update_time(last)
689 .next_update_time(CheckTiming::builder().time(next).build())
690 .build()
691 )
692 );
693 }
694
695 #[test]
696 fn test_update_check_schedule_builder_all_fields() {
697 let mock_time = MockTimeSource::new_from_now();
698 let now = PartialComplexTime::from(mock_time.now());
699 assert_eq!(
700 UpdateCheckSchedule::builder()
701 .last_update_time(PartialComplexTime::from(
702 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
703 ))
704 .next_update_time(
705 CheckTiming::builder().time(now).minimum_wait(Duration::from_secs(100)).build()
706 )
707 .build(),
708 UpdateCheckSchedule {
709 last_update_time: Some(PartialComplexTime::from(
710 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
711 )),
712 next_update_time: Some(CheckTiming {
713 time: now,
714 minimum_wait: Some(Duration::from_secs(100))
715 }),
716 ..Default::default()
717 }
718 );
719 }
720
721 #[test]
722 fn test_update_check_schedule_builder_all_fields_from_options() {
723 let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
724 assert_eq!(
725 UpdateCheckSchedule::builder()
726 .last_update_time(Some(PartialComplexTime::from(
727 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
728 )))
729 .next_update_time(Some(
730 CheckTiming::builder()
731 .time(next_time)
732 .minimum_wait(Duration::from_secs(100))
733 .build()
734 ))
735 .build(),
736 UpdateCheckSchedule {
737 last_update_time: Some(PartialComplexTime::from(
738 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
739 )),
740 next_update_time: Some(CheckTiming {
741 time: next_time,
742 minimum_wait: Some(Duration::from_secs(100))
743 }),
744 ..Default::default()
745 }
746 );
747 }
748
749 #[test]
750 fn test_update_check_schedule_builder_subset_fields() {
751 assert_eq!(
752 UpdateCheckSchedule::builder()
753 .last_update_time(PartialComplexTime::from(
754 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
755 ))
756 .build(),
757 UpdateCheckSchedule {
758 last_update_time: Some(PartialComplexTime::from(
759 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
760 )),
761 ..Default::default()
762 }
763 );
764
765 let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
766 assert_eq!(
767 UpdateCheckSchedule::builder()
768 .next_update_time(
769 CheckTiming::builder()
770 .time(next_time)
771 .minimum_wait(Duration::from_secs(5))
772 .build()
773 )
774 .build(),
775 UpdateCheckSchedule {
776 next_update_time: Some(CheckTiming {
777 time: next_time,
778 minimum_wait: Some(Duration::from_secs(5))
779 }),
780 ..Default::default()
781 }
782 );
783 }
784
785 #[test]
786 fn test_update_check_schedule_builder_defaults_are_same_as_default_impl() {
787 assert_eq!(UpdateCheckSchedule::builder().build(), UpdateCheckSchedule::default());
788 }
789}